Skip to content

Commit cb22f80

Browse files
committed
pci-placement: Isolate pcie-expander-bus per IOMMU device when sharing a NUMA node
On a 2-superchip GB200, each Grace CPU has 2 Blackwell GPUs connected via NVLink-C2C that share the same sysfs NUMA node as the CPU socket: GPU 1 (0008:01:00.0): NUMA 0 (Grace CPU kubevirt#1) GPU 2 (0009:01:00.0): NUMA 0 (Grace CPU kubevirt#1) GPU 3 (0018:01:00.0): NUMA 1 (Grace CPU kubevirt#2) GPU 4 (0019:01:00.0): NUMA 1 (Grace CPU kubevirt#2) VEP 115's per-NUMA expander bus placement puts both GPUs on the same pcie-expander-bus, which means they share a single IOMMU device created by placeDevice. On ARM64 Grace systems this shared SMMUv3 causes CMD_SYNC timeout and tegra241_cmdqv errors in the guest, preventing multi-GPU initialization. When multiple IOMMU devices (marked with ACPI NodeSet "tofill") share a NUMA node, each now gets its own pcie-expander-bus controller. A single IOMMU device on a NUMA node continues to use the shared per-NUMA expander bus. Non-IOMMU devices always share a pcie-expander-bus per NUMA node. Ref: VOYAGER-707 Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Lee Yarwood <lyarwood@redhat.com>
1 parent 1eeb4f5 commit cb22f80

2 files changed

Lines changed: 186 additions & 19 deletions

File tree

pkg/virt-launcher/virtwrap/converter/pci-placement.go

Lines changed: 58 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ type expanderBusAssigner struct {
188188
controllerIndex uint32
189189
controllerCount uint32
190190
topologyMap map[uint32]*numaAwareTopology
191+
deviceTopologies []*numaAwareTopology
191192
devices map[string]*api.HostDevice
192193
devicesNUMANodes map[string]uint32
193194

@@ -329,6 +330,17 @@ func (a *expanderBusAssigner) getNumaAwareTopology(numaKey uint32) *numaAwareTop
329330
return topology
330331
}
331332

333+
// createDeviceTopology creates a dedicated topology for a single device,
334+
// with its own expander bus tagged to the specified NUMA node.
335+
func (a *expanderBusAssigner) createDeviceTopology(numaKey uint32) *numaAwareTopology {
336+
topology := &numaAwareTopology{
337+
expanderBus: a.createController(api.ControllerModelPCIeExpanderBus, "", 0, &numaKey),
338+
addressPerDeviceSourcePCI: make(map[string]*api.Address),
339+
}
340+
a.deviceTopologies = append(a.deviceTopologies, topology)
341+
return topology
342+
}
343+
332344
// addRootPort creates a PCIe root port and adds it to the topology.
333345
func (a *expanderBusAssigner) addRootPort(topology *numaAwareTopology, parentBus string) *api.Controller {
334346
slot := uint32(len(topology.rootPorts))
@@ -397,34 +409,58 @@ func (a *expanderBusAssigner) placeDevice(topology *numaAwareTopology, device *a
397409
return nil
398410
}
399411

400-
// buildTopology groups devices by NUMA node by using a pcie-expander-bus per
401-
// NUMA node. Within a pcie-expander-bus one pcie-root-port per device is created.
402-
// Each device is then placed behind its respective root port.
403-
//
404-
// pcie-expander-bus (one per NUMA node) -> pcie-root-port (one per device) -> device
412+
// buildTopology groups devices by NUMA node. When multiple IOMMU devices
413+
// (marked with ACPI NodeSet "tofill") share a NUMA node, each gets its own
414+
// pcie-expander-bus to avoid sharing a single IOMMU device. A single IOMMU
415+
// device on a NUMA node shares the per-NUMA expander bus. Non-IOMMU devices
416+
// always share a pcie-expander-bus per NUMA node. Within each expander bus,
417+
// one pcie-root-port per device is created.
405418
//
406-
// It modifies the topology per NUMA node in place by creating the necessary controllers
419+
// It modifies the topology in place by creating the necessary controllers
407420
// and updating the addresses of the devices.
408421
func (a *expanderBusAssigner) buildTopology() error {
409422
numaDeviceGroups := a.groupDevicesByNUMA()
410423

424+
iommuCountPerNUMA := make(map[uint32]int)
425+
for numaKey, devices := range numaDeviceGroups {
426+
for _, device := range devices {
427+
if device.ACPI != nil && device.ACPI.NodeSet == "tofill" {
428+
iommuCountPerNUMA[numaKey]++
429+
}
430+
}
431+
}
432+
411433
for numaKey, devices := range numaDeviceGroups {
412-
topology := a.getNumaAwareTopology(numaKey)
434+
needsPerDeviceIsolation := iommuCountPerNUMA[numaKey] > 1
413435

414436
for _, device := range devices {
437+
isIOMMUDevice := device.ACPI != nil && device.ACPI.NodeSet == "tofill"
438+
439+
var topology *numaAwareTopology
440+
if isIOMMUDevice && needsPerDeviceIsolation {
441+
topology = a.createDeviceTopology(numaKey)
442+
} else {
443+
topology = a.getNumaAwareTopology(numaKey)
444+
}
445+
415446
if err := a.placeDevice(topology, device); err != nil {
416447
return fmt.Errorf("failed to place device %s: %w", hardware.PCIAddressToString(device.Source.Address), err)
417448
}
418-
}
419449

420-
// Set the busNr of the expander bus so that it has enough space for all
421-
// its children. We start from 254 (1 expander bus + 1 root port, when one
422-
// device is aligned with a NUMA node) and go downwards to leave space for
423-
// system controllers and additional expander buses.
424-
busNr := maxExpanderBusNr - a.controllerCount + 1
425-
topology.expanderBus.Target.BusNr = ptr.To(busNr)
450+
if isIOMMUDevice && needsPerDeviceIsolation {
451+
busNr := maxExpanderBusNr - a.controllerCount + 1
452+
topology.expanderBus.Target.BusNr = ptr.To(busNr)
453+
a.lastAssignedBusNr = busNr
454+
}
455+
}
426456

427-
a.lastAssignedBusNr = busNr
457+
// Set the busNr of the shared expander bus (if any devices used it)
458+
// so that it has enough space for all its children.
459+
if topology, exists := a.topologyMap[numaKey]; exists {
460+
busNr := maxExpanderBusNr - a.controllerCount + 1
461+
topology.expanderBus.Target.BusNr = ptr.To(busNr)
462+
a.lastAssignedBusNr = busNr
463+
}
428464
}
429465

430466
return nil
@@ -440,7 +476,13 @@ func (a *expanderBusAssigner) PlaceNumaAlignedDevices() error {
440476
return fmt.Errorf("failed to create PCIe topology with NUMA alignment: %w", err)
441477
}
442478

443-
for _, topology := range a.topologyMap {
479+
allTopologies := make([]*numaAwareTopology, 0, len(a.topologyMap)+len(a.deviceTopologies))
480+
for _, t := range a.topologyMap {
481+
allTopologies = append(allTopologies, t)
482+
}
483+
allTopologies = append(allTopologies, a.deviceTopologies...)
484+
485+
for _, topology := range allTopologies {
444486
a.domainSpec.Devices.Controllers = append(a.domainSpec.Devices.Controllers, *topology.expanderBus)
445487

446488
for _, rootPort := range topology.rootPorts {
@@ -451,9 +493,6 @@ func (a *expanderBusAssigner) PlaceNumaAlignedDevices() error {
451493
if device, exists := a.devices[sourceAddress]; exists {
452494
device.Address = address
453495
}
454-
// If a device was not placed in the topology (e.g. missing vCPU
455-
// affinity information), we leave it unmodified so that it can be
456-
// placed by the root slot assigner.
457496
}
458497
if topology.iommuDev != nil {
459498
a.domainSpec.Devices.IOMMU = append(a.domainSpec.Devices.IOMMU, *topology.iommuDev)

pkg/virt-launcher/virtwrap/converter/pci-placement_test.go

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,12 @@ var _ = Describe("PCIe Expander Bus Assigner", func() {
7272
}
7373
}
7474

75+
createIOMMUPCIDevice := func(alias, bus string) api.HostDevice {
76+
dev := createPCIDevice(alias, bus)
77+
dev.ACPI = &api.ACPIHostDev{NodeSet: "tofill"}
78+
return dev
79+
}
80+
7581
createNonPCIDevice := func(deviceType string) api.HostDevice {
7682
return api.HostDevice{
7783
Type: deviceType,
@@ -312,9 +318,131 @@ var _ = Describe("PCIe Expander Bus Assigner", func() {
312318
devices: []api.HostDevice{createPCIDevice("device1", "0x01")},
313319
expectedControllers: 0,
314320
}),
321+
Entry("places single IOMMU device with dedicated expander bus", devicePlacementTestCase{
322+
name: "single IOMMU device",
323+
devices: []api.HostDevice{createIOMMUPCIDevice("gpu1", "0x01")},
324+
expectedControllers: 2,
325+
expectedExpanderBuses: 1,
326+
expectedRootPorts: 1,
327+
}),
328+
Entry("places multiple IOMMU devices on same NUMA node with separate expander buses", devicePlacementTestCase{
329+
name: "multiple IOMMU devices same NUMA",
330+
devices: []api.HostDevice{
331+
createIOMMUPCIDevice("gpu1", "0x01"),
332+
createIOMMUPCIDevice("gpu2", "0x03"),
333+
},
334+
expectedControllers: 4,
335+
expectedExpanderBuses: 2,
336+
expectedRootPorts: 2,
337+
}),
338+
Entry("places IOMMU devices on different NUMA nodes with separate expander buses", devicePlacementTestCase{
339+
name: "IOMMU devices on different NUMA nodes",
340+
devices: []api.HostDevice{
341+
createIOMMUPCIDevice("gpu_numa0", "0x01"),
342+
createIOMMUPCIDevice("gpu_numa1", "0x02"),
343+
},
344+
expectedControllers: 4,
345+
expectedExpanderBuses: 2,
346+
expectedRootPorts: 2,
347+
}),
315348
)
316349
})
317350

351+
Describe("IOMMU device topology isolation", func() {
352+
var (
353+
domainSpec *api.DomainSpec
354+
iommuPCI *iommupci.IommuPCI
355+
)
356+
357+
BeforeEach(func() {
358+
domainSpec = createDomainSpecWithNUMA(
359+
[]api.NUMACell{{ID: "0", CPUs: "0-1"}, {ID: "1", CPUs: "2-3"}},
360+
[]api.CPUTuneVCPUPin{{VCPU: 0, CPUSet: "0"}, {VCPU: 2, CPUSet: "4"}},
361+
)
362+
iommuPCI = iommupci.NewIommuPCI(runtime.GOARCH)
363+
})
364+
365+
It("should create separate smmuv3 IOMMU devices for each GPU on the same NUMA node", func() {
366+
domainSpec.Devices.HostDevices = []api.HostDevice{
367+
createIOMMUPCIDevice("gpu1", "0x01"),
368+
createIOMMUPCIDevice("gpu2", "0x03"),
369+
}
370+
371+
err := PlacePCIDevicesWithNUMAAlignment(domainSpec, iommuPCI)
372+
Expect(err).ToNot(HaveOccurred())
373+
374+
Expect(domainSpec.Devices.IOMMU).To(HaveLen(2), "each GPU should have its own smmuv3")
375+
Expect(domainSpec.Devices.IOMMU[0].Model).To(Equal("smmuv3"))
376+
Expect(domainSpec.Devices.IOMMU[1].Model).To(Equal("smmuv3"))
377+
378+
Expect(domainSpec.Devices.IOMMU[0].Driver.PciBus).ToNot(Equal(domainSpec.Devices.IOMMU[1].Driver.PciBus),
379+
"each smmuv3 should reference a different expander bus")
380+
})
381+
382+
It("should assign non-overlapping bus numbers to per-device expander buses", func() {
383+
domainSpec.Devices.HostDevices = []api.HostDevice{
384+
createIOMMUPCIDevice("gpu1", "0x01"),
385+
createIOMMUPCIDevice("gpu2", "0x03"),
386+
}
387+
388+
err := PlacePCIDevicesWithNUMAAlignment(domainSpec, iommuPCI)
389+
Expect(err).ToNot(HaveOccurred())
390+
391+
busNumbers := map[uint32]bool{}
392+
for _, controller := range domainSpec.Devices.Controllers {
393+
if controller.Model == api.ControllerModelPCIeExpanderBus {
394+
Expect(controller.Target).ToNot(BeNil())
395+
Expect(controller.Target.BusNr).ToNot(BeNil())
396+
busNr := *controller.Target.BusNr
397+
Expect(busNumbers[busNr]).To(BeFalse(), "bus number %d assigned twice", busNr)
398+
busNumbers[busNr] = true
399+
}
400+
}
401+
Expect(busNumbers).To(HaveLen(2))
402+
})
403+
404+
It("should use shared per-NUMA expander bus when only one IOMMU device on a NUMA node", func() {
405+
domainSpec.Devices.HostDevices = []api.HostDevice{
406+
createIOMMUPCIDevice("gpu1", "0x01"),
407+
createPCIDevice("nic1", "0x03"),
408+
}
409+
410+
err := PlacePCIDevicesWithNUMAAlignment(domainSpec, iommuPCI)
411+
Expect(err).ToNot(HaveOccurred())
412+
413+
expanderBuses := 0
414+
for _, controller := range domainSpec.Devices.Controllers {
415+
if controller.Model == api.ControllerModelPCIeExpanderBus {
416+
expanderBuses++
417+
}
418+
}
419+
Expect(expanderBuses).To(Equal(1), "single IOMMU device should share the per-NUMA expander bus")
420+
421+
Expect(domainSpec.Devices.IOMMU).To(HaveLen(1), "single IOMMU device should still get an smmuv3")
422+
Expect(domainSpec.Devices.IOMMU[0].Model).To(Equal("smmuv3"))
423+
})
424+
425+
It("should assign each GPU device to its own root port on separate expander buses", func() {
426+
domainSpec.Devices.HostDevices = []api.HostDevice{
427+
createIOMMUPCIDevice("gpu1", "0x01"),
428+
createIOMMUPCIDevice("gpu2", "0x03"),
429+
}
430+
431+
err := PlacePCIDevicesWithNUMAAlignment(domainSpec, iommuPCI)
432+
Expect(err).ToNot(HaveOccurred())
433+
434+
deviceBuses := map[string]bool{}
435+
for _, device := range domainSpec.Devices.HostDevices {
436+
Expect(device.Address).ToNot(BeNil())
437+
Expect(device.Address.Bus).ToNot(BeEmpty())
438+
Expect(deviceBuses[device.Address.Bus]).To(BeFalse(),
439+
"bus %s used by multiple devices", device.Address.Bus)
440+
deviceBuses[device.Address.Bus] = true
441+
}
442+
Expect(deviceBuses).To(HaveLen(2))
443+
})
444+
})
445+
318446
Describe("PlacePCIDevicesWithNUMAAlignment", func() {
319447
var (
320448
domainSpec *api.DomainSpec

0 commit comments

Comments
 (0)