Skip to content
Draft
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
472 changes: 470 additions & 2 deletions THIRD_PARTY_NOTICES.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
/**
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: remove the year from the copyright header

# SPDX-License-Identifier: Apache-2.0
#
# 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 cudamemorylimits

import (
"context"
"fmt"
"strconv"
"strings"

"github.com/NVIDIA/go-nvml/pkg/nvml"
"github.com/urfave/cli/v3"

cgroupinfo "github.com/NVIDIA/nvidia-container-toolkit/internal/info/cgroup"
"github.com/NVIDIA/nvidia-container-toolkit/internal/logger"
"github.com/NVIDIA/nvidia-container-toolkit/internal/oci"
"github.com/NVIDIA/nvidia-container-toolkit/pkg/lookup"
)

type command struct {
logger logger.Interface
}

type config struct {
driverRoot string
gpuIds []string
containerSpec string
}

func NewCommand(logger logger.Interface) *cli.Command {
c := command{
logger: logger,
}
return c.build()
}

func (m command) build() *cli.Command {
cfg := config{}

c := cli.Command{
Name: "apply-cuda-memory-limits",
Usage: "Set the soft and hard limits of CUDA memory usage on a GPU device in the container.",
Before: func(ctx context.Context, cmd *cli.Command) (context.Context, error) {
return ctx, m.validateFlags(cmd, &cfg)
},
Action: func(ctx context.Context, cmd *cli.Command) error {
return m.run(cmd, &cfg)
},
Flags: []cli.Flag{
&cli.StringFlag{
Name: "driver-root",
Usage: "Specify the driver root",
Destination: &cfg.driverRoot,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question -- do we need to define a default value for this field, or validate that it is specified in validateFlags()?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. What would you suggest here?

},
&cli.StringSliceFlag{
Name: "gpu-id",
Usage: "Specify the UUID of the GPU",
Destination: &cfg.gpuIds,
},
&cli.StringFlag{
Name: "container-spec",
Usage: "Specify the path to the OCI container spec. If empty or '-' the spec will be read from STDIN",
Destination: &cfg.containerSpec,
},
},
}

return &c
}

func (m command) validateFlags(_ *cli.Command, cfg *config) error {
for _, id := range cfg.gpuIds {
if strings.TrimSpace(id) == "" {
return fmt.Errorf("gpu-id must not be empty")
}
}

return nil
}

func (m command) run(_ *cli.Command, cfg *config) error {
s, err := oci.LoadContainerState(cfg.containerSpec)
if err != nil {
return fmt.Errorf("failed to load container state: %w", err)
}
specFilePath := oci.GetSpecFilePath(s.Bundle)
fs := oci.NewFileSpec(specFilePath, false)
ctrSpec, err := fs.Load()
if err != nil {
return fmt.Errorf("failed to load OCI container spec: %w", err)
}

memReqStr, ok1 := fs.LookupEnv("NVIDIA_GPU_MEMORY_REQUESTS")
if !ok1 {
memReqStr, ok1 = fs.LookupEnv("NVIDIA_GPU_MEMORY_REQUEST")
}
memLimitStr, ok2 := fs.LookupEnv("NVIDIA_GPU_MEMORY_LIMITS")
if !ok2 {
memLimitStr, ok2 = fs.LookupEnv("NVIDIA_GPU_MEMORY_LIMIT")
}
if !ok1 || !ok2 {
return nil
}
Comment on lines +116 to +118

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question -- does this mean that it is invalid to only specify one of these envvars (but not the other)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am open to changing this. We can just limit it to 2 out of the 4 env vars here (maybe drop the plurals?) for the sake of simplicity here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My question was more so out of curiosity. Is it valid to only specify a memory limit and not a request?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I believe it is. If unspecified, the value stays the same as it was


if !cgroupinfo.IsCgroupV2() {
return fmt.Errorf("setting GPU memory limits is only supported in cgroup v2")
}

cgroupPath, err := cgroupinfo.GetAbsolutePath(*ctrSpec)
if err != nil {
return fmt.Errorf("failed to resolve cgroup path: %w", err)
}

memoryRequests, err := strconv.ParseUint(memReqStr, 10, 64)
if err != nil {
return fmt.Errorf("failed to parse NVIDIA_GPU_MEMORY_REQUESTS: %w", err)
}

memoryLimits, err := strconv.ParseUint(memLimitStr, 10, 64)
if err != nil {
return fmt.Errorf("failed to parse NVIDIA_GPU_MEMORY_LIMITS: %w", err)
}
if memoryRequests > memoryLimits {
return fmt.Errorf("memory request (%d MiB) exceeds memory limit (%d MiB)", memoryRequests, memoryLimits)
}

return m.runApplyCudaMemoryLimits(cgroupPath, memoryRequests, memoryLimits, cfg.driverRoot, cfg.gpuIds)
}

func (m command) runApplyCudaMemoryLimits(cgroupPath string, requests uint64, limits uint64, driverRoot string, gpuIDs []string) error {

driverLibLocator := lookup.NewLibraryLocator(
lookup.WithLogger(m.logger),
lookup.WithRoot(driverRoot),
)

candidates, err := driverLibLocator.Locate("libnvidia-ml.so.1")
if err != nil {
return fmt.Errorf("failed to locate libnvidia-ml.so.1: %w", err)
}
if len(candidates) == 0 {
return fmt.Errorf("no libnvidia-ml.so.1 found")
}

m.logger.Infof("driver library found: %s", candidates[0])

nvmllib := nvml.New(nvml.WithLibraryPath(candidates[0]))
ret := nvmllib.Init()
if ret != nvml.SUCCESS {
return fmt.Errorf("failed to initialize nvml: %v", ret)
}
defer func() {
_ = nvmllib.Shutdown()
}()

for _, gpuID := range gpuIDs {
device, ret := nvmllib.DeviceGetHandleByUUID(gpuID)
if ret != nvml.SUCCESS {
return fmt.Errorf("failed to get GPU device handle with uuid %s: %v", gpuID, ret)
}
if device == nil {
return fmt.Errorf("empty GPU device handle: %s", gpuID)
}
ret = device.SetMemoryLimits_v1(cgroupPath, int(requests*1024*1024), int(limits*1024*1024))
if ret != nvml.SUCCESS {
return fmt.Errorf("failed to set memory limits for gpu %q: %v", gpuID, ret)
}
}
return nil
}
2 changes: 2 additions & 0 deletions cmd/nvidia-cdi-hook/commands/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (

"github.com/urfave/cli/v3"

cudamemorylimits "github.com/NVIDIA/nvidia-container-toolkit/cmd/nvidia-cdi-hook/apply-cuda-memory-limits"
"github.com/NVIDIA/nvidia-container-toolkit/cmd/nvidia-cdi-hook/chmod"
symlinks "github.com/NVIDIA/nvidia-container-toolkit/cmd/nvidia-cdi-hook/create-symlinks"
"github.com/NVIDIA/nvidia-container-toolkit/cmd/nvidia-cdi-hook/cudacompat"
Expand Down Expand Up @@ -91,6 +92,7 @@ func ConfigureCDIHookCommand(logger logger.Interface, base *cli.Command) *cli.Co
chmod.NewCommand(logger),
cudacompat.NewCommand(logger),
disabledevicenodemodification.NewCommand(logger),
cudamemorylimits.NewCommand(logger),
updateapplicationprofile.NewCommand(logger),
{
Name: "noop",
Expand Down
40 changes: 35 additions & 5 deletions cmd/nvidia-ctk/cdi/generate/generate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,35 @@ devices:
deviceNodes:
- path: /dev/nvidia0
hostPath: {{ .driverRoot }}/dev/nvidia0
hooks:
- hookName: createRuntime
path: /usr/bin/nvidia-cdi-hook
args:
- nvidia-cdi-hook
- apply-cuda-memory-limits
- --driver-root
- {{ .driverRoot }}
- --gpu-id
- {{ .gpuID }}
env:
- NVIDIA_CTK_DEBUG=false
- name: all
containerEdits:
deviceNodes:
- path: /dev/nvidia0
hostPath: {{ .driverRoot }}/dev/nvidia0
hooks:
- hookName: createRuntime
path: /usr/bin/nvidia-cdi-hook
args:
- nvidia-cdi-hook
- apply-cuda-memory-limits
- --driver-root
- {{ .driverRoot }}
- --gpu-id
- {{ .gpuID }}
env:
- NVIDIA_CTK_DEBUG=false
containerEdits:
env:
- NVIDIA_CTK_LIBCUDA_DIR=/lib/x86_64-linux-gnu
Expand Down Expand Up @@ -180,7 +204,7 @@ containerEdits:
vendor: "example.com",
class: "device",
driverRoot: driverRoot,
disabledHooks: []string{"enable-cuda-compat"},
disabledHooks: []string{"enable-cuda-compat", "apply-cuda-memory-limits"},
},
expectedOptions: options{
format: "yaml",
Expand All @@ -189,7 +213,7 @@ containerEdits:
class: "device",
nvidiaCDIHookPath: "/usr/bin/nvidia-cdi-hook",
driverRoot: driverRoot,
disabledHooks: []string{"enable-cuda-compat"},
disabledHooks: []string{"enable-cuda-compat", "apply-cuda-memory-limits"},
},
expectedSpec: `---
cdiVersion: 0.5.0
Expand Down Expand Up @@ -274,7 +298,7 @@ containerEdits:
vendor: "example.com",
class: "device",
driverRoot: driverRoot,
disabledHooks: []string{"enable-cuda-compat", "update-ldcache"},
disabledHooks: []string{"enable-cuda-compat", "apply-cuda-memory-limits", "update-ldcache"},
},
expectedOptions: options{
format: "yaml",
Expand All @@ -283,7 +307,7 @@ containerEdits:
class: "device",
nvidiaCDIHookPath: "/usr/bin/nvidia-cdi-hook",
driverRoot: driverRoot,
disabledHooks: []string{"enable-cuda-compat", "update-ldcache"},
disabledHooks: []string{"enable-cuda-compat", "apply-cuda-memory-limits", "update-ldcache"},
},
expectedSpec: `---
cdiVersion: 0.5.0
Expand Down Expand Up @@ -539,7 +563,13 @@ containerEdits:
require.NoError(t, err)
}

require.Equal(t, strings.ReplaceAll(tc.expectedSpec, "{{ .driverRoot }}", driverRoot), buf.String())
gpuID, ret := server.Devices[0].GetUUID()
require.True(t, ret == nvml.SUCCESS, gpuID)

expected := strings.ReplaceAll(tc.expectedSpec, "{{ .driverRoot }}", driverRoot)
expected = strings.ReplaceAll(expected, "{{ .gpuID }}", gpuID)

require.Equal(t, expected, buf.String())
})
}
}
Expand Down
5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ go 1.25.0
require (
github.com/Masterminds/semver/v3 v3.5.0
github.com/NVIDIA/go-nvlib v0.12.0
github.com/NVIDIA/go-nvml v0.13.3-1
github.com/NVIDIA/go-nvml v0.13.3-1.0.20260814002628-7f946c0908a3
github.com/containerd/nri v0.12.1
github.com/cyphar/filepath-securejoin v0.7.0
github.com/google/uuid v1.6.0
Expand All @@ -31,12 +31,15 @@ require (
cyphar.com/go-pathrs v0.2.5 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/ttrpc v1.2.7 // indirect
github.com/coreos/go-systemd/v22 v22.7.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/knqyf263/go-plugin v0.9.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/moby/sys/capability v0.4.0 // indirect
github.com/moby/sys/userns v0.1.0 // indirect
github.com/opencontainers/runtime-tools v0.9.1-0.20251114084447-edf4cb3d2116 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.11.0 // indirect
Expand Down
10 changes: 8 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAw
github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/NVIDIA/go-nvlib v0.12.0 h1:LICVlUGlDnpbwQv64rOZQn11xQae1J+c+dvH9CCi7jc=
github.com/NVIDIA/go-nvlib v0.12.0/go.mod h1:J5M/QPIJJtaipjdONqevSnfgBlkW49uVWX5cFOoQpoA=
github.com/NVIDIA/go-nvml v0.13.3-1 h1:P76U2h88OZSiMtdhRsJjSF5DXyXUqHIXKeDicVAaae0=
github.com/NVIDIA/go-nvml v0.13.3-1/go.mod h1:ahi2psRYoa+wYUBIrZPRO+wJs9lcvMhxSSkjjvsJJNQ=
github.com/NVIDIA/go-nvml v0.13.3-1.0.20260814002628-7f946c0908a3 h1:r6oO3PV4w/DCEqY7EXbZsQCu5TGs2oM0owr1vWvQxHg=
github.com/NVIDIA/go-nvml v0.13.3-1.0.20260814002628-7f946c0908a3/go.mod h1:ahi2psRYoa+wYUBIrZPRO+wJs9lcvMhxSSkjjvsJJNQ=
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
github.com/brianvoe/gofakeit/v7 v7.12.1 h1:df1tiI4SL1dR5Ix4D/r6a3a+nXBJ/OBGU5jEKRBmmqg=
Expand All @@ -16,6 +16,8 @@ github.com/containerd/nri v0.12.1 h1:Nkp14W/mdhP0ze83ja/O437du6NQA33W5Y0O+ar3aKM
github.com/containerd/nri v0.12.1/go.mod h1:TGAfPLH4a+qwbv0PxsefPiR+PobYecDj2aXMtz7GQcg=
github.com/containerd/ttrpc v1.2.7 h1:qIrroQvuOL9HQ1X6KHe2ohc7p+HP/0VE6XPU7elJRqQ=
github.com/containerd/ttrpc v1.2.7/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o=
github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA=
github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cyphar/filepath-securejoin v0.7.0 h1:s0Y3ITPy6sQn5xt54DuYvTF8hu134ooYLUb58DX/HjE=
github.com/cyphar/filepath-securejoin v0.7.0/go.mod h1:ymLGms/u3BYaviIiuKFnUx8EkQEZeK6cInNoAPJA3o4=
Expand All @@ -27,6 +29,8 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
Expand Down Expand Up @@ -54,6 +58,8 @@ github.com/moby/sys/reexec v0.1.0 h1:RrBi8e0EBTLEgfruBOFcxtElzRGTEUkeIFaVXgU7wok
github.com/moby/sys/reexec v0.1.0/go.mod h1:EqjBg8F3X7iZe5pU6nRZnYCMUTXoxsjiIfHup5wYIN8=
github.com/moby/sys/symlink v0.3.0 h1:GZX89mEZ9u53f97npBy4Rc3vJKj7JBDj/PN2I22GrNU=
github.com/moby/sys/symlink v0.3.0/go.mod h1:3eNdhduHmYPcgsJtZXW1W4XUJdZGBIkttZ8xKqPUJq0=
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/onsi/ginkgo/v2 v2.19.1 h1:QXgq3Z8Crl5EL1WBAC98A5sEBHARrAJNzAmMxzLcRF0=
github.com/onsi/ginkgo/v2 v2.19.1/go.mod h1:O3DtEWQkPa/F7fBMgmZQKKsluAy8pd3rEQdrjkPb9zA=
github.com/onsi/gomega v1.34.0 h1:eSSPsPNp6ZpsG8X1OVmOTxig+CblTc4AxpPBykhe2Os=
Expand Down
11 changes: 10 additions & 1 deletion internal/discover/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ const (
// An UpdateLDCacheHook is the hook used to update the ldcache in the
// container. This allows injected libraries to be discoverable.
UpdateLDCacheHook = HookName("update-ldcache")
// ApplyCudaMemoryLimitsHook is used to assign soft and hard limits of CUDA memory usage to a container
ApplyCudaMemoryLimitsHook = HookName("apply-cuda-memory-limits")

defaultNvidiaCDIHookPath = "/usr/bin/nvidia-cdi-hook"
)
Expand Down Expand Up @@ -222,6 +224,8 @@ func (c cdiHookCreator) getOCIHookType(name HookName) OCIHookType {
switch name {
case CreateSymlinksHook, ChmodHook, DisableDeviceNodeModificationHook, EnableCudaCompatHook, UpdateLDCacheHook, ApplicationProfileHook:
return OCIHookTypeCreateContainer
case ApplyCudaMemoryLimitsHook:
return OCIHookTypeCreateRuntime
default:
return OCIHookTypeCreateContainer
}
Expand All @@ -238,7 +242,7 @@ func (c cdiHookCreator) isDisabled(name HookName, args ...string) bool {

// still reject hooks that require args if none were provided
switch name {
case CreateSymlinksHook, ChmodHook:
case CreateSymlinksHook, ChmodHook, ApplyCudaMemoryLimitsHook:
return len(args) == 0
}
return false
Expand Down Expand Up @@ -267,6 +271,11 @@ func (c cdiHookCreator) transformArgs(name HookName, args ...string) []string {
for _, arg := range args {
transformedArgs = append(transformedArgs, "--folder", arg)
}
case ApplyCudaMemoryLimitsHook:
transformedArgs = append(transformedArgs, "--driver-root", args[0])
for _, arg := range args[1:] {
transformedArgs = append(transformedArgs, "--gpu-id", arg)
}
default:
return args
}
Expand Down
Loading
Loading