| package daemon |
| |
| import ( |
| "context" |
| "errors" |
| "fmt" |
| "os" |
| "os/exec" |
| "strconv" |
| "strings" |
| |
| "github.com/containerd/log" |
| "github.com/moby/moby/api/types/container" |
| "github.com/moby/moby/v2/daemon/internal/capabilities" |
| "github.com/opencontainers/runtime-spec/specs-go" |
| ) |
| |
| // TODO: nvidia should not be hard-coded, and should be a device plugin instead on the daemon object. |
| // TODO: add list of device capabilities in daemon/node info |
| |
| var errConflictCountDeviceIDs = errors.New("cannot set both Count and DeviceIDs on device request") |
| |
| const ( |
| nvidiaContainerRuntimeHookExecutableName = "nvidia-container-runtime-hook" |
| nvidiaCDIHookExecutableName = "nvidia-cdi-hook" |
| ) |
| |
| // These are NVIDIA-specific capabilities stolen from github.com/containerd/containerd/contrib/nvidia.allCaps |
| var allNvidiaCaps = map[string]struct{}{ |
| "compute": {}, |
| "compat32": {}, |
| "graphics": {}, |
| "utility": {}, |
| "video": {}, |
| "display": {}, |
| } |
| |
| func getNVIDIADeviceDrivers() map[string]*deviceDriver { |
| var composite firstSuccessfulUpdater |
| nvidiaDrivers := make(map[string]*deviceDriver) |
| |
| if _, err := exec.LookPath(nvidiaCDIHookExecutableName); err == nil { |
| // Register a driver specific to CDI if present. |
| // This has no capabilities associated to not inadvertently match requests. |
| cdiDeviceDriver := &deviceDriver{ |
| updateSpec: (&cdiDeviceInjector{ |
| defaultCDIDeviceKind: "nvidia.com/gpu", |
| }).injectDevices, |
| } |
| nvidiaDrivers["nvidia.cdi"] = cdiDeviceDriver |
| composite = append(composite, cdiDeviceDriver.updateSpec) |
| } |
| |
| if _, err := exec.LookPath(nvidiaContainerRuntimeHookExecutableName); err == nil { |
| // Register a driver specific to the nvidia-container-runtime-hook if present. |
| // This has no capabilities associated to not inadvertently match requests. |
| runtimeHookDeviceDriver := &deviceDriver{ |
| updateSpec: injectNVIDIARuntimeHook, |
| } |
| nvidiaDrivers["nvidia.runtime-hook"] = runtimeHookDeviceDriver |
| composite = append(composite, runtimeHookDeviceDriver.updateSpec) |
| } |
| |
| if len(nvidiaDrivers) == 0 { |
| return nil |
| } |
| |
| // We associate all NVIDIA capabilities with the composite updater |
| capset := capabilities.Set{"gpu": struct{}{}, "nvidia": struct{}{}} |
| for c := range allNvidiaCaps { |
| capset[c] = struct{}{} |
| } |
| nvidiaDrivers["nvidia"] = &deviceDriver{ |
| capset: capset, |
| updateSpec: composite.updateSpec, |
| } |
| |
| return nvidiaDrivers |
| } |
| |
| // specUpdaters refer to a list of functions used updated an OCI spec for a |
| // given device instance. |
| type firstSuccessfulUpdater []func(*specs.Spec, *deviceInstance) error |
| |
| // updateSpec returns on the first successful spec update. |
| func (us firstSuccessfulUpdater) updateSpec(s *specs.Spec, dev *deviceInstance) error { |
| var errs []error |
| for _, u := range us { |
| if u == nil { |
| continue |
| } |
| if err := u(s, dev); err != nil { |
| errs = append(errs, err) |
| continue |
| } |
| if len(errs) > 0 { |
| log.G(context.TODO()).WithError(errors.Join(errs...)).Warning("Ignoring previous errors updating spec") |
| } |
| return nil |
| } |
| return errors.Join(errs...) |
| } |
| |
| // injectNVIDIARuntimeHook handles requests for NVIDIA GPUs. |
| // This is done by updating the OCI runtime spec to include the correct value |
| // for the NVIDIA_VISIBLE_DEVICES environment variable and injecting the |
| // NVIDIA Container Runtime Hook as a container prestart hook. |
| func injectNVIDIARuntimeHook(s *specs.Spec, dev *deviceInstance) error { |
| deviceIDs, err := getRequestedDevicesIDs(dev.req) |
| if err != nil { |
| return err |
| } |
| if len(deviceIDs) == 0 { |
| return nil |
| } |
| s.Process.Env = append(s.Process.Env, "NVIDIA_VISIBLE_DEVICES="+strings.Join(deviceIDs, ",")) |
| |
| var nvidiaCaps []string |
| // req.Capabilities contains device capabilities, some but not all are NVIDIA driver capabilities. |
| for _, c := range dev.selectedCaps { |
| if _, isNvidiaCap := allNvidiaCaps[c]; isNvidiaCap { |
| nvidiaCaps = append(nvidiaCaps, c) |
| continue |
| } |
| // TODO: nvidia.WithRequiredCUDAVersion |
| // for now we let the prestart hook verify cuda versions but errors are not pretty. |
| } |
| |
| if nvidiaCaps != nil { |
| s.Process.Env = append(s.Process.Env, "NVIDIA_DRIVER_CAPABILITIES="+strings.Join(nvidiaCaps, ",")) |
| } |
| |
| path, err := exec.LookPath(nvidiaContainerRuntimeHookExecutableName) |
| if err != nil { |
| return err |
| } |
| |
| if s.Hooks == nil { |
| s.Hooks = &specs.Hooks{} |
| } |
| |
| // This implementation uses prestart hooks, which are deprecated. |
| // CreateRuntime is the closest equivalent, and executed in the same |
| // locations as prestart-hooks, but depending on what these hooks do, |
| // possibly one of the other hooks could be used instead (such as |
| // CreateContainer or StartContainer). |
| s.Hooks.Prestart = append(s.Hooks.Prestart, specs.Hook{ //nolint:staticcheck // FIXME(thaJeztah); replace prestart hook with a non-deprecated one. |
| Path: path, |
| Args: []string{ |
| nvidiaContainerRuntimeHookExecutableName, |
| "prestart", |
| }, |
| Env: os.Environ(), |
| }) |
| |
| return nil |
| } |
| |
| // getRequestedDeviceIDs returns the list of requested devices by ID based on |
| // the device request. |
| func getRequestedDevicesIDs(req container.DeviceRequest) ([]string, error) { |
| if req.Count != 0 && len(req.DeviceIDs) > 0 { |
| return nil, errConflictCountDeviceIDs |
| } |
| |
| switch { |
| case len(req.DeviceIDs) > 0: |
| return req.DeviceIDs, nil |
| case req.Count > 0: |
| return countToDevices(req.Count), nil |
| case req.Count < 0: |
| return []string{"all"}, nil |
| case req.Count == 0: |
| return nil, nil |
| } |
| return nil, nil |
| } |
| |
| // countToDevices returns the list 0, 1, ... count-1 of deviceIDs. |
| func countToDevices(count int) []string { |
| devices := make([]string, count) |
| for i := range devices { |
| devices[i] = strconv.Itoa(i) |
| } |
| return devices |
| } |
| |
| // A cdiDeviceInjector is used to map regular device requests to CDI device |
| // requests. |
| type cdiDeviceInjector struct { |
| defaultCDIDeviceKind string |
| } |
| |
| // injectDevices converts an incoming device request to a request for devices |
| // using CDI. |
| // The requested device IDs are converted to CDI device names if required using |
| // the specified default kind. |
| func (i *cdiDeviceInjector) injectDevices(s *specs.Spec, dev *deviceInstance) error { |
| deviceIDs, err := getRequestedDevicesIDs(dev.req) |
| if err != nil { |
| return err |
| } |
| if len(deviceIDs) == 0 { |
| return nil |
| } |
| |
| // If the cdi device driver is not available then we return an error. |
| cdiDeviceDriver := deviceDrivers["cdi"] |
| if cdiDeviceDriver == nil { |
| return fmt.Errorf("no CDI device driver registered: %w", incompatibleDeviceRequest{dev.req.Driver, dev.req.Capabilities}) |
| } |
| |
| var cdiDeviceIDs []string |
| for _, deviceID := range deviceIDs { |
| cdiDeviceIDs = append(cdiDeviceIDs, i.normalizeDeviceID(deviceID)) |
| } |
| |
| // We construct a device instance using the CDI device IDs and forward this |
| // to the cdiDeviceDriver. |
| return cdiDeviceDriver.updateSpec(s, &deviceInstance{ |
| req: container.DeviceRequest{ |
| Driver: dev.req.Driver, |
| DeviceIDs: cdiDeviceIDs, |
| Capabilities: dev.req.Capabilities, |
| }, |
| selectedCaps: nil, |
| }) |
| } |
| |
| // normalizeDeviceID ensures that the specified deviceID is a fully-qualified |
| // CDI device name. |
| // If the deviceID is already a fully-qualified CDI device name it is returned |
| // as-is, otherwise, the default CDI device kind (vendor/class) is used to |
| // construct a fully qualified CDI device name. |
| func (i *cdiDeviceInjector) normalizeDeviceID(deviceID string) string { |
| // if deviceID is of the form vendor.com/class=name, we return it as-is. |
| // TODO: We should ideally use the parser from the tags.cncf.io/cdi packages. |
| if _, _, ok := strings.Cut(deviceID, "="); ok { |
| return deviceID |
| } |
| |
| return i.defaultCDIDeviceKind + "=" + deviceID |
| } |