Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,23 @@ $ echo $?
1
```

## Supported vGPU frameworks

The NVIDIA vGPU Manager driver exposes vGPU devices through one of two frameworks, depending on the GPU architecture:

- **Mediated devices (mdev)**: used on GPUs up to and including the Ampere architecture. vGPU devices are created through `/sys/class/mdev_bus`.
- **Vendor-specific VFIO**: used by vGPU 17.0+ on the Ada, Hopper and newer architectures (e.g. L40S, H100, H200). There is no mdev bus on these systems. Each GPU is an SR-IOV physical function bound to the `nvidia` driver, and vGPU devices are created on its virtual functions by writing a vGPU type ID to the per-VF `nvidia/current_vgpu_type` sysfs file. See the [NVIDIA vGPU documentation](https://docs.nvidia.com/vgpu/latest/grid-vgpu-user-guide/index.html) for details.

`nvidia-vgpu-dm` detects the framework automatically and uses the same configuration file format and CLI semantics for both. The detection is node-wide: a single framework is selected for all GPUs on the node, so nodes mixing GPUs from both framework generations are not supported.

On systems using the vendor-specific VFIO framework:

- SR-IOV virtual functions are enabled automatically (via the `sriov-manage` script shipped with the vGPU Manager driver) if they are not enabled already. This requires the vGPU Manager driver to be installed on the host root filesystem; with a containerized driver the virtual functions are expected to be enabled by the driver container itself. When `nvidia-vgpu-dm apply` runs inside a container, pass `--host-root-mount` (environment variable `VGPU_DM_HOST_ROOT_MOUNT`) so that `sriov-manage` is run from the host driver installation through chroot.
- MIG-backed vGPU types require MIG mode to be enabled and GPU instances to be created before the vGPU devices can be created. The Kubernetes deployment handles this automatically; standalone CLI users must configure MIG first (e.g. with [mig-parted](https://github.com/NVIDIA/mig-parted)).
- NVML (`libnvidia-ml.so.1`) is used to resolve the vGPU type names of already-created vGPU devices, since the sysfs `creatable_vgpu_types` files only list types that can still be created. When deploying in Kubernetes, use a deployment that mounts the driver installation and sets `LD_PRELOAD` (see `examples/nvidia-vgpu-device-manager-mig-support-example.yaml`).
- MIG mode persists across reboots, but MIG instances, SR-IOV virtual functions and vGPU devices do not; they are re-created on every boot, which is exactly what the Kubernetes deployment does when it applies the selected configuration at node startup.
- Applying a configuration at boot (before VMs are started) is the reliable path. Live re-configuration after MIG changes is best-effort: on some driver versions the `creatable_vgpu_types` enumeration has been observed to come up empty after MIG instances are re-created without a reboot (restarting the host `nvidia-vgpud` service may help).

## Build `nvidia-vgpu-dm`

```
Expand Down
4 changes: 4 additions & 0 deletions cmd/nvidia-k8s-vgpu-dm/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import (
migpartedv1 "github.com/NVIDIA/mig-parted/api/spec/v1"

v1 "github.com/NVIDIA/vgpu-device-manager/api/spec/v1"
"github.com/NVIDIA/vgpu-device-manager/cmd/nvidia-vgpu-dm/apply"
"github.com/NVIDIA/vgpu-device-manager/cmd/nvidia-vgpu-dm/assert"
"github.com/NVIDIA/vgpu-device-manager/internal/info"
)
Expand Down Expand Up @@ -427,6 +428,9 @@ func applyConfig(config string) error {
"-c", config,
}
cmd := exec.Command(cliName, args...)
// Propagate the host root mount so that the vendor-specific VFIO backend
// can run host binaries (e.g. sriov-manage) when enabling SR-IOV VFs.
cmd.Env = append(os.Environ(), fmt.Sprintf("%s=%s", apply.HostRootMountEnvVar, hostRootMountFlag))
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
Expand Down
12 changes: 12 additions & 0 deletions cmd/nvidia-vgpu-dm/apply/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ import (

var log = logrus.New()

// HostRootMountEnvVar is the environment variable used to pass the container
// path where the host root directory is mounted to the 'apply' command.
const HostRootMountEnvVar = "VGPU_DM_HOST_ROOT_MOUNT"

// GetLogger returns the logger for the 'apply' command
func GetLogger() *logrus.Logger {
return log
Expand All @@ -35,6 +39,7 @@ func GetLogger() *logrus.Logger {
// Flags for the 'apply' command
type Flags struct {
assert.Flags
HostRootMount string
}

// Context containing CLI flags and the selected VGPUConfig to apply
Expand Down Expand Up @@ -69,6 +74,13 @@ func BuildCommand() *cli.Command {
Destination: &applyFlags.SelectedConfig,
EnvVars: []string{"VGPU_DM_SELECTED_CONFIG"},
},
&cli.StringFlag{
Name: "host-root-mount",
Aliases: []string{"m"},
Usage: "Container path where the host root directory is mounted; used to run host binaries (e.g. sriov-manage) when configuring vGPU devices on the vendor-specific VFIO framework",
Destination: &applyFlags.HostRootMount,
EnvVars: []string{HostRootMountEnvVar},
},
}

return &apply
Expand Down
15 changes: 13 additions & 2 deletions cmd/nvidia-vgpu-dm/apply/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,25 @@ import (

// VGPUConfig applies the selected vGPU config to the node
func VGPUConfig(c *Context) error {
configManager := vgpu.NewVGPUConfigManager(c.Flags.HostRootMount)
return assert.WalkSelectedVGPUConfigForEachGPU(c.VGPUConfig, func(vc *v1.VGPUConfigSpec, i int, d types.DeviceID) error {
configManager := vgpu.NewNvlibVGPUConfigManager()
current, err := configManager.GetVGPUConfig(i)
if err != nil {
return fmt.Errorf("error getting vGPU config: %v", err)
}

if current.Equals(vc.VGPUDevices) {
// GetVGPUConfig reports vGPU type names as the driver knows them,
// which may differ from the names in the config file by a MIG
// attribute suffix. Compare against the normalized config so that a
// config that is already applied is not torn down and recreated.
desired := vc.VGPUDevices
if normalized, err := configManager.NormalizeVGPUConfig(i, desired); err == nil {
desired = normalized
} else {
log.Debugf(" Unable to normalize the desired vGPU config, comparing as-is: %v", err)
}

if current.Equals(desired) {
log.Debugf(" Skipping -- already set to desired value")
return nil
}
Expand Down
16 changes: 13 additions & 3 deletions cmd/nvidia-vgpu-dm/assert/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,25 @@ func VGPUConfig(c *Context) error {
}

matched := make([]bool, len(gpus))
configManager := vgpu.NewVGPUConfigManager("")
err = WalkSelectedVGPUConfigForEachGPU(c.VGPUConfig, func(vc *v1.VGPUConfigSpec, i int, d types.DeviceID) error {
configManager := vgpu.NewNvlibVGPUConfigManager()
current, err := configManager.GetVGPUConfig(i)
if err != nil {
return fmt.Errorf("error getting vGPU config: %v", err)
}

log.Debugf(" Asserting vGPU config: %v", vc.VGPUDevices)
if current.Equals(vc.VGPUDevices) {
// GetVGPUConfig reports vGPU type names as the driver knows them,
// which may differ from the names in the config file by a MIG
// attribute suffix. Compare against the normalized config.
desired := vc.VGPUDevices
if normalized, err := configManager.NormalizeVGPUConfig(i, desired); err == nil {
desired = normalized
} else {
log.Debugf(" Unable to normalize the desired vGPU config, comparing as-is: %v", err)
}

log.Debugf(" Asserting vGPU config: %v", desired)
if current.Equals(desired) {
log.Debugf(" Skipping -- already set to desired value")
matched[i] = true
return nil
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.26.0

require (
github.com/NVIDIA/go-nvlib v0.12.0
github.com/NVIDIA/go-nvml v0.13.3-1
github.com/NVIDIA/mig-parted v0.14.4
github.com/go-playground/validator/v10 v10.30.3
github.com/google/uuid v1.6.0
Expand All @@ -17,7 +18,6 @@ require (
)

require (
github.com/NVIDIA/go-nvml v0.13.3-1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
Expand Down
27 changes: 27 additions & 0 deletions pkg/types/vgpu_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"fmt"
"regexp"
"strconv"
"strings"
)

const (
Expand All @@ -42,6 +43,32 @@ const (
migBackedRegex = "^(?P<GPU>[A-Z0-9]+)-(?P<G>[1-9])-(?P<GB>0|[1-9][0-9]*)(?P<S>A|B|C|Q)(?P<ATTR>(ME|NOME|MEALL|GFX))?$"
)

// StripAttributeSuffix removes a trailing MIG profile attribute suffix
// (ME, NOME, MEALL, GFX) from a vGPU type name. It returns the name unchanged
// when no known attribute suffix is present.
//
// This is needed because some GPUs (e.g. RTX Pro 6000 Blackwell) only support
// MIG-backed vGPU types on MIG instances created with a particular attribute
// (such as GFX), yet the corresponding vGPU type names do not carry that
// suffix. Stripping the suffix lets the same config type name be matched
// against the set of types the driver reports as creatable.
func StripAttributeSuffix(vgpuType string) string {
// Order matters: MEALL contains ME, so it must be checked first.
suffixes := []string{
AttributeMediaExtensionsAll, // MEALL
AttributeNoMediaExtensions, // NOME
AttributeMediaExtensions, // ME
AttributeGraphics, // GFX
}

for _, suffix := range suffixes {
if strings.HasSuffix(vgpuType, suffix) {
return strings.TrimSuffix(vgpuType, suffix)
}
}
return vgpuType
}

// VGPUType represents a specific vGPU type.
// Time-sliced vGPU types appear as <gpu>-<gb><series>.
// MIG-backed vGPU types appear as <gpu>-<g>-<gb><series>[ME, NOME, MEALL, GFX].
Expand Down
119 changes: 119 additions & 0 deletions pkg/types/vgpu_type_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
* Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package types

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestStripAttributeSuffix(t *testing.T) {
testCases := []struct {
description string
input string
expected string
}{
{
"No suffix - time-sliced vGPU type",
"A100-5C",
"A100-5C",
},
{
"No suffix - MIG-backed vGPU type",
"A100-1-5C",
"A100-1-5C",
},
{
"ME suffix",
"A100-1-5CME",
"A100-1-5C",
},
{
"NOME suffix",
"A100-1-5CNOME",
"A100-1-5C",
},
{
"MEALL suffix",
"A100-1-5CMEALL",
"A100-1-5C",
},
{
"GFX suffix",
"A100-1-5CGFX",
"A100-1-5C",
},
{
"MEALL suffix (should be stripped instead of ME)",
"A100-7-40CMEALL",
"A100-7-40C",
},
{
"Empty string",
"",
"",
},
{
"String with ME in middle (should not be stripped)",
"A100ME-5C",
"A100ME-5C",
},
{
"String with NOME in middle (should not be stripped)",
"A100NOME-5C",
"A100NOME-5C",
},
{
"String with MEALL in middle (should not be stripped)",
"A100MEALL-5C",
"A100MEALL-5C",
},
{
"String with GFX in middle (should not be stripped)",
"A100GFX-5C",
"A100GFX-5C",
},
{
"RTX Ada series with GFX suffix",
"RTX6000-Ada-2QGFX",
"RTX6000-Ada-2Q",
},
{
"RTX Ada series without suffix",
"RTX6000-Ada-2Q",
"RTX6000-Ada-2Q",
},
{
"H100 with ME suffix",
"H100-1-20CME",
"H100-1-20C",
},
{
"Multiple valid suffixes in name - only last one stripped",
"A100-1-5CNOMEME",
"A100-1-5CNOME",
},
}

for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
result := StripAttributeSuffix(tc.input)
require.Equal(t, tc.expected, result, "StripAttributeSuffix(%q) = %q, expected %q", tc.input, result, tc.expected)
})
}
}
Loading