Skip to content

Commit e222fd7

Browse files
committed
PCIe NUMA topology: support cross-NUMA device placement
On NVIDIA GB200 Grace Blackwell systems, GPUs are on separate NUMA nodes from all CPUs by design. A 2-superchip GB200 exposes: NUMA 0: Grace CPU kubevirt#1 (CPUs 0-71, ~490 GB LPDDR5X) NUMA 1: Grace CPU kubevirt#2 (CPUs 72-143, ~490 GB LPDDR5X) NUMA 2: Blackwell GPU kubevirt#1 (no CPUs, ~188 GB HBM) NUMA 10: Blackwell GPU kubevirt#2 (no CPUs, ~188 GB HBM) The existing PCIe NUMA-aware topology code discovers the host NUMA node for each passthrough device but discards it when no vCPUs are pinned to that node, causing the device to fall back to the default pci.0 bus without NUMA affinity information. Extend LookupDevicesNumaNodes to return both aligned devices (host NUMA node has pinned vCPUs) and unaligned devices (host NUMA node has no pinned vCPUs but is a valid NUMA node). When the expander bus assigner encounters an unaligned device, it creates a CPU-less guest NUMA cell for the device's host NUMA node and places the device under a pcie-expander-bus targeting that cell. On a GB200 each GPU naturally gets its own expander bus and SMMUv3 IOMMU device through the standard per-NUMA topology path, since each GPU is on its own NUMA node. Also adds an explicit check for NUMA node -1 in GetDeviceNumaNode to avoid relying on uint32 overflow for devices without NUMA affinity. Ref: https://docs.nvidia.com/dccpu/grace-perf-tuning-guide/system.html Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Lee Yarwood <lyarwood@redhat.com>
1 parent 42435eb commit e222fd7

4 files changed

Lines changed: 306 additions & 32 deletions

File tree

