Skip to content

Commit a6ae9b3

Browse files
committed
libvirt: add cpuset support for CPU pinning
Add --cpuset flag and LIBVIRT_CPUSET env var to pin vCPUs to specific physical CPUs. Useful for AI inferencing where cores from different sockets improve performance. Changes: - Add CPUSet field to Config and vmConfig structures - Add --cpuset CLI flag and LIBVIRT_CPUSET environment variable - Embed CPUSet attribute in Domain XML for all architectures - Expose LIBVIRT_CPUSET in kustomization.yaml for deployment - Add unit tests for CPUSet functionality Fixes: #2397 Signed-off-by: tak-ka3 <[email protected]>
1 parent 832bf73 commit a6ae9b3

File tree

6 files changed

+115
-5
lines changed

6 files changed

+115
-5
lines changed

src/cloud-api-adaptor/install/overlays/libvirt/kustomization.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ configMapGenerator:
3030
#- LIBVIRT_VOL_NAME="" # Uncomment and set if you want to use a specific volume name. Defaults to podvm-base.qcow2
3131
#- LIBVIRT_CPU="2"
3232
#- LIBVIRT_MEMORY="8192"
33+
#- LIBVIRT_CPUSET="" # Uncomment and set to pin vCPUs to physical CPUs (e.g., "0,2,4,6" or "0-3")
3334
#- PAUSE_IMAGE="" # Uncomment and set if you want to use a specific pause image
3435
#- TUNNEL_TYPE="" # Uncomment and set if you want to use a specific tunnel type. Defaults to vxlan
3536
#- VXLAN_PORT="" # Uncomment and set if you want to use a specific vxlan port. Defaults to 4789

src/cloud-providers/libvirt/libvirt.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,8 @@ func createDomainXMLs390x(client *libvirtClient, cfg *domainConfig, vm *vmConfig
250250
Value: cfg.mem, Unit: "MiB",
251251
},
252252
VCPU: &libvirtxml.DomainVCPU{
253-
Value: cfg.cpu,
253+
Value: cfg.cpu,
254+
CPUSet: vm.cpuset,
254255
},
255256
Clock: &libvirtxml.DomainClock{
256257
Offset: "utc",
@@ -309,7 +310,7 @@ func createDomainXMLx86_64(client *libvirtClient, cfg *domainConfig, vm *vmConfi
309310
Name: cfg.name,
310311
Description: "This Virtual Machine is the peer-pod VM",
311312
Memory: &libvirtxml.DomainMemory{Value: cfg.mem, Unit: "MiB", DumpCore: "on"},
312-
VCPU: &libvirtxml.DomainVCPU{Value: cfg.cpu},
313+
VCPU: &libvirtxml.DomainVCPU{Value: cfg.cpu, CPUSet: vm.cpuset},
313314
OS: &libvirtxml.DomainOS{
314315
Type: &libvirtxml.DomainOSType{Arch: "x86_64", Type: typeHardwareVirtualMachine},
315316
},
@@ -459,7 +460,7 @@ func createDomainXMLaarch64(client *libvirtClient, cfg *domainConfig, vm *vmConf
459460
Firmware: "efi",
460461
},
461462
Memory: &libvirtxml.DomainMemory{Value: cfg.mem, Unit: "MiB"},
462-
VCPU: &libvirtxml.DomainVCPU{Value: cfg.cpu},
463+
VCPU: &libvirtxml.DomainVCPU{Value: cfg.cpu, CPUSet: vm.cpuset},
463464
CPU: &libvirtxml.DomainCPU{Mode: "host-passthrough"},
464465
Devices: &libvirtxml.DomainDeviceList{
465466
Disks: []libvirtxml.DomainDisk{

src/cloud-providers/libvirt/libvirt_test.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,3 +152,108 @@ func TestCreateDomainXMLaarch64(t *testing.T) {
152152
t.Error(err)
153153
}
154154
}
155+
156+
func TestCPUSetInDomainXML(t *testing.T) {
157+
checkConfig(t)
158+
159+
client, err := NewLibvirtClient(testCfg)
160+
if err != nil {
161+
t.Fatal(err)
162+
}
163+
defer client.connection.Close()
164+
165+
testCases := []struct {
166+
name string
167+
cpuset string
168+
expectedCPUSet string
169+
}{
170+
{
171+
name: "with cpuset specified",
172+
cpuset: "0,2,4,6",
173+
expectedCPUSet: "0,2,4,6",
174+
},
175+
{
176+
name: "with cpuset range",
177+
cpuset: "0-3",
178+
expectedCPUSet: "0-3",
179+
},
180+
{
181+
name: "with empty cpuset",
182+
cpuset: "",
183+
expectedCPUSet: "",
184+
},
185+
}
186+
187+
for _, tc := range testCases {
188+
t.Run(tc.name, func(t *testing.T) {
189+
vm := vmConfig{
190+
cpuset: tc.cpuset,
191+
}
192+
193+
domainCfg := domainConfig{
194+
name: "TestCPUSet",
195+
cpu: 4,
196+
mem: 4096,
197+
networkName: client.networkName,
198+
bootDisk: "/var/lib/libvirt/images/root.qcow2",
199+
cidataDisk: "/var/lib/libvirt/images/cidata.iso",
200+
}
201+
202+
domCfg, err := createDomainXML(client, &domainCfg, &vm)
203+
if err != nil {
204+
t.Error(err)
205+
return
206+
}
207+
208+
assert.Equal(t, tc.expectedCPUSet, domCfg.VCPU.CPUSet,
209+
"CPUSet should be set correctly in domain XML")
210+
})
211+
}
212+
}
213+
214+
func TestCPUSetXMLMarshaling(t *testing.T) {
215+
// Test that CPUSet is correctly marshaled to XML without requiring libvirt connection
216+
testCases := []struct {
217+
name string
218+
cpuset string
219+
expectedSubstr string
220+
}{
221+
{
222+
name: "cpuset with comma-separated values",
223+
cpuset: "0,2,4,6",
224+
expectedSubstr: `cpuset="0,2,4,6"`,
225+
},
226+
{
227+
name: "cpuset with range",
228+
cpuset: "0-7",
229+
expectedSubstr: `cpuset="0-7"`,
230+
},
231+
{
232+
name: "cpuset with mixed format",
233+
cpuset: "0,2,4-7,10",
234+
expectedSubstr: `cpuset="0,2,4-7,10"`,
235+
},
236+
}
237+
238+
for _, tc := range testCases {
239+
t.Run(tc.name, func(t *testing.T) {
240+
domain := &libvirtxml.Domain{
241+
Type: "kvm",
242+
Name: "test-vm",
243+
VCPU: &libvirtxml.DomainVCPU{
244+
Value: 4,
245+
CPUSet: tc.cpuset,
246+
},
247+
}
248+
249+
xmlStr, err := domain.Marshal()
250+
if err != nil {
251+
t.Errorf("Failed to marshal domain XML: %v", err)
252+
return
253+
}
254+
255+
assert.Contains(t, xmlStr, tc.expectedSubstr,
256+
"Marshaled XML should contain cpuset attribute")
257+
})
258+
}
259+
}

src/cloud-providers/libvirt/manager.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ func (_ *Manager) ParseCmd(flags *flag.FlagSet) {
4343
reg.StringWithEnv(&libvirtcfg.Firmware, "firmware", defaultFirmware, "LIBVIRT_EFI_FIRMWARE", "Path to OVMF")
4444
reg.UintWithEnv(&libvirtcfg.CPU, "cpu", 2, "LIBVIRT_CPU", "Number of processors allocated")
4545
reg.UintWithEnv(&libvirtcfg.Memory, "memory", 8192, "LIBVIRT_MEMORY", "Amount of memory in MiB")
46+
reg.StringWithEnv(&libvirtcfg.CPUSet, "cpuset", "", "LIBVIRT_CPUSET", "CPU set for pinning vCPUs to physical CPUs (e.g., \"0,2,4,6\" or \"0-3\")")
4647

4748
// Flags without environment variable support (pass empty string for envVarName)
4849
reg.StringWithEnv(&libvirtcfg.DataDir, "data-dir", defaultDataDir, "", "libvirt storage dir")

src/cloud-providers/libvirt/provider.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ func (p *libvirtProvider) CreateInstance(ctx context.Context, podName, sandboxID
7171
}
7272

7373
// TODO: Specify the maximum instance name length in Libvirt
74-
vm := &vmConfig{name: instanceName, cpu: instanceVCPUs, mem: instanceMemory, userData: userData, firmware: p.serviceConfig.Firmware}
74+
vm := &vmConfig{name: instanceName, cpu: instanceVCPUs, mem: instanceMemory, userData: userData, firmware: p.serviceConfig.Firmware, cpuset: p.serviceConfig.CPUSet}
7575

7676
if p.serviceConfig.DisableCVM {
7777
vm.launchSecurityType = NoLaunchSecurity

src/cloud-providers/libvirt/types.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ type Config struct {
2222
LaunchSecurity string
2323
Firmware string
2424
CPU uint
25-
Memory uint // It stores the value in MiB
25+
Memory uint // It stores the value in MiB
26+
CPUSet string // CPU set for pinning vCPUs (e.g., "0,2,4,6" or "0-3")
2627
}
2728

2829
type vmConfig struct {
@@ -35,6 +36,7 @@ type vmConfig struct {
3536
instanceId string //keeping it consistent with sandbox.vsi
3637
launchSecurityType LaunchSecurityType
3738
firmware string
39+
cpuset string // CPU set for pinning vCPUs (e.g., "0,2,4,6" or "0-3")
3840
}
3941

4042
type createDomainOutput struct {

0 commit comments

Comments
 (0)