mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-26 11:03:34 +00:00
feat: support service.gpus and reservations.devices (#156)
* feat: support compose.yaml `service.gpus` and `service.deploy.resources.reservations.devices` DeviceRequests to Docker container * Fix lint error * Adjust test comments to work with linter * Rename DeviceRequests to DeviceReservations to be more consistent with compose nomenclature * Device reservation changes are immutable and should trigger a container recreate rather than update
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/mount"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"github.com/psviderski/uncloud/pkg/api"
|
||||
@@ -124,6 +125,9 @@ func resourcesFromCompose(service types.ServiceConfig) api.ContainerResources {
|
||||
MemoryReservation: int64(service.MemReservation),
|
||||
}
|
||||
|
||||
// Convert GPU device requests from compose format, appending "gpu" capability.
|
||||
resources.DeviceReservations = append(resources.DeviceReservations, deviceReservationsFromCompose(service.Gpus, "gpu")...)
|
||||
|
||||
// Map resources from deploy section if specified.
|
||||
if service.Deploy != nil {
|
||||
if service.Deploy.Resources.Limits != nil {
|
||||
@@ -139,12 +143,39 @@ func resourcesFromCompose(service types.ServiceConfig) api.ContainerResources {
|
||||
if service.Deploy.Resources.Reservations.MemoryBytes > 0 {
|
||||
resources.MemoryReservation = int64(service.Deploy.Resources.Reservations.MemoryBytes)
|
||||
}
|
||||
// Handle arbitrary device reservations (same structure as Gpus above).
|
||||
resources.DeviceReservations = append(resources.DeviceReservations, deviceReservationsFromCompose(service.Deploy.Resources.Reservations.Devices)...)
|
||||
}
|
||||
}
|
||||
|
||||
return resources
|
||||
}
|
||||
|
||||
// Converts compose-go DeviceRequest format to Docker API DeviceRequest format.
|
||||
// Additional capabilities can be appended via extraCapabilities (e.g., "gpu" for service.Gpus).
|
||||
func deviceReservationsFromCompose(devices []types.DeviceRequest, extraCapabilities ...string) []container.DeviceRequest {
|
||||
if devices == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
requests := make([]container.DeviceRequest, 0, len(devices))
|
||||
for _, deviceRequest := range devices {
|
||||
// Docker expects an OR'd list of AND'd capabilities (e.g. [][]string),
|
||||
// but compose-go provides a single AND'd list (e.g. []string).
|
||||
capabilities := [][]string{append(deviceRequest.Capabilities, extraCapabilities...)}
|
||||
|
||||
spec := container.DeviceRequest{
|
||||
Driver: deviceRequest.Driver,
|
||||
Count: int(deviceRequest.Count),
|
||||
DeviceIDs: deviceRequest.IDs,
|
||||
Capabilities: capabilities,
|
||||
Options: deviceRequest.Options,
|
||||
}
|
||||
requests = append(requests, spec)
|
||||
}
|
||||
return requests
|
||||
}
|
||||
|
||||
func volumeSpecsFromCompose(
|
||||
volumes types.Volumes, serviceVolumes []types.ServiceVolumeConfig,
|
||||
) ([]api.VolumeSpec, []api.VolumeMount, error) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/loader"
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/mount"
|
||||
"github.com/docker/go-units"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
@@ -434,6 +435,193 @@ services:
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceSpecFromCompose_GPUs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
composeYAML string
|
||||
expectedDeviceReqs []container.DeviceRequest
|
||||
}{
|
||||
{
|
||||
name: "gpus_all_shorthand",
|
||||
composeYAML: `
|
||||
services:
|
||||
ai:
|
||||
image: nvidia/cuda
|
||||
gpus: all
|
||||
`,
|
||||
expectedDeviceReqs: []container.DeviceRequest{
|
||||
{
|
||||
Count: -1,
|
||||
Capabilities: [][]string{{"gpu"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gpus_device_ids",
|
||||
composeYAML: `
|
||||
services:
|
||||
ai:
|
||||
image: nvidia/cuda
|
||||
gpus:
|
||||
- device_ids: ['0', '1']
|
||||
capabilities: [compute]
|
||||
`,
|
||||
expectedDeviceReqs: []container.DeviceRequest{
|
||||
{
|
||||
DeviceIDs: []string{"0", "1"},
|
||||
Capabilities: [][]string{{"compute", "gpu"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gpus_count",
|
||||
composeYAML: `
|
||||
services:
|
||||
ai:
|
||||
image: nvidia/cuda
|
||||
gpus:
|
||||
- count: 2
|
||||
capabilities: [utility]
|
||||
`,
|
||||
expectedDeviceReqs: []container.DeviceRequest{
|
||||
{
|
||||
Count: 2,
|
||||
Capabilities: [][]string{{"utility", "gpu"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gpus_driver_and_options",
|
||||
composeYAML: `
|
||||
services:
|
||||
ai:
|
||||
image: nvidia/cuda
|
||||
gpus:
|
||||
- driver: nvidia
|
||||
count: 1
|
||||
capabilities: [compute, utility]
|
||||
options:
|
||||
key1: value1
|
||||
key2: value2
|
||||
`,
|
||||
expectedDeviceReqs: []container.DeviceRequest{
|
||||
{
|
||||
Driver: "nvidia",
|
||||
Count: 1,
|
||||
Capabilities: [][]string{{"compute", "utility", "gpu"}},
|
||||
Options: map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "deploy_resources_reservations_devices",
|
||||
composeYAML: `
|
||||
services:
|
||||
ai:
|
||||
image: nvidia/cuda
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- driver: nvidia
|
||||
count: 1
|
||||
capabilities: [gpu]
|
||||
`,
|
||||
expectedDeviceReqs: []container.DeviceRequest{
|
||||
{
|
||||
Driver: "nvidia",
|
||||
Count: 1,
|
||||
Capabilities: [][]string{{"gpu"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gpus_with_existing_gpu_capability",
|
||||
composeYAML: `
|
||||
services:
|
||||
ai:
|
||||
image: nvidia/cuda
|
||||
gpus:
|
||||
- capabilities: [gpu, compute]
|
||||
`,
|
||||
expectedDeviceReqs: []container.DeviceRequest{
|
||||
{
|
||||
// defaults to "all" when count not specified
|
||||
Count: -1,
|
||||
// gpu appended even if already present (matching Docker Compose behavior)
|
||||
Capabilities: [][]string{{"gpu", "compute", "gpu"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple_device_reservations",
|
||||
composeYAML: `
|
||||
services:
|
||||
ai:
|
||||
image: nvidia/cuda
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- driver: nvidia
|
||||
count: 2
|
||||
capabilities: [gpu, compute]
|
||||
- capabilities: [tpu]
|
||||
count: 1
|
||||
`,
|
||||
expectedDeviceReqs: []container.DeviceRequest{
|
||||
{
|
||||
Driver: "nvidia",
|
||||
Count: 2,
|
||||
Capabilities: [][]string{{"gpu", "compute"}},
|
||||
},
|
||||
{
|
||||
Count: 1,
|
||||
Capabilities: [][]string{{"tpu"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "tpu_reservation",
|
||||
composeYAML: `
|
||||
services:
|
||||
ai:
|
||||
image: tensorflow/tensorflow:latest
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- capabilities: [tpu]
|
||||
count: 4
|
||||
driver: google
|
||||
`,
|
||||
expectedDeviceReqs: []container.DeviceRequest{
|
||||
{
|
||||
Driver: "google",
|
||||
Count: 4,
|
||||
Capabilities: [][]string{{"tpu"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
project, err := loadProjectFromContent(t, tt.composeYAML)
|
||||
require.NoError(t, err)
|
||||
|
||||
spec, err := ServiceSpecFromCompose(project, "ai")
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tt.expectedDeviceReqs, spec.Container.Resources.DeviceReservations,
|
||||
"DeviceReservations should match expected")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceSpecFromCompose_XMachinesPlacement(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
Reference in New Issue
Block a user