pkg/util/hardware/hw_utils.go

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -197,11 +197,18 @@ func PCIAddressToString(pciBusID *api.Address) string {
197197
// LookupDevicesNumaNodes looks up the NUMA nodes of multiple devices based on
198198
// their PCI addresses and the domain spec of the virtual machine.
199199
//
200-
// It returns a map of PCI addresses to their corresponding NUMA node IDs.
201-
func LookupDevicesNumaNodes(pciAddresses []string, domainSpec *api.DomainSpec) map[string]uint32 {
202-
results := make(map[string]uint32)
200+
// It returns two maps:
201+
// - aligned: PCI addresses mapped to guest NUMA cell IDs where the device's
202+
// host NUMA node has vCPUs pinned to it.
203+
// - unaligned: PCI addresses mapped to host NUMA node IDs where the device's
204+
// host NUMA node has no pinned vCPUs. The caller can use this to create
205+
// CPU-less guest NUMA cells for cross-NUMA device placement (e.g., NVIDIA
206+
// GB200 where the GPU is on a separate NUMA node from all CPUs).
207+
func LookupDevicesNumaNodes(pciAddresses []string, domainSpec *api.DomainSpec) (aligned map[string]uint32, unaligned map[string]uint32) {
208+
aligned = make(map[string]uint32)
209+
unaligned = make(map[string]uint32)
203210
if len(pciAddresses) == 0 || domainSpec == nil || domainSpec.CPU.NUMA == nil || domainSpec.CPUTune == nil {
204-
return results
211+
return
205212
}
206213

207214
// pcpu -> vcpu mapping
@@ -242,28 +249,46 @@ func LookupDevicesNumaNodes(pciAddresses []string, domainSpec *api.DomainSpec) m
242249
if err != nil {
243250
continue
244251
}
252+
253+
// Skip devices that report no NUMA affinity (-1 wraps to max uint32)
254+
if int32(*deviceNuma) == -1 {
255+
continue
256+
}
257+
245258
var pcpus []uint32
246259
// if another device is already on this NUMA node, use the same pcpu set
247260
if res, exists := pNumaToPCPUSetMap[*deviceNuma]; exists {
248261
pcpus = res
249262
} else {
250263
pcpusNuma, err := GetNumaNodeCPUList(int(*deviceNuma))
251264
if err != nil {
252-
continue
253-
}
254-
for _, pcpu := range pcpusNuma {
255-
pcpus = append(pcpus, uint32(pcpu))
265+
// NUMA node may have no CPUs (e.g., GPU-only NUMA node)
266+
pNumaToPCPUSetMap[*deviceNuma] = nil
267+
pcpus = nil
268+
} else {
269+
for _, pcpu := range pcpusNuma {
270+
pcpus = append(pcpus, uint32(pcpu))
271+
}
272+
pNumaToPCPUSetMap[*deviceNuma] = pcpus
256273
}
257-
pNumaToPCPUSetMap[*deviceNuma] = pcpus
258274
}
259275

276+
found := false
260277
for _, pcpu := range pcpus {
261278
if vCPU, exist := p2vCPUMap[pcpu]; exist {
262279
cellID := vCPUToCellMap[vCPU]
263-
results[pciAddress] = cellID
280+
aligned[pciAddress] = cellID
281+
found = true
264282
break
265283
}
266284
}
285+
286+
// Device is on a host NUMA node with no pinned vCPUs.
287+
// Record the host NUMA node so the caller can create a
288+
// CPU-less guest NUMA cell for it.
289+
if !found {
290+
unaligned[pciAddress] = *deviceNuma
291+
}
267292
}
268-
return results
293+
return
269294
}

pkg/util/hardware/hw_utils_test.go

Lines changed: 65 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ import (
3434

3535
var _ = Describe("Hardware utils test", func() {
3636
const (
37-
testPCIAddress = "0000:00:01.0"
37+
testPCIAddress = "0000:00:01.0"
38+
testPCIAddress2 = "0000:00:02.0"
3839
)
3940

4041
var (
@@ -53,7 +54,7 @@ var _ = Describe("Hardware utils test", func() {
5354
fakeNodeBasePath, err = os.MkdirTemp("", "numa_nodes")
5455
Expect(err).ToNot(HaveOccurred())
5556

56-
// Create test PCI device with NUMA node
57+
// Create test PCI device on NUMA node 0
5758
pciDevicePath := filepath.Join(fakePciBasePath, testPCIAddress)
5859
err = os.MkdirAll(pciDevicePath, 0o755)
5960
Expect(err).ToNot(HaveOccurred())
@@ -62,6 +63,15 @@ var _ = Describe("Hardware utils test", func() {
6263
err = os.WriteFile(numaNodeFile, []byte("0\n"), 0o644)
6364
Expect(err).ToNot(HaveOccurred())
6465

66+
// Create test PCI device on NUMA node 1
67+
pciDevicePath2 := filepath.Join(fakePciBasePath, testPCIAddress2)
68+
err = os.MkdirAll(pciDevicePath2, 0o755)
69+
Expect(err).ToNot(HaveOccurred())
70+
71+
numaNodeFile2 := filepath.Join(pciDevicePath2, "numa_node")
72+
err = os.WriteFile(numaNodeFile2, []byte("1\n"), 0o644)
73+
Expect(err).ToNot(HaveOccurred())
74+
6575
// Create NUMA node 0 with cpulist
6676
numaNode0Path := filepath.Join(fakeNodeBasePath, "node0")
6777
err = os.MkdirAll(numaNode0Path, 0o755)
@@ -277,26 +287,29 @@ var _ = Describe("Hardware utils test", func() {
277287
})
278288

279289
Context("devices NUMA affinity", func() {
280-
It("should return an empty result for no PCI addresses", func() {
290+
It("should return empty results for no PCI addresses", func() {
281291
domainSpec := &api.DomainSpec{}
282-
devicesNumaNodes := LookupDevicesNumaNodes([]string{}, domainSpec)
283-
Expect(devicesNumaNodes).To(BeEmpty())
292+
aligned, unaligned := LookupDevicesNumaNodes([]string{}, domainSpec)
293+
Expect(aligned).To(BeEmpty())
294+
Expect(unaligned).To(BeEmpty())
284295
})
285296

286-
It("should return an empty result for nil domain spec", func() {
287-
devicesNumaNodes := LookupDevicesNumaNodes([]string{testPCIAddress}, nil)
288-
Expect(devicesNumaNodes).To(BeEmpty())
297+
It("should return empty results for nil domain spec", func() {
298+
aligned, unaligned := LookupDevicesNumaNodes([]string{testPCIAddress}, nil)
299+
Expect(aligned).To(BeEmpty())
300+
Expect(unaligned).To(BeEmpty())
289301
})
290302

291-
It("should return an empty result when domain spec has no NUMA info", func() {
303+
It("should return empty results when domain spec has no NUMA info", func() {
292304
domainSpec := &api.DomainSpec{
293305
CPU: api.CPU{},
294306
}
295-
devicesNumaNodes := LookupDevicesNumaNodes([]string{testPCIAddress}, domainSpec)
296-
Expect(devicesNumaNodes).To(BeEmpty())
307+
aligned, unaligned := LookupDevicesNumaNodes([]string{testPCIAddress}, domainSpec)
308+
Expect(aligned).To(BeEmpty())
309+
Expect(unaligned).To(BeEmpty())
297310
})
298311

299-
It("should handle domain spec with NUMA cells but no vCPU affinity", func() {
312+
It("should return device as unaligned when NUMA cells exist but no vCPU affinity", func() {
300313
domainSpec := &api.DomainSpec{
301314
CPU: api.CPU{
302315
NUMA: &api.NUMA{
@@ -311,8 +324,12 @@ var _ = Describe("Hardware utils test", func() {
311324
},
312325
}
313326

314-
devicesNumaNodes := LookupDevicesNumaNodes([]string{testPCIAddress}, domainSpec)
315-
Expect(devicesNumaNodes).To(BeEmpty())
327+
// Device is on NUMA 0 but no vCPUs are pinned anywhere,
328+
// so it should be reported as unaligned with its host NUMA node
329+
aligned, unaligned := LookupDevicesNumaNodes([]string{testPCIAddress}, domainSpec)
330+
Expect(aligned).To(BeEmpty())
331+
Expect(unaligned).To(HaveKey(testPCIAddress))
332+
Expect(unaligned[testPCIAddress]).To(Equal(uint32(0)))
316333
})
317334

318335
It("should return devices vCPU NUMA nodes for their aligned vCPUs", func() {
@@ -334,10 +351,40 @@ var _ = Describe("Hardware utils test", func() {
334351
}
335352

336353
// Device is on host NUMA node 0, and vCPU 0 is on guest NUMA cell 0
337-
devicesNumaNodes := LookupDevicesNumaNodes([]string{testPCIAddress}, domainSpec)
338-
Expect(devicesNumaNodes).ToNot(BeEmpty())
339-
Expect(devicesNumaNodes).To(HaveKey(testPCIAddress))
340-
Expect(devicesNumaNodes[testPCIAddress]).To(Equal(uint32(0)))
354+
aligned, unaligned := LookupDevicesNumaNodes([]string{testPCIAddress}, domainSpec)
355+
Expect(aligned).ToNot(BeEmpty())
356+
Expect(aligned).To(HaveKey(testPCIAddress))
357+
Expect(aligned[testPCIAddress]).To(Equal(uint32(0)))
358+
Expect(unaligned).To(BeEmpty())
359+
})
360+
361+
It("should return unaligned devices when device is on NUMA node without vCPUs", func() {
362+
// Simulate GB200: vCPUs on NUMA 0, device on NUMA 1
363+
domainSpec := &api.DomainSpec{
364+
CPU: api.CPU{
365+
NUMA: &api.NUMA{
366+
Cells: []api.NUMACell{
367+
{ID: "0", CPUs: "0-1", Memory: pointer.P(uint64(2048)), Unit: "MiB"},
368+
},
369+
},
370+
},
371+
CPUTune: &api.CPUTune{
372+
VCPUPin: []api.CPUTuneVCPUPin{
373+
{VCPU: 0, CPUSet: "0"},
374+
{VCPU: 1, CPUSet: "1"},
375+
},
376+
},
377+
}
378+
379+
// testPCIAddress (0000:00:01.0) is on NUMA node 0,
380+
// use a device on NUMA node 1
381+
aligned, unaligned := LookupDevicesNumaNodes([]string{testPCIAddress, testPCIAddress2}, domainSpec)
382+
// testPCIAddress is on NUMA 0 with vCPUs -> aligned
383+
Expect(aligned).To(HaveKey(testPCIAddress))
384+
Expect(aligned[testPCIAddress]).To(Equal(uint32(0)))
385+
// testPCIAddress2 is on NUMA 1 with no vCPUs -> unaligned
386+
Expect(unaligned).To(HaveKey(testPCIAddress2))
387+
Expect(unaligned[testPCIAddress2]).To(Equal(uint32(1)))
341388
})
342389
})
343390
})

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

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,11 @@ type expanderBusAssigner struct {
191191
devices map[string]*api.HostDevice
192192
devicesNUMANodes map[string]uint32
193193

194+
// hostNUMAToCellMap tracks CPU-less guest NUMA cells created for host
195+
// NUMA nodes that have passthrough devices but no pinned vCPUs (e.g.,
196+
// NVIDIA GB200 where the GPU is on a separate NUMA node from all CPUs).
197+
hostNUMAToCellMap map[uint32]uint32
198+
194199
// lastAssignedBusNr tracks the last assigned bus number for expander buses.
195200
// It starts from maxExpanderBusNr and decreases as expander buses are assigned
196201
// to ensure controller indices don't conflict with expander bus number space.
@@ -222,6 +227,7 @@ func newExpanderBusAssigner(domainSpec *api.DomainSpec, iommupci *iommupci.Iommu
222227
topologyMap: make(map[uint32]*numaAwareTopology),
223228
devices: make(map[string]*api.HostDevice),
224229
devicesNUMANodes: make(map[string]uint32),
230+
hostNUMAToCellMap: make(map[uint32]uint32),
225231
controllerIndex: currentControllerIndex,
226232
controllerCount: 0,
227233
lastAssignedBusNr: maxExpanderBusNr,
@@ -286,18 +292,67 @@ func (a *expanderBusAssigner) addDevices(devices []api.HostDevice) {
286292
devicesByAddress[address] = &devices[i]
287293
}
288294

289-
numaNodes := hardware.LookupDevicesNumaNodes(pciAddresses, a.domainSpec)
295+
aligned, unaligned := hardware.LookupDevicesNumaNodes(pciAddresses, a.domainSpec)
290296

291297
for address, device := range devicesByAddress {
292-
if numaNode, exists := numaNodes[address]; exists {
298+
if numaNode, exists := aligned[address]; exists {
293299
a.devices[address] = device
294300
a.devicesNUMANodes[address] = numaNode
301+
} else if hostNUMANode, exists := unaligned[address]; exists {
302+
// Device is on a host NUMA node with no pinned vCPUs (e.g.,
303+
// NVIDIA GB200 where the GPU is on a separate NUMA node from
304+
// all CPUs). Create a CPU-less guest NUMA cell for this host
305+
// NUMA node so the device can be placed under an expander bus
306+
// targeting it.
307+
guestCellID := a.getOrCreateCPULessNUMACell(hostNUMANode)
308+
a.devices[address] = device
309+
a.devicesNUMANodes[address] = guestCellID
295310
} else {
296311
log.Log.Infof("device %s has no NUMA affinity information, skipping for pcie-expander-bus assignment", address)
297312
}
298313
}
299314
}
300315

316+
// getOrCreateCPULessNUMACell returns the guest NUMA cell ID for a host NUMA
317+
// node that has no pinned vCPUs. If a CPU-less cell has already been created
318+
// for this host NUMA node, it returns the existing cell ID. Otherwise, it
319+
// creates a new CPU-less NUMA cell in the domain spec.
320+
func (a *expanderBusAssigner) getOrCreateCPULessNUMACell(hostNUMANode uint32) uint32 {
321+
// Check if we already created a cell for this host NUMA node
322+
if cellID, exists := a.hostNUMAToCellMap[hostNUMANode]; exists {
323+
return cellID
324+
}
325+
326+
// Find the next available cell ID
327+
nextCellID := uint32(0)
328+
if a.domainSpec.CPU.NUMA != nil {
329+
for _, cell := range a.domainSpec.CPU.NUMA.Cells {
330+
id, err := strconv.Atoi(cell.ID)
331+
if err != nil {
332+
continue
333+
}
334+
if uint32(id) >= nextCellID {
335+
nextCellID = uint32(id) + 1
336+
}
337+
}
338+
}
339+
340+
newCell := api.NUMACell{
341+
ID: strconv.Itoa(int(nextCellID)),
342+
CPUs: "",
343+
}
344+
345+
if a.domainSpec.CPU.NUMA == nil {
346+
a.domainSpec.CPU.NUMA = &api.NUMA{}
347+
}
348+
a.domainSpec.CPU.NUMA.Cells = append(a.domainSpec.CPU.NUMA.Cells, newCell)
349+
a.hostNUMAToCellMap[hostNUMANode] = nextCellID
350+
351+
log.Log.Infof("created CPU-less guest NUMA cell %d for host NUMA node %d", nextCellID, hostNUMANode)
352+
353+
return nextCellID
354+
}
355+
301356
// numaDeviceGroups represents a mapping of NUMA nodes to host devices.
302357
type numaDeviceGroups map[uint32][]*api.HostDevice
303358

0 commit comments

Comments
 (0)