mirror of
https://github.com/hanzoai/visor.git
synced 2026-09-24 17:04:14 +00:00
Add support for NV_ESC_EXPORT_TO_DMABUF_FD, which CUDA issues to export GPU memory as a dma-buf file descriptor. This is the mechanism used for GPUDirect RDMA: the returned fd is handed to ibv_reg_dmabuf_mr so a NIC can DMA directly to and from GPU memory. The driver allocates a host dma-buf and returns its fd in the ioctl's FD field; nvproxy wraps that host fd in a sentry file description installed in the application's fd table. The wrapper implements vfs.HostFDProvider so the RDMA device proxy can recover the host fd for registration. This ioctl exposes GPU memory to external devices, so it is gated behind a new privileged driver capability, CapRDMA, and is only permitted (and its seccomp filter only installed) when that capability is enabled. CapRDMA is enabled automatically when RDMA support is requested. Co-authored-by: Alessio Ricci Toniolo <[email protected]>
278 lines
10 KiB
Go
278 lines
10 KiB
Go
// Copyright 2023 The gVisor Authors.
|
|
//
|
|
// 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 specutils
|
|
|
|
import (
|
|
"fmt"
|
|
"path/filepath"
|
|
"regexp"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
|
|
specs "github.com/opencontainers/runtime-spec/specs-go"
|
|
"gvisor.dev/gvisor/pkg/abi/nvgpu"
|
|
"gvisor.dev/gvisor/pkg/log"
|
|
"gvisor.dev/gvisor/pkg/sentry/devices/nvproxy/nvconf"
|
|
"gvisor.dev/gvisor/runsc/config"
|
|
)
|
|
|
|
const (
|
|
// NVIDIA_VISIBLE_DEVICES environment variable controls which GPUs are
|
|
// visible and accessible to the container.
|
|
nvidiaVisibleDevsEnv = "NVIDIA_VISIBLE_DEVICES"
|
|
// NVIDIA_DRIVER_CAPABILITIES environment variable allows to fine-tune which
|
|
// NVIDIA driver components are mounted and accessible within a container.
|
|
nvidiaDriverCapsEnv = "NVIDIA_DRIVER_CAPABILITIES"
|
|
// CUDA_VERSION environment variable indicates the version of the CUDA
|
|
// toolkit installed on in the container image.
|
|
cudaVersionEnv = "CUDA_VERSION"
|
|
// NVIDIA_REQUIRE_CUDA environment variable indicates the CUDA toolkit
|
|
// version that a container needs.
|
|
requireCudaEnv = "NVIDIA_REQUIRE_CUDA"
|
|
// AnnotationNVProxy enables nvproxy.
|
|
AnnotationNVProxy = "dev.gvisor.internal.nvproxy"
|
|
)
|
|
|
|
// NVProxyEnabled returns true if nvproxy should be enabled.
|
|
func NVProxyEnabled(spec *specs.Spec, conf *config.Config) bool {
|
|
return conf.NVProxy || AnnotationToBool(spec, AnnotationNVProxy) || GPUFunctionalityRequested(spec, conf)
|
|
}
|
|
|
|
// GPUFunctionalityRequested returns true if the container should have access
|
|
// to GPU functionality based on the spec.
|
|
func GPUFunctionalityRequested(spec *specs.Spec, conf *config.Config) bool {
|
|
// In GKE, the nvidia_gpu device plugin injects NVIDIA devices into
|
|
// spec.Linux.Devices when GPUs are allocated to a container.
|
|
if spec.Linux != nil {
|
|
for _, dev := range spec.Linux.Devices {
|
|
if dev.Path == "/dev/nvidiactl" {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return GPUFunctionalityRequestedViaHook(spec, conf)
|
|
}
|
|
|
|
// GPUFunctionalityRequestedViaHook returns true if the container should have
|
|
// access to GPU functionality configured via nvidia-container-runtime-hook.
|
|
// This hook is used by:
|
|
// - Docker when using `--gpus` flag from the CLI.
|
|
// - nvidia-container-runtime when using its legacy mode.
|
|
func GPUFunctionalityRequestedViaHook(spec *specs.Spec, conf *config.Config) bool {
|
|
kind := findNvidiaHook(spec, conf)
|
|
if kind == nvidiaHookNone {
|
|
return false
|
|
}
|
|
// In Docker mode, GPU access is only requested if NVIDIA_VISIBLE_DEVICES is
|
|
// non-empty and set to a value that doesn't mean "no GPU".
|
|
if spec.Process == nil {
|
|
return false
|
|
}
|
|
nvd := nvidiaVisibleDevsEnvVar(spec.Process.Env, kind)
|
|
// A value of "none" means "no GPU device, but still access to driver
|
|
// functionality", so it is not a value we check for here.
|
|
return nvd != "" && nvd != "void"
|
|
}
|
|
|
|
type nvidiaHookKind uint8
|
|
|
|
const (
|
|
nvidiaHookNone nvidiaHookKind = iota
|
|
nvidiaHookLegacy
|
|
nvidiaHookCDI
|
|
)
|
|
|
|
func findNvidiaHook(spec *specs.Spec, conf *config.Config) nvidiaHookKind {
|
|
if spec.Hooks != nil {
|
|
for _, h := range spec.Hooks.CreateContainer {
|
|
if strings.HasSuffix(h.Path, "/nvidia-cdi-hook") {
|
|
return nvidiaHookCDI
|
|
}
|
|
}
|
|
for _, h := range spec.Hooks.Prestart {
|
|
if strings.HasSuffix(h.Path, "/nvidia-container-runtime-hook") {
|
|
return nvidiaHookLegacy
|
|
}
|
|
}
|
|
}
|
|
if conf.NVProxyDocker {
|
|
// This has the effect of injecting the nvidia-container-runtime-hook.
|
|
return nvidiaHookLegacy
|
|
}
|
|
return nvidiaHookNone
|
|
}
|
|
|
|
func nvidiaVisibleDevsEnvVar(envs []string, kind nvidiaHookKind) string {
|
|
if kind != nvidiaHookCDI {
|
|
nvd, _ := EnvVar(envs, nvidiaVisibleDevsEnv)
|
|
return nvd
|
|
}
|
|
// After
|
|
// https://github.com/NVIDIA/nvidia-container-toolkit/commit/88ad42ccd16a3ba6fd7867511d4070aa8111b8fa,
|
|
// when Docker communicates with nvidia-container-toolkit via CDI, the
|
|
// latter always appends a "NVIDIA_VISIBLE_DEVICES=void" environment
|
|
// variable to the container spec, which we want to ignore.
|
|
const prefix = nvidiaVisibleDevsEnv + "="
|
|
nvds := make([]string, 0, 2)
|
|
for _, env := range envs {
|
|
if strings.HasPrefix(env, prefix) {
|
|
nvds = append(nvds, env[len(prefix):])
|
|
}
|
|
}
|
|
if len(nvds) == 0 {
|
|
return ""
|
|
}
|
|
if len(nvds) > 1 && nvds[len(nvds)-1] == "void" {
|
|
return nvds[len(nvds)-2]
|
|
}
|
|
return nvds[len(nvds)-1]
|
|
}
|
|
|
|
// ParseNvidiaVisibleDevices parses NVIDIA_VISIBLE_DEVICES env var and returns
|
|
// the devices specified in it. This can be passed to nvidia-container-cli.
|
|
//
|
|
// Precondition: GPUFunctionalityRequestedViaHook(spec, conf).
|
|
func ParseNvidiaVisibleDevices(spec *specs.Spec, conf *config.Config) (string, error) {
|
|
nvd := nvidiaVisibleDevsEnvVar(spec.Process.Env, findNvidiaHook(spec, conf))
|
|
if nvd == "none" {
|
|
return "", nil
|
|
}
|
|
if nvd == "all" {
|
|
return "all", nil
|
|
}
|
|
|
|
for _, gpuDev := range strings.Split(nvd, ",") {
|
|
// Validate gpuDev. We only support the following formats for now:
|
|
// * GPU indices (e.g. 0,1,2)
|
|
// * GPU UUIDs (e.g. GPU-fef8089b)
|
|
//
|
|
// We do not support MIG devices yet.
|
|
if strings.HasPrefix(gpuDev, "GPU-") {
|
|
continue
|
|
}
|
|
_, err := strconv.ParseUint(gpuDev, 10, 32)
|
|
if err != nil {
|
|
return "", fmt.Errorf("invalid %q in NVIDIA_VISIBLE_DEVICES %q: %w", gpuDev, nvd, err)
|
|
}
|
|
}
|
|
return nvd, nil
|
|
}
|
|
|
|
// NVProxyDriverCapsAllowed returns the driver capabilities allowed by the
|
|
// configuration, irrespective of what a container requests.
|
|
// This should be used to determine the bounding set of driver capabilities
|
|
// that a container can request.
|
|
func NVProxyDriverCapsAllowed(conf *config.Config) (nvconf.DriverCaps, error) {
|
|
// Construct the set of allowed driver capabilities.
|
|
allowedDriverCaps, hasAll, err := nvconf.DriverCapsFromString(conf.NVProxyAllowedDriverCapabilities)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("invalid set of allowed NVIDIA driver capabilities %q: %w", conf.NVProxyAllowedDriverCapabilities, err)
|
|
}
|
|
// allowedDriverCaps is already a subset of `nvconf.SupportedDriverCaps`
|
|
// as this was checked by `config.Config.validate`.
|
|
if hasAll {
|
|
allowedDriverCaps |= nvconf.AllContainerDriverCaps
|
|
}
|
|
// GPUDirect RDMA requires the privileged CapRDMA capability to export GPU
|
|
// memory to a dma-buf fd. There is no container-facing driver-capability
|
|
// flag for it, so enable it implicitly whenever RDMA passthrough is on.
|
|
if conf.RDMAProxy {
|
|
allowedDriverCaps |= nvconf.CapRDMA
|
|
}
|
|
return allowedDriverCaps, nil
|
|
}
|
|
|
|
// NVProxyDriverCapsFromEnv returns the driver capabilities requested by the
|
|
// application via the NVIDIA_DRIVER_CAPABILITIES env var. See
|
|
// nvidia-container-toolkit/cmd/nvidia-container-runtime-hook/container_config.go:getDriverCapabilities().
|
|
func NVProxyDriverCapsFromEnv(spec *specs.Spec, conf *config.Config) (nvconf.DriverCaps, error) {
|
|
allowedDriverCaps, err := NVProxyDriverCapsAllowed(conf)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
// Extract the set of driver capabilities requested by the application.
|
|
driverCapsEnvStr, ok := EnvVar(spec.Process.Env, nvidiaDriverCapsEnv)
|
|
if !ok {
|
|
// Nothing requested. Fallback to default configurations.
|
|
if IsLegacyCudaImage(spec) {
|
|
return allowedDriverCaps, nil
|
|
}
|
|
return nvconf.DefaultDriverCaps & allowedDriverCaps, nil
|
|
}
|
|
if len(driverCapsEnvStr) == 0 {
|
|
// Empty. Fallback to nvconf.DefaultDriverCaps.
|
|
return nvconf.DefaultDriverCaps & allowedDriverCaps, nil
|
|
}
|
|
envDriverCaps, enableAll, err := nvconf.DriverCapsFromString(driverCapsEnvStr)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("invalid set of requested NVIDIA driver capabilities %q: %w", driverCapsEnvStr, err)
|
|
}
|
|
if enableAll {
|
|
// The "all" keyword here is confusing but we need to match the behavior of
|
|
// nvidia-container-toolkit:cmd/nvidia-container-runtime-hook/container_config.go:getDriverCapabilities.
|
|
// If the environment variable contains "all", the intuitive thing to do
|
|
// would be to expand it to mean "all allowed capabilities". Rather, in
|
|
// nvidia-container-toolkit, it means "expand to the entire set of
|
|
// capabilities, then silently drop all disallowed or unsupported
|
|
// capabilities from this set".
|
|
// We aim to be drop-in compatible with this behavior so we need to
|
|
// implement it too, but log a warning when these behaviors result in a
|
|
// different outcome.
|
|
if intuitiveCaps := envDriverCaps | allowedDriverCaps; intuitiveCaps != allowedDriverCaps {
|
|
log.Warningf("Container requested NVIDIA driver capabilities %q; this expands to %v which is a larger set than allowed capabilities (%v). The extra capabilities (%v) will be dropped.", driverCapsEnvStr, intuitiveCaps, allowedDriverCaps, intuitiveCaps&^allowedDriverCaps)
|
|
}
|
|
return allowedDriverCaps, nil
|
|
}
|
|
// Intersect what's requested with what's allowed.
|
|
if driverCaps := allowedDriverCaps & envDriverCaps; driverCaps != envDriverCaps {
|
|
return 0, fmt.Errorf(`disallowed driver capabilities requested: "%v" (allowed "%v"), update --nvproxy-allowed-driver-capabilities to allow them`, envDriverCaps, driverCaps)
|
|
}
|
|
return envDriverCaps, nil
|
|
}
|
|
|
|
// IsLegacyCudaImage returns true if spec represents a legacy CUDA image.
|
|
// See nvidia-container-toolkit/internal/config/image/cuda_image.go:IsLegacy().
|
|
func IsLegacyCudaImage(spec *specs.Spec) bool {
|
|
cudaVersion, _ := EnvVar(spec.Process.Env, cudaVersionEnv)
|
|
requireCuda, _ := EnvVar(spec.Process.Env, requireCudaEnv)
|
|
return len(cudaVersion) > 0 && len(requireCuda) == 0
|
|
}
|
|
|
|
// removeNvproxyFrontendDevices removes regular Nvidia frontend devices from
|
|
// both o and n, since we handle these by remapping.
|
|
func removeNvproxyFrontendDevices(field, cName string, o, n []specs.LinuxDevice) ([]specs.LinuxDevice, []specs.LinuxDevice, error) {
|
|
nvidiaDeviceRegex := regexp.MustCompile(`^nvidia(\d+)$`)
|
|
shouldDelete := func(dev specs.LinuxDevice) bool {
|
|
basename := filepath.Base(dev.Path)
|
|
ms := nvidiaDeviceRegex.FindStringSubmatch(basename)
|
|
if ms == nil {
|
|
return false
|
|
}
|
|
minor, err := strconv.ParseUint(ms[1], 10, 32)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
if minor > nvgpu.NV_MINOR_DEVICE_NUMBER_REGULAR_MAX {
|
|
return false
|
|
}
|
|
return dev.Major == nvgpu.NV_MAJOR_DEVICE_NUMBER && dev.Minor == int64(minor)
|
|
}
|
|
o = slices.DeleteFunc(slices.Clone(o), shouldDelete)
|
|
n = slices.DeleteFunc(slices.Clone(n), shouldDelete)
|
|
return o, n, nil
|
|
}
|