Skip to content

Commit 2a081fd

Browse files
Merge pull request #435 from NVIDIA/kv-subsystem-id-matching
Support optional subsystem ID matching in device filters
2 parents 4f279a9 + 3882a4d commit 2a081fd

9 files changed

Lines changed: 352 additions & 22 deletions

File tree

api/spec/v1/helpers.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ func (ms *MigConfigSpec) MatchesDeviceFilter(deviceID types.DeviceID) bool {
3838

3939
for _, df := range deviceFilter {
4040
newDeviceID, _ := types.NewDeviceIDFromString(df)
41-
if newDeviceID == deviceID {
41+
if newDeviceID.Matches(deviceID) {
4242
return true
4343
}
4444
}

api/spec/v1/helpers_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package v1
18+
19+
import (
20+
"testing"
21+
22+
"github.com/stretchr/testify/require"
23+
24+
"github.com/NVIDIA/mig-parted/pkg/types"
25+
)
26+
27+
func TestMigConfigSpecMatchesDeviceFilter(t *testing.T) {
28+
h100 := types.NewDeviceIDWithSubsystem(0x2330, 0x10DE, 0x16C0, 0x10DE)
29+
30+
testCases := []struct {
31+
name string
32+
filter interface{}
33+
want bool
34+
}{
35+
{
36+
name: "empty filter matches all devices",
37+
filter: "",
38+
want: true,
39+
},
40+
{
41+
name: "primary-only filter matches subsystem-aware hardware",
42+
filter: "0x233010DE",
43+
want: true,
44+
},
45+
{
46+
name: "matching subsystem filter matches hardware",
47+
filter: "0x233010DE:0x16C010DE",
48+
want: true,
49+
},
50+
{
51+
name: "sibling subsystem filter does not match hardware",
52+
filter: "0x233010DE:0x16C110DE",
53+
want: false,
54+
},
55+
{
56+
name: "multiple filters stop at a matching subsystem entry",
57+
filter: []string{"0x233010DE:0x16C110DE", "0x233010DE:0x16C010DE"},
58+
want: true,
59+
},
60+
{
61+
name: "malformed filters with extra separators do not match",
62+
filter: "0x233010DE:0x16C010DE:extra",
63+
want: false,
64+
},
65+
}
66+
67+
for _, tc := range testCases {
68+
t.Run(tc.name, func(t *testing.T) {
69+
spec := MigConfigSpec{
70+
DeviceFilter: tc.filter,
71+
Devices: "all",
72+
}
73+
74+
require.Equal(t, tc.want, spec.MatchesDeviceFilter(h100))
75+
})
76+
}
77+
}

cmd/nvidia-mig-parted/export/config.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ func ExportMigConfigs(c *Context) (*v1.Spec, error) {
5050

5151
configSpecs := make(v1.MigConfigSpecSlice, len(deviceIDs))
5252
for i, deviceID := range deviceIDs {
53-
deviceFilter := deviceID.String()
53+
// Strip any subsystem qualifier so exported configs apply to all board variants of a GPU.
54+
deviceFilter := deviceID.Primary().String()
5455

5556
enabled := false
5657
capable, err := modeManager.IsMigCapable(i)

cmd/nvidia-mig-parted/util/device.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ func pciVisitGPUs(visit func(*nvpci.NvidiaPCIDevice) error) error {
6767
func pciGetGPUDeviceIDs() ([]types.DeviceID, error) {
6868
var ids []types.DeviceID
6969
err := pciVisitGPUs(func(gpu *nvpci.NvidiaPCIDevice) error {
70-
ids = append(ids, types.NewDeviceID(gpu.Device, gpu.Vendor))
70+
ids = append(ids, types.NewDeviceIDWithSubsystem(gpu.Device, gpu.Vendor, gpu.SubsystemDevice, gpu.SubsystemVendor))
7171
return nil
7272
})
7373
if err != nil {
@@ -91,7 +91,7 @@ func nvmlGetGPUDeviceIDs() ([]types.DeviceID, error) {
9191
return nil
9292
}
9393

94-
ids = append(ids, types.NewDeviceID(gpu.Device, gpu.Vendor))
94+
ids = append(ids, types.NewDeviceIDWithSubsystem(gpu.Device, gpu.Vendor, gpu.SubsystemDevice, gpu.SubsystemVendor))
9595
return nil
9696
})
9797
if err != nil {

pkg/mig/config/known_configs.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ import (
2323
"github.com/NVIDIA/mig-parted/pkg/types"
2424
)
2525

26-
const (
27-
A100_SXM4_40GB types.DeviceID = 0x20B010DE
26+
var (
27+
A100_SXM4_40GB = types.NewDeviceID(0x20B0, 0x10DE)
2828
)
2929

3030
type LoopControl int

pkg/mig/discovery/discovery.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,10 @@ import (
3232
// could be discovered.
3333
var ErrNoProfilesDiscovered = errors.New("no MIG profiles discovered for MIG-capable GPUs")
3434

35-
const (
35+
var (
3636
// deviceIDA30 is the PCI device ID for A30-24GB GPUs.
3737
// NVML has a bug that reports incorrect profiles for this GPU.
38-
deviceIDA30 = 0x20B710DE
38+
deviceIDA30 = types.NewDeviceID(0x20B7, 0x10DE)
3939
)
4040

4141
// ProfileInfo represents a discovered MIG profile with its metadata
@@ -65,9 +65,9 @@ func isCIProfile(c, g int) bool {
6565
func getDeviceID(dev nvdev.Device) (types.DeviceID, error) {
6666
pciInfo, ret := nvml.Device(dev).GetPciInfo()
6767
if ret != nvml.SUCCESS {
68-
return 0, fmt.Errorf("failed to get PCI info: %v", ret)
68+
return types.DeviceID{}, fmt.Errorf("failed to get PCI info: %v", ret)
6969
}
70-
return types.DeviceID(pciInfo.PciDeviceId), nil
70+
return types.NewDeviceIDFromPacked(pciInfo.PciDeviceId), nil
7171
}
7272

7373
// getHardcodedA30Profiles returns hardcoded MIG profiles for A30 GPUs.
@@ -132,7 +132,7 @@ func (d *discoverer) discoverProfiles() (DeviceProfiles, error) {
132132
// Check for A30 - use hardcoded profiles due to NVML bug where
133133
// GetGpuInstanceProfileInfo returns incorrect InstanceCount values.
134134
// The hardcoded values match the A30 MIG profiles from nvidia-smi.
135-
if uint32(deviceID) == deviceIDA30 {
135+
if deviceIDA30.Matches(deviceID) {
136136
log.Infof("Device %d is A30 (DeviceID: %s), using hardcoded profiles due to NVML bug",
137137
i, deviceID.String())
138138
result[i] = getHardcodedA30Profiles(deviceID)

pkg/mig/discovery/discovery_test.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@ import (
2323
"github.com/NVIDIA/go-nvml/pkg/nvml/mock/dgxa100"
2424
"github.com/stretchr/testify/assert"
2525
"github.com/stretchr/testify/require"
26-
27-
"github.com/NVIDIA/mig-parted/pkg/types"
2826
)
2927

3028
func TestDiscoverProfiles(t *testing.T) {
@@ -73,7 +71,7 @@ func TestIsCIProfile(t *testing.T) {
7371
}
7472

7573
func TestGetHardcodedA30Profiles(t *testing.T) {
76-
deviceID := types.DeviceID(deviceIDA30)
74+
deviceID := deviceIDA30
7775
profiles := getHardcodedA30Profiles(deviceID)
7876

7977
require.Len(t, profiles, 5)

pkg/types/device.go

Lines changed: 96 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,36 +19,124 @@ package types
1919
import (
2020
"fmt"
2121
"strconv"
22+
"strings"
2223
)
2324

2425
// DeviceID represents a GPU Device ID as read from a GPUs PCIe config space.
25-
type DeviceID uint32
26+
type DeviceID struct {
27+
Device uint16
28+
Vendor uint16
29+
SubsystemDevice uint16
30+
SubsystemVendor uint16
31+
HasSubsystem bool
32+
}
2633

2734
// NewDeviceID constructs a new 'DeviceID' from the device and vendor values pulled from a GPUs PCIe config space.
2835
func NewDeviceID(device, vendor uint16) DeviceID {
29-
return DeviceID((uint32(device) << 16) | uint32(vendor))
36+
return DeviceID{
37+
Device: device,
38+
Vendor: vendor,
39+
}
40+
}
41+
42+
// NewDeviceIDWithSubsystem constructs a new 'DeviceID' with subsystem values.
43+
func NewDeviceIDWithSubsystem(device, vendor, subDevice, subVendor uint16) DeviceID {
44+
return DeviceID{
45+
Device: device,
46+
Vendor: vendor,
47+
SubsystemDevice: subDevice,
48+
SubsystemVendor: subVendor,
49+
HasSubsystem: true,
50+
}
51+
}
52+
53+
// NewDeviceIDFromPacked constructs a 'DeviceID' from a combined 32-bit device+vendor
54+
// value, as reported in a GPUs PCIe config space or by NVML.
55+
func NewDeviceIDFromPacked(packed uint32) DeviceID {
56+
device, vendor := splitRawDeviceID(uint64(packed))
57+
return NewDeviceID(device, vendor)
3058
}
3159

3260
// NewDeviceIDFromString constructs a 'DeviceID' from its string representation.
3361
func NewDeviceIDFromString(str string) (DeviceID, error) {
34-
deviceID, err := strconv.ParseInt(str, 0, 32)
62+
parts := strings.Split(str, ":")
63+
if len(parts) > 2 {
64+
return DeviceID{}, fmt.Errorf(
65+
"invalid DeviceID format '%v': expected '<devicevendor>' or '<devicevendor>:<subdevicevendor>'",
66+
str,
67+
)
68+
}
69+
70+
deviceIDRaw, err := strconv.ParseUint(parts[0], 0, 32)
3571
if err != nil {
36-
return 0, fmt.Errorf("unable to create DeviceID from string '%v': %v", str, err)
72+
return DeviceID{}, fmt.Errorf("unable to create DeviceID from string '%v': %v", str, err)
73+
}
74+
75+
device, vendor := splitRawDeviceID(deviceIDRaw)
76+
77+
deviceID := DeviceID{
78+
Device: device,
79+
Vendor: vendor,
80+
}
81+
82+
if len(parts) == 2 {
83+
subIDRaw, err := strconv.ParseUint(parts[1], 0, 32)
84+
if err != nil {
85+
return DeviceID{}, fmt.Errorf("unable to create Subsystem from string '%v': %v", str, err)
86+
}
87+
88+
subsystemDevice, subsystemVendor := splitRawDeviceID(subIDRaw)
89+
90+
deviceID.SubsystemDevice = subsystemDevice
91+
deviceID.SubsystemVendor = subsystemVendor
92+
deviceID.HasSubsystem = true
3793
}
38-
return DeviceID(deviceID), nil
94+
95+
return deviceID, nil
3996
}
4097

4198
// String returns a 'DeviceID' as a string.
4299
func (d DeviceID) String() string {
43-
return fmt.Sprintf("0x%X", uint32(d))
100+
primary := fmt.Sprintf("0x%04X%04X", d.Device, d.Vendor)
101+
if d.HasSubsystem {
102+
return fmt.Sprintf("%s:0x%04X%04X", primary, d.SubsystemDevice, d.SubsystemVendor)
103+
}
104+
return primary
44105
}
45106

46107
// GetVendor returns the 'vendor' portion of a 'DeviceID'.
47108
func (d DeviceID) GetVendor() uint16 {
48-
return uint16(d)
109+
return d.Vendor
49110
}
50111

51112
// GetDevice returns the 'device' portion of a 'DeviceID'.
52113
func (d DeviceID) GetDevice() uint16 {
53-
return uint16(d >> 16)
114+
return d.Device
115+
}
116+
117+
// Primary returns a copy of the 'DeviceID' with any subsystem qualification removed.
118+
func (d DeviceID) Primary() DeviceID {
119+
return NewDeviceID(d.Device, d.Vendor)
120+
}
121+
122+
// Matches checks if a hardware GPU matches the DeviceID filter.
123+
// If the filter has a subsystem defined, it requires an exact match on all 4 components.
124+
// Otherwise, it only matches on the primary device and vendor IDs.
125+
func (filter DeviceID) Matches(hardware DeviceID) bool {
126+
if filter.Device != hardware.Device || filter.Vendor != hardware.Vendor {
127+
return false
128+
}
129+
130+
if filter.HasSubsystem {
131+
if filter.SubsystemDevice != hardware.SubsystemDevice || filter.SubsystemVendor != hardware.SubsystemVendor {
132+
return false
133+
}
134+
}
135+
136+
return true
137+
}
138+
139+
func splitRawDeviceID(raw uint64) (uint16, uint16) {
140+
// nolint:gosec // raw is parsed as a 32-bit value, so both halves fit in a uint16
141+
return uint16(raw >> 16), uint16(raw & 0xFFFF)
54142
}

0 commit comments

Comments
 (0)