mirror of
https://github.com/moby/moby.git
synced 2026-08-09 09:33:50 +00:00
244 lines
7.4 KiB
Go
244 lines
7.4 KiB
Go
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
|
|
}
|