-
Notifications
You must be signed in to change notification settings - Fork 18.8k
Add DeviceRequests to HostConfig to support NVIDIA GPUs #38828
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
This patch hard-codes support for NVIDIA GPUs. In a future patch it should move out into its own Device Plugin. Signed-off-by: Tibor Vass <tibor@docker.com>
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
@@ -210,6 +210,43 @@ definitions: | |||||||||
PathInContainer: "/dev/deviceName" | ||||||||||
CgroupPermissions: "mrw" | ||||||||||
|
||||||||||
DeviceRequest: | ||||||||||
type: "object" | ||||||||||
description: "A request for devices to be sent to device drivers" | ||||||||||
properties: | ||||||||||
Driver: | ||||||||||
type: "string" | ||||||||||
tiborvass marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||
example: "nvidia" | ||||||||||
Count: | ||||||||||
type: "integer" | ||||||||||
tiborvass marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||
example: -1 | ||||||||||
DeviceIDs: | ||||||||||
type: "array" | ||||||||||
items: | ||||||||||
type: "string" | ||||||||||
example: | ||||||||||
- "0" | ||||||||||
- "1" | ||||||||||
- "GPU-fef8089b-4820-abfc-e83e-94318197576e" | ||||||||||
Capabilities: | ||||||||||
description: | | ||||||||||
A list of capabilities; an OR list of AND lists of capabilities. | ||||||||||
type: "array" | ||||||||||
items: | ||||||||||
type: "array" | ||||||||||
items: | ||||||||||
type: "string" | ||||||||||
example: | ||||||||||
# gpu AND nvidia AND compute | ||||||||||
|
# gpu AND nvidia AND compute | |
# gpu AND nvidia AND compute, OR gpu AND intel | |
- ["gpu", "nvidia", "compute"] | |
- ["gpu", "intel"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No it's fine, the reason I put it there is so that we can support it in the future without breaking the API.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we don't want to support OR
yet; we should error out if len(capabilities) > 1
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What I meant is that it is supported, but not from the CLI.
thaJeztah marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
tiborvass marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -244,6 +244,16 @@ func (n PidMode) Container() string { | |
return "" | ||
} | ||
|
||
// DeviceRequest represents a request for devices from a device driver. | ||
// Used by GPU device drivers. | ||
type DeviceRequest struct { | ||
Driver string // Name of device driver | ||
Count int // Number of devices to request (-1 = All) | ||
DeviceIDs []string // List of device IDs as recognizable by the device driver | ||
Capabilities [][]string // An OR list of AND lists of device capabilities (e.g. "gpu") | ||
|
||
Options map[string]string // Options to pass onto the device driver | ||
} | ||
|
||
// DeviceMapping represents the device mapping between the host and the container. | ||
type DeviceMapping struct { | ||
PathOnHost string | ||
|
@@ -327,6 +337,7 @@ type Resources struct { | |
CpusetMems string // CpusetMems 0-2, 0,1 | ||
Devices []DeviceMapping // List of devices to map inside the container | ||
DeviceCgroupRules []string // List of rule to be added to the device cgroup | ||
DeviceRequests []DeviceRequest // List of device requests for device drivers | ||
DiskQuota int64 // Disk limit (in bytes) | ||
KernelMemory int64 // Kernel memory limit (in bytes) | ||
KernelMemoryTCP int64 // Hard limit for kernel TCP buffer memory (in bytes) | ||
|
Original file line number | Diff line number | Diff line change | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,38 @@ | ||||||||||||||
package daemon // import "github.com/docker/docker/daemon" | ||||||||||||||
|
||||||||||||||
import ( | ||||||||||||||
"github.com/docker/docker/api/types/container" | ||||||||||||||
"github.com/docker/docker/pkg/capabilities" | ||||||||||||||
specs "github.com/opencontainers/runtime-spec/specs-go" | ||||||||||||||
) | ||||||||||||||
|
||||||||||||||
var deviceDrivers = map[string]*deviceDriver{} | ||||||||||||||
|
||||||||||||||
type deviceDriver struct { | ||||||||||||||
capset capabilities.Set | ||||||||||||||
updateSpec func(*specs.Spec, *deviceInstance) error | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
type deviceInstance struct { | ||||||||||||||
req container.DeviceRequest | ||||||||||||||
selectedCaps []string | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
func registerDeviceDriver(name string, d *deviceDriver) { | ||||||||||||||
deviceDrivers[name] = d | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
func (daemon *Daemon) handleDevice(req container.DeviceRequest, spec *specs.Spec) error { | ||||||||||||||
if req.Driver == "" { | ||||||||||||||
for _, dd := range deviceDrivers { | ||||||||||||||
if selected := dd.capset.Match(req.Capabilities); selected != nil { | ||||||||||||||
|
Request | GPU-A | GPU-B | Driver | Driver Match | GPU Match |
---|---|---|---|---|---|
"capA,capB" | "capA, capC" | "capB, capC" | "capA,capB,capC" | ✅ | ❌ |
What would happen in that case? (i.e., conversion to OCI succeeds, hook is registered, but no GPU is found)? Will a proper error be produced?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could make it an OR list of ANDs as well instead of a map.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps we should if this is a concern, so in that case the driver would report itself as;
{
"capabilities": [
["capA", "capB"],
["capB", "capC"]
]
}
Could even decide to make it just return a list of capabilities for each GPU (then we can even determine the number of GPUs available);
{
"capabilities": [
["capA", "capB"],
["capA", "capB"],
["capA", "capB"],
["capA", "capB"],
["capB", "capC"]
]
}
But perhaps that breaks the abstraction
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest we punt on the problem since the problem is extremely unlikely to happen at this time and the structure is that of the device driver so it's internal, we can change it. The API needs to be locked down.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
package daemon | ||
|
||
import ( | ||
"os/exec" | ||
"strconv" | ||
|
||
"github.com/containerd/containerd/contrib/nvidia" | ||
"github.com/docker/docker/pkg/capabilities" | ||
"github.com/opencontainers/runtime-spec/specs-go" | ||
"github.com/pkg/errors" | ||
) | ||
|
||
// 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") | ||
|
||
// stolen from github.com/containerd/containerd/contrib/nvidia | ||
const nvidiaCLI = "nvidia-container-cli" | ||
|
||
// These are NVIDIA-specific capabilities stolen from github.com/containerd/containerd/contrib/nvidia.allCaps | ||
var allNvidiaCaps = map[nvidia.Capability]struct{}{ | ||
nvidia.Compute: {}, | ||
nvidia.Compat32: {}, | ||
nvidia.Graphics: {}, | ||
nvidia.Utility: {}, | ||
nvidia.Video: {}, | ||
nvidia.Display: {}, | ||
} | ||
|
||
func init() { | ||
if _, err := exec.LookPath(nvidiaCLI); err != nil { | ||
// do not register Nvidia driver if helper binary is not present. | ||
return | ||
} | ||
capset := capabilities.Set{"gpu": struct{}{}, "nvidia": struct{}{}} | ||
nvidiaDriver := &deviceDriver{ | ||
capset: capset, | ||
updateSpec: setNvidiaGPUs, | ||
} | ||
for c := range capset { | ||
nvidiaDriver.capset[c] = struct{}{} | ||
} | ||
registerDeviceDriver("nvidia", nvidiaDriver) | ||
} | ||
|
||
func setNvidiaGPUs(s *specs.Spec, dev *deviceInstance) error { | ||
var opts []nvidia.Opts | ||
|
||
req := dev.req | ||
if req.Count != 0 && len(req.DeviceIDs) > 0 { | ||
return errConflictCountDeviceIDs | ||
} | ||
|
||
if len(req.DeviceIDs) > 0 { | ||
var ids []int | ||
var uuids []string | ||
for _, devID := range req.DeviceIDs { | ||
id, err := strconv.Atoi(devID) | ||
if err == nil { | ||
ids = append(ids, id) | ||
continue | ||
} | ||
// if not an integer, then assume UUID. | ||
uuids = append(uuids, devID) | ||
} | ||
if len(ids) > 0 { | ||
opts = append(opts, nvidia.WithDevices(ids...)) | ||
} | ||
if len(uuids) > 0 { | ||
opts = append(opts, nvidia.WithDeviceUUIDs(uuids...)) | ||
} | ||
} | ||
|
||
if req.Count < 0 { | ||
opts = append(opts, nvidia.WithAllDevices) | ||
} else if req.Count > 0 { | ||
opts = append(opts, nvidia.WithDevices(countToDevices(req.Count)...)) | ||
} | ||
|
||
var nvidiaCaps []nvidia.Capability | ||
// req.Capabilities contains device capabilities, some but not all are NVIDIA driver capabilities. | ||
for _, c := range dev.selectedCaps { | ||
nvcap := nvidia.Capability(c) | ||
if _, isNvidiaCap := allNvidiaCaps[nvcap]; isNvidiaCap { | ||
nvidiaCaps = append(nvidiaCaps, nvcap) | ||
continue | ||
} | ||
// TODO: nvidia.WithRequiredCUDAVersion | ||
// for now we let the prestart hook verify cuda versions but errors are not pretty. | ||
} | ||
|
||
if nvidiaCaps != nil { | ||
opts = append(opts, nvidia.WithCapabilities(nvidiaCaps...)) | ||
} | ||
|
||
return nvidia.WithGPUs(opts...)(nil, nil, nil, s) | ||
} | ||
|
||
// countToDevices returns the list 0, 1, ... count-1 of deviceIDs. | ||
func countToDevices(count int) []int { | ||
tiborvass marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
devices := make([]int, count) | ||
for i := range devices { | ||
devices[i] = i | ||
} | ||
return devices | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
// Package capabilities allows to generically handle capabilities. | ||
package capabilities | ||
|
||
// Set represents a set of capabilities. | ||
type Set map[string]struct{} | ||
|
||
// Match tries to match set with caps, which is an OR list of AND lists of capabilities. | ||
// The matched AND list of capabilities is returned; or nil if none are matched. | ||
func (set Set) Match(caps [][]string) []string { | ||
tiborvass marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
if set == nil { | ||
thaJeztah marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
return nil | ||
} | ||
anyof: | ||
for _, andList := range caps { | ||
for _, cap := range andList { | ||
if _, ok := set[cap]; !ok { | ||
continue anyof | ||
} | ||
} | ||
return andList | ||
} | ||
return nil | ||
} |
Uh oh!
There was an error while loading. Please reload this page.