Skip to content

Commit 3a51a2f

Browse files
committed
gpu: add pci device id allow support
By defining allowed PCI IDs, it's possible to only select some GPUs per host. For example, on a desktop with integrated and discrete graphics, GPU plugin can only register the discrete one. Signed-off-by: Tuomas Katila <[email protected]>
1 parent 60f41aa commit 3a51a2f

File tree

9 files changed

+236
-0
lines changed

9 files changed

+236
-0
lines changed

cmd/gpu_plugin/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ For workloads on different KMDs, see [KMD and UMD](#kmd-and-umd).
5757
| -health-management | - | disabled | Enable health management by requesting data from oneAPI/Level-Zero interface. Requires [GPU Level-Zero](../gpu_levelzero/) sidecar. See [health management](#health-management) |
5858
| -wsl | - | disabled | Adapt plugin to run in the WSL environment. Requires [GPU Level-Zero](../gpu_levelzero/) sidecar. |
5959
| -shared-dev-num | int | 1 | Number of containers that can share the same GPU device |
60+
| -allowlist-ids | string | "" | A list of PCI Device IDs that are allowed to be registered as resources. Default is empty (=all registered). |
6061
| -allocation-policy | string | none | 3 possible values: balanced, packed, none. For shared-dev-num > 1: _balanced_ mode spreads workloads among GPU devices, _packed_ mode fills one GPU fully before moving to next, and _none_ selects first available device from kubelet. Default is _none_. |
6162

6263
The plugin also accepts a number of other arguments (common to all plugins) related to logging.

cmd/gpu_plugin/gpu_plugin.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ const (
6969

7070
type cliOptions struct {
7171
preferredAllocationPolicy string
72+
allowlistIDs string
7273
sharedDevNum int
7374
temperatureLimit int
7475
enableMonitoring bool
@@ -204,6 +205,31 @@ func packedPolicy(req *pluginapi.ContainerPreferredAllocationRequest) []string {
204205
return deviceIds
205206
}
206207

208+
func parsePCIDeviceIDs(allowlist string) ([]string, error) {
209+
deviceIDs := make([]string, 0, strings.Count(allowlist, ",")+1)
210+
211+
r := regexp.MustCompile(`^0x[0-9a-f]{4}$`)
212+
213+
for id := range strings.SplitSeq(allowlist, ",") {
214+
id = strings.TrimSpace(id)
215+
if id == "" {
216+
klog.Errorf("Empty PCI device ID given")
217+
218+
return []string{}, os.ErrInvalid
219+
}
220+
221+
if !r.MatchString(id) {
222+
klog.Errorf("Invalid PCI device ID %s", id)
223+
224+
return []string{}, os.ErrInvalid
225+
}
226+
227+
deviceIDs = append(deviceIDs, id)
228+
}
229+
230+
return deviceIDs, nil
231+
}
232+
207233
func (dp *devicePlugin) pciAddressForCard(cardPath, cardName string) (string, error) {
208234
linkPath, err := os.Readlink(cardPath)
209235
if err != nil {
@@ -585,6 +611,23 @@ func (dp *devicePlugin) filterOutInvalidCards(files []fs.DirEntry) []fs.DirEntry
585611
continue
586612
}
587613

614+
// Skip if the device is not in allowlist.
615+
if len(dp.options.allowlistIDs) > 0 {
616+
pciID, err := pciDeviceIDForCard(path.Join(dp.sysfsDir, f.Name()))
617+
if err != nil {
618+
klog.Warningf("Failed to get PCI ID for device %s: %+v", f.Name(), err)
619+
620+
continue
621+
}
622+
623+
if !strings.Contains(dp.options.allowlistIDs, pciID) {
624+
klog.V(4).Infof("Skipping device %s (%s), not in allowlist: %s",
625+
f.Name(), pciID, dp.options.allowlistIDs)
626+
627+
continue
628+
}
629+
}
630+
588631
filtered = append(filtered, f)
589632
}
590633

@@ -723,6 +766,7 @@ func main() {
723766
flag.IntVar(&opts.sharedDevNum, "shared-dev-num", 1, "number of containers sharing the same GPU device")
724767
flag.IntVar(&opts.temperatureLimit, "temp-limit", 100, "temperature limit at which device is marked unhealthy")
725768
flag.StringVar(&opts.preferredAllocationPolicy, "allocation-policy", "none", "modes of allocating GPU devices: balanced, packed and none")
769+
flag.StringVar(&opts.allowlistIDs, "allowlist-ids", "", "comma-separated list of device IDs to allow (e.g. 0x49c5,0x49c6)")
726770
flag.Parse()
727771

728772
if opts.sharedDevNum < 1 {
@@ -736,6 +780,18 @@ func main() {
736780
os.Exit(1)
737781
}
738782

783+
if opts.allowlistIDs != "" {
784+
if allowListIDs, err := parsePCIDeviceIDs(opts.allowlistIDs); err != nil {
785+
klog.Error("Failed to parse allowlist-ids: ", err)
786+
787+
os.Exit(1)
788+
} else {
789+
klog.V(2).Infof("Allowed device IDs: %q", allowListIDs)
790+
791+
opts.allowlistIDs = strings.Join(allowListIDs, ",")
792+
}
793+
}
794+
739795
klog.V(1).Infof("GPU device plugin started with %s preferred allocation policy", opts.preferredAllocationPolicy)
740796

741797
plugin := newDevicePlugin(prefix+sysfsDrmDirectory, prefix+devfsDriDirectory, opts)

cmd/gpu_plugin/gpu_plugin_test.go

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,66 @@ func TestScan(t *testing.T) {
361361
expectedI915Devs: 1,
362362
expectedI915Monitors: 1,
363363
},
364+
{
365+
name: "two devices with only one allowed",
366+
sysfsdirs: []string{"card0/device/drm/card0", "card0/device/drm/controlD64", "card1/device/drm/card1"},
367+
sysfsfiles: map[string][]byte{
368+
"card0/device/vendor": []byte("0x8086"),
369+
"card0/device/device": []byte("0x1234"),
370+
"card1/device/vendor": []byte("0x8086"),
371+
"card1/device/device": []byte("0x9876"),
372+
},
373+
symlinkfiles: map[string]string{
374+
"card0/device/driver": "drivers/xe",
375+
"card1/device/driver": "drivers/i915",
376+
},
377+
devfsdirs: []string{
378+
"card0",
379+
"by-path/pci-0000:00:00.0-card",
380+
"by-path/pci-0000:00:00.0-render",
381+
"card1",
382+
"by-path/pci-0000:00:01.0-card",
383+
"by-path/pci-0000:00:01.0-render",
384+
},
385+
options: cliOptions{enableMonitoring: true, allowlistIDs: "0x1234"},
386+
expectedXeDevs: 1,
387+
expectedXeMonitors: 1,
388+
expectedI915Devs: 0,
389+
expectedI915Monitors: 0,
390+
},
391+
{
392+
name: "three devices with two allowed",
393+
sysfsdirs: []string{"card0/device/drm/card0", "card0/device/drm/controlD64", "card1/device/drm/card1", "card2/device/drm/card2"},
394+
sysfsfiles: map[string][]byte{
395+
"card0/device/vendor": []byte("0x8086"),
396+
"card0/device/device": []byte("0x1234"),
397+
"card1/device/vendor": []byte("0x8086"),
398+
"card1/device/device": []byte("0x9876"),
399+
"card2/device/vendor": []byte("0x8086"),
400+
"card2/device/device": []byte("0x0101"),
401+
},
402+
symlinkfiles: map[string]string{
403+
"card0/device/driver": "drivers/xe",
404+
"card1/device/driver": "drivers/i915",
405+
"card2/device/driver": "drivers/i915",
406+
},
407+
devfsdirs: []string{
408+
"card0",
409+
"by-path/pci-0000:00:00.0-card",
410+
"by-path/pci-0000:00:00.0-render",
411+
"card1",
412+
"by-path/pci-0000:00:01.0-card",
413+
"by-path/pci-0000:00:01.0-render",
414+
"card2",
415+
"by-path/pci-0000:00:02.0-card",
416+
"by-path/pci-0000:00:02.0-render",
417+
},
418+
options: cliOptions{enableMonitoring: true, allowlistIDs: "0x1234,0x9876"},
419+
expectedXeDevs: 1,
420+
expectedXeMonitors: 1,
421+
expectedI915Devs: 1,
422+
expectedI915Monitors: 1,
423+
},
364424
{
365425
name: "sriov-1-pf-no-vfs + monitoring",
366426
sysfsdirs: []string{"card0/device/drm/card0", "card0/device/drm/controlD64"},
@@ -1048,3 +1108,73 @@ func TestCDIDeviceInclusion(t *testing.T) {
10481108
t.Error("Invalid count for device (xe)")
10491109
}
10501110
}
1111+
1112+
func TestParsePCIDeviceIDs(t *testing.T) {
1113+
tests := []struct {
1114+
name string
1115+
input string
1116+
want []string
1117+
wantError bool
1118+
}{
1119+
{
1120+
name: "valid single ID",
1121+
input: "0x1234",
1122+
want: []string{"0x1234"},
1123+
wantError: false,
1124+
},
1125+
{
1126+
name: "valid multiple IDs",
1127+
input: "0x1234,0x5678,0x9abc",
1128+
want: []string{"0x1234", "0x5678", "0x9abc"},
1129+
wantError: false,
1130+
},
1131+
{
1132+
name: "valid IDs with spaces",
1133+
input: " 0x1234 , 0x5678 ",
1134+
want: []string{"0x1234", "0x5678"},
1135+
wantError: false,
1136+
},
1137+
{
1138+
name: "empty string",
1139+
input: "",
1140+
want: []string{},
1141+
wantError: true,
1142+
},
1143+
{
1144+
name: "invalid ID format",
1145+
input: "0x1234,abcd",
1146+
want: []string{},
1147+
wantError: true,
1148+
},
1149+
{
1150+
name: "invalid hex length",
1151+
input: "0x123,0x5678",
1152+
want: []string{},
1153+
wantError: true,
1154+
},
1155+
{
1156+
name: "extra comma",
1157+
input: "0x1234,",
1158+
want: []string{},
1159+
wantError: true,
1160+
},
1161+
{
1162+
name: "capita hex",
1163+
input: "0xAA12,",
1164+
want: []string{},
1165+
wantError: true,
1166+
},
1167+
}
1168+
1169+
for _, tt := range tests {
1170+
t.Run(tt.name, func(t *testing.T) {
1171+
got, err := parsePCIDeviceIDs(tt.input)
1172+
if (err != nil) != tt.wantError {
1173+
t.Errorf("parsePCIDeviceIDs() error = %v, wantError %v", err, tt.wantError)
1174+
}
1175+
if !reflect.DeepEqual(got, tt.want) {
1176+
t.Errorf("parsePCIDeviceIDs() = %v, want %v", got, tt.want)
1177+
}
1178+
})
1179+
}
1180+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
apiVersion: apps/v1
2+
kind: DaemonSet
3+
metadata:
4+
name: intel-gpu-plugin
5+
spec:
6+
template:
7+
spec:
8+
containers:
9+
- name: intel-gpu-plugin
10+
args:
11+
- "-v=4"
12+
- "-allowlist-ids=0x56a6,0x56a5,0x56a1,0x56a0,0x5694,0x5693,0x5692,0x5691,0x5690,0x56b3,0x56b2,0x56a4,0x56a3,0x5697,0x5696,0x5695,0x56b1,0x56b0,0x56a2,0x56ba,0x56bc,0x56bd,0x56bb"
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
resources:
2+
- ../../base
3+
patches:
4+
- path: add-args.yaml

deployments/operator/crd/bases/deviceplugin.intel.com_gpudeviceplugins.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ spec:
5555
spec:
5656
description: GpuDevicePluginSpec defines the desired state of GpuDevicePlugin.
5757
properties:
58+
allowListIDs:
59+
description: |-
60+
AllowListIDs is a comma-separated list of PCI IDs of GPU devices that should only be advertised by the plugin.
61+
If not set, all devices are advertised.
62+
The list can contain IDs in the form of '0x1234,0x49a4,0x50b4'.
63+
type: string
5864
enableMonitoring:
5965
description: |-
6066
EnableMonitoring enables the monitoring resource ('i915_monitoring')

pkg/apis/deviceplugin/v1/gpudeviceplugin_types.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ type GpuDevicePluginSpec struct {
3434
// InitImage is a container image with tools (e.g., GPU NFD source hook) installed on each node.
3535
InitImage string `json:"initImage,omitempty"`
3636

37+
// AllowListIDs is a comma-separated list of PCI IDs of GPU devices that should only be advertised by the plugin.
38+
// If not set, all devices are advertised.
39+
// The list can contain IDs in the form of '0x1234,0x49a4,0x50b4'.
40+
AllowListIDs string `json:"allowListIDs,omitempty"`
41+
3742
// PreferredAllocationPolicy sets the mode of allocating GPU devices on a node.
3843
// See documentation for detailed description of the policies. Only valid when SharedDevNum > 1 is set.
3944
// +kubebuilder:validation:Enum=balanced;packed;none

pkg/apis/deviceplugin/v1/gpudeviceplugin_webhook.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,20 @@ package v1
1616

1717
import (
1818
"fmt"
19+
"regexp"
20+
"strings"
1921

2022
ctrl "sigs.k8s.io/controller-runtime"
2123

2224
"github.com/intel/intel-device-plugins-for-kubernetes/pkg/controllers"
2325
)
2426

27+
var pciIDRegex regexp.Regexp
28+
2529
// SetupWebhookWithManager sets up a webhook for GpuDevicePlugin custom resources.
2630
func (r *GpuDevicePlugin) SetupWebhookWithManager(mgr ctrl.Manager) error {
31+
pciIDRegex = *regexp.MustCompile(`^0x[0-9a-f]{4}$`)
32+
2733
return ctrl.NewWebhookManagedBy(mgr).
2834
For(r).
2935
WithDefaulter(&commonDevicePluginDefaulter{
@@ -44,5 +50,17 @@ func (r *GpuDevicePlugin) validatePlugin(ref *commonDevicePluginValidator) error
4450
return fmt.Errorf("%w: PreferredAllocationPolicy is valid only when setting sharedDevNum > 1", errValidation)
4551
}
4652

53+
if r.Spec.AllowListIDs != "" {
54+
for id := range strings.SplitSeq(r.Spec.AllowListIDs, ",") {
55+
if id == "" {
56+
return fmt.Errorf("%w: Empty PCI Device ID in AllowListIDs", errValidation)
57+
}
58+
59+
if !pciIDRegex.MatchString(id) {
60+
return fmt.Errorf("%w: Invalid PCI Device ID: %s", errValidation, id)
61+
}
62+
}
63+
}
64+
4765
return validatePluginImage(r.Spec.Image, ref.expectedImage, &ref.expectedVersion)
4866
}

pkg/controllers/gpu/controller.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,5 +277,9 @@ func getPodArgs(gdp *devicepluginv1.GpuDevicePlugin) []string {
277277
args = append(args, "-allocation-policy", "none")
278278
}
279279

280+
if gdp.Spec.AllowListIDs != "" {
281+
args = append(args, "-allowlist-ids", gdp.Spec.AllowListIDs)
282+
}
283+
280284
return args
281285
}

0 commit comments

Comments
 (0)