Repository: compose-spec/compose-go
Pull request #693 (2024-10-04 13:43:49)

introduce service.gpus
Align with `docker run --gpus` to allocate a GPU by device request
implementation will have to manage reconciliation with deploy.resources.reservations.device


Patch:
diff --git a/schema/compose-spec.json b/schema/compose-spec.json
index 95326f3..8fdda4a 100644
--- a/schema/compose-spec.json
+++ b/schema/compose-spec.json
@@ -267,6 +267,7 @@
         },
         "external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
         "extra_hosts": {"$ref": "#/definitions/extra_hosts"},
+        "gpus": {"$ref": "#/definitions/gpus"},
         "group_add": {
           "type": "array",
           "items": {
@@ -651,6 +652,23 @@
       }
     },
 
+    "gpus": {
+      "id": "#/definitions/gpus",
+      "type": "array",
+      "items": {
+        "type": "object",
+        "properties": {
+          "capabilities": {"$ref": "#/definitions/list_of_strings"},
+          "count": {"type": ["string", "integer"]},
+          "device_ids": {"$ref": "#/definitions/list_of_strings"},
+          "driver":{"type": "string"},
+          "options":{"$ref": "#/definitions/list_or_dict"}
+        },
+        "additionalProperties": false,
+        "patternProperties": {"^x-": {}}
+      }
+    },
+
     "include": {
       "id": "#/definitions/include",
       "oneOf": [
diff --git a/transform/canonical.go b/transform/canonical.go
index fa844e7..a248b4b 100644
--- a/transform/canonical.go
+++ b/transform/canonical.go
@@ -29,7 +29,6 @@ func init() {
 	transformers["services.*.build.secrets.*"] = transformFileMount
 	transformers["services.*.build.additional_contexts"] = transformKeyValue
 	transformers["services.*.depends_on"] = transformDependsOn
-	transformers["services.*.deploy.resources.reservations.devices.*"] = transformDeviceRequest
 	transformers["services.*.env_file"] = transformEnvFile
 	transformers["services.*.extends"] = transformExtends
 	transformers["services.*.networks"] = transformServiceNetworks
diff --git a/transform/defaults.go b/transform/defaults.go
index 96693c6..041a689 100644
--- a/transform/defaults.go
+++ b/transform/defaults.go
@@ -26,6 +26,8 @@ func init() {
 	defaultValues["services.*.build"] = defaultBuildContext
 	defaultValues["services.*.secrets.*"] = defaultSecretMount
 	defaultValues["services.*.ports.*"] = portDefaults
+	defaultValues["services.*.deploy.resources.reservations.devices.*"] = deviceRequestDefaults
+	defaultValues["services.*.gpus.*"] = deviceRequestDefaults
 }
 
 // SetDefaultValues transforms a compose model to set default values to missing attributes
diff --git a/transform/devices.go b/transform/devices.go
index 558d47e..3ce7fa0 100644
--- a/transform/devices.go
+++ b/transform/devices.go
@@ -22,19 +22,15 @@ import (
 	"github.com/compose-spec/compose-go/v2/tree"
 )
 
-func transformDeviceRequest(data any, p tree.Path, ignoreParseError bool) (any, error) {
-	switch v := data.(type) {
-	case map[string]any:
-		_, hasCount := v["count"]
-		_, hasIds := v["device_ids"]
-		if hasCount && hasIds {
-			return nil, fmt.Errorf(`%s: "count" and "device_ids" attributes are exclusive`, p)
-		}
-		if !hasCount && !hasIds {
-			v["count"] = "all"
-		}
-		return transformMapping(v, p, ignoreParseError)
-	default:
+func deviceRequestDefaults(data any, p tree.Path, _ bool) (any, error) {
+	v, ok := data.(map[string]any)
+	if !ok {
 		return data, fmt.Errorf("%s: invalid type %T for device request", p, v)
 	}
+	_, hasCount := v["count"]
+	_, hasIds := v["device_ids"]
+	if !hasCount && !hasIds {
+		v["count"] = "all"
+	}
+	return v, nil
 }
diff --git a/types/derived.gen.go b/types/derived.gen.go
index bc5b5d9..31323d0 100644
--- a/types/derived.gen.go
+++ b/types/derived.gen.go
@@ -445,12 +445,30 @@ func deriveDeepCopyService(dst, src *ServiceConfig) {
 		}
 		copy(dst.GroupAdd, src.GroupAdd)
 	}
+	if src.Gpus == nil {
+		dst.Gpus = nil
+	} else {
+		if dst.Gpus != nil {
+			if len(src.Gpus) > len(dst.Gpus) {
+				if cap(dst.Gpus) >= len(src.Gpus) {
+					dst.Gpus = (dst.Gpus)[:len(src.Gpus)]
+				} else {
+					dst.Gpus = make([]DeviceRequest, len(src.Gpus))
+				}
+			} else if len(src.Gpus) < len(dst.Gpus) {
+				dst.Gpus = (dst.Gpus)[:len(src.Gpus)]
+			}
+		} else {
+			dst.Gpus = make([]DeviceRequest, len(src.Gpus))
+		}
+		deriveDeepCopy_15(dst.Gpus, src.Gpus)
+	}
 	dst.Hostname = src.Hostname
 	if src.HealthCheck == nil {
 		dst.HealthCheck = nil
 	} else {
 		dst.HealthCheck = new(HealthCheckConfig)
-		deriveDeepCopy_15(dst.HealthCheck, src.HealthCheck)
+		deriveDeepCopy_16(dst.HealthCheck, src.HealthCheck)
 	}
 	dst.Image = src.Image
 	if src.Init == nil {
@@ -495,7 +513,7 @@ func deriveDeepCopyService(dst, src *ServiceConfig) {
 		dst.Logging = nil
 	} else {
 		dst.Logging = new(LoggingConfig)
-		deriveDeepCopy_16(dst.Logging, src.Logging)
+		deriveDeepCopy_17(dst.Logging, src.Logging)
 	}
 	dst.LogDriver = src.LogDriver
 	if src.LogOpt != nil {
@@ -513,7 +531,7 @@ func deriveDeepCopyService(dst, src *ServiceConfig) {
 	dst.NetworkMode = src.NetworkMode
 	if src.Networks != nil {
 		dst.Networks = make(map[string]*ServiceNetworkConfig, len(src.Networks))
-		deriveDeepCopy_17(dst.Networks, src.Networks)
+		deriveDeepCopy_18(dst.Networks, src.Networks)
 	} else {
 		dst.Networks = nil
 	}
@@ -538,7 +556,7 @@ func deriveDeepCopyService(dst, src *ServiceConfig) {
 		} else {
 			dst.Ports = make([]ServicePortConfig, len(src.Ports))
 		}
-		deriveDeepCopy_18(dst.Ports, src.Ports)
+		deriveDeepCopy_19(dst.Ports, src.Ports)
 	}
 	dst.Privileged = src.Privileged
 	dst.PullPolicy = src.PullPolicy
@@ -567,7 +585,7 @@ func deriveDeepCopyService(dst, src *ServiceConfig) {
 		} else {
 			dst.Secrets = make([]ServiceSecretConfig, len(src.Secrets))
 		}
-		deriveDeepCopy_19(dst.Secrets, src.Secrets)
+		deriveDeepCopy_20(dst.Secrets, src.Secrets)
 	}
 	if src.SecurityOpt == nil {
 		dst.SecurityOpt = nil
@@ -629,7 +647,7 @@ func deriveDeepCopyService(dst, src *ServiceConfig) {
 	dst.Tty = src.Tty
 	if src.Ulimits != nil {
 		dst.Ulimits = make(map[string]*UlimitsConfig, len(src.Ulimits))
-		deriveDeepCopy_20(dst.Ulimits, src.Ulimits)
+		deriveDeepCopy_21(dst.Ulimits, src.Ulimits)
 	} else {
 		dst.Ulimits = nil
 	}
@@ -653,7 +671,7 @@ func deriveDeepCopyService(dst, src *ServiceConfig) {
 		} else {
 			dst.Volumes = make([]ServiceVolumeConfig, len(src.Volumes))
 		}
-		deriveDeepCopy_21(dst.Volumes, src.Volumes)
+		deriveDeepCopy_22(dst.Volumes, src.Volumes)
 	}
 	if src.VolumesFrom == nil {
 		dst.VolumesFrom = nil
@@ -690,7 +708,7 @@ func deriveDeepCopyService(dst, src *ServiceConfig) {
 		} else {
 			dst.PostStart = make([]ServiceHook, len(src.PostStart))
 		}
-		deriveDeepCopy_22(dst.PostStart, src.PostStart)
+		deriveDeepCopy_23(dst.PostStart, src.PostStart)
 	}
 	if src.PreStop == nil {
 		dst.PreStop = nil
@@ -708,7 +726,7 @@ func deriveDeepCopyService(dst, src *ServiceConfig) {
 		} else {
 			dst.PreStop = make([]ServiceHook, len(src.PreStop))
 		}
-		deriveDeepCopy_22(dst.PreStop, src.PreStop)
+		deriveDeepCopy_23(dst.PreStop, src.PreStop)
 	}
 	if src.Extensions != nil {
 		dst.Extensions = make(map[string]any, len(src.Extensions))
@@ -734,7 +752,7 @@ func deriveDeepCopy_(dst, src map[string]NetworkConfig) {
 	for src_key, src_value := range src {
 		func() {
 			field := new(NetworkConfig)
-			deriveDeepCopy_23(field, &src_value)
+			deriveDeepCopy_24(field, &src_value)
 			dst[src_key] = *field
 		}()
 	}
@@ -745,7 +763,7 @@ func deriveDeepCopy_1(dst, src map[string]VolumeConfig) {
 	for src_key, src_value := range src {
 		func() {
 			field := new(VolumeConfig)
-			deriveDeepCopy_24(field, &src_value)
+			deriveDeepCopy_25(field, &src_value)
 			dst[src_key] = *field
 		}()
 	}
@@ -756,7 +774,7 @@ func deriveDeepCopy_2(dst, src map[string]SecretConfig) {
 	for src_key, src_value := range src {
 		func() {
 			field := new(SecretConfig)
-			deriveDeepCopy_25(field, &src_value)
+			deriveDeepCopy_26(field, &src_value)
 			dst[src_key] = *field
 		}()
 	}
@@ -767,7 +785,7 @@ func deriveDeepCopy_3(dst, src map[string]ConfigObjConfig) {
 	for src_key, src_value := range src {
 		func() {
 			field := new(ConfigObjConfig)
-			deriveDeepCopy_26(field, &src_value)
+			deriveDeepCopy_27(field, &src_value)
 			dst[src_key] = *field
 		}()
 	}
@@ -902,7 +920,7 @@ func deriveDeepCopy_5(dst, src *BuildConfig) {
 		} else {
 			dst.Secrets = make([]ServiceSecretConfig, len(src.Secrets))
 		}
-		deriveDeepCopy_19(dst.Secrets, src.Secrets)
+		deriveDeepCopy_20(dst.Secrets, src.Secrets)
 	}
 	dst.ShmSize = src.ShmSize
 	if src.Tags == nil {
@@ -925,7 +943,7 @@ func deriveDeepCopy_5(dst, src *BuildConfig) {
 	}
 	if src.Ulimits != nil {
 		dst.Ulimits = make(map[string]*UlimitsConfig, len(src.Ulimits))
-		deriveDeepCopy_20(dst.Ulimits, src.Ulimits)
+		deriveDeepCopy_21(dst.Ulimits, src.Ulimits)
 	} else {
 		dst.Ulimits = nil
 	}
@@ -974,7 +992,7 @@ func deriveDeepCopy_6(dst, src *DevelopConfig) {
 		} else {
 			dst.Watch = make([]Trigger, len(src.Watch))
 		}
-		deriveDeepCopy_27(dst.Watch, src.Watch)
+		deriveDeepCopy_28(dst.Watch, src.Watch)
 	}
 	if src.Extensions != nil {
 		dst.Extensions = make(map[string]any, len(src.Extensions))
@@ -1003,7 +1021,7 @@ func deriveDeepCopy_7(dst, src *BlkioConfig) {
 		} else {
 			dst.WeightDevice = make([]WeightDevice, len(src.WeightDevice))
 		}
-		deriveDeepCopy_28(dst.WeightDevice, src.WeightDevice)
+		deriveDeepCopy_29(dst.WeightDevice, src.WeightDevice)
 	}
 	if src.DeviceReadBps == nil {
 		dst.DeviceReadBps = nil
@@ -1021,7 +1039,7 @@ func deriveDeepCopy_7(dst, src *BlkioConfig) {
 		} else {
 			dst.DeviceReadBps = make([]ThrottleDevice, len(src.DeviceReadBps))
 		}
-		deriveDeepCopy_29(dst.DeviceReadBps, src.DeviceReadBps)
+		deriveDeepCopy_30(dst.DeviceReadBps, src.DeviceReadBps)
 	}
 	if src.DeviceReadIOps == nil {
 		dst.DeviceReadIOps = nil
@@ -1039,7 +1057,7 @@ func deriveDeepCopy_7(dst, src *BlkioConfig) {
 		} else {
 			dst.DeviceReadIOps = make([]ThrottleDevice, len(src.DeviceReadIOps))
 		}
-		deriveDeepCopy_29(dst.DeviceReadIOps, src.DeviceReadIOps)
+		deriveDeepCopy_30(dst.DeviceReadIOps, src.DeviceReadIOps)
 	}
 	if src.DeviceWriteBps == nil {
 		dst.DeviceWriteBps = nil
@@ -1057,7 +1075,7 @@ func deriveDeepCopy_7(dst, src *BlkioConfig) {
 		} else {
 			dst.DeviceWriteBps = make([]ThrottleDevice, len(src.DeviceWriteBps))
 		}
-		deriveDeepCopy_29(dst.DeviceWriteBps, src.DeviceWriteBps)
+		deriveDeepCopy_30(dst.DeviceWriteBps, src.DeviceWriteBps)
 	}
 	if src.DeviceWriteIOps == nil {
 		dst.DeviceWriteIOps = nil
@@ -1075,7 +1093,7 @@ func deriveDeepCopy_7(dst, src *BlkioConfig) {
 		} else {
 			dst.DeviceWriteIOps = make([]ThrottleDevice, len(src.DeviceWriteIOps))
 		}
-		deriveDeepCopy_29(dst.DeviceWriteIOps, src.DeviceWriteIOps)
+		deriveDeepCopy_30(dst.DeviceWriteIOps, src.DeviceWriteIOps)
 	}
 	if src.Extensions != nil {
 		dst.Extensions = make(map[string]any, len(src.Extensions))
@@ -1090,7 +1108,7 @@ func deriveDeepCopy_8(dst, src []ServiceConfigObjConfig) {
 	for src_i, src_value := range src {
 		func() {
 			field := new(ServiceConfigObjConfig)
-			deriveDeepCopy_30(field, &src_value)
+			deriveDeepCopy_31(field, &src_value)
 			dst[src_i] = *field
 		}()
 	}
@@ -1114,7 +1132,7 @@ func deriveDeepCopy_10(dst, src map[string]ServiceDependency) {
 	for src_key, src_value := range src {
 		func() {
 			field := new(ServiceDependency)
-			deriveDeepCopy_31(field, &src_value)
+			deriveDeepCopy_32(field, &src_value)
 			dst[src_key] = *field
 		}()
 	}
@@ -1139,28 +1157,28 @@ func deriveDeepCopy_11(dst, src *DeployConfig) {
 		dst.UpdateConfig = nil
 	} else {
 		dst.UpdateConfig = new(UpdateConfig)
-		deriveDeepCopy_32(dst.UpdateConfig, src.UpdateConfig)
+		deriveDeepCopy_33(dst.UpdateConfig, src.UpdateConfig)
 	}
 	if src.RollbackConfig == nil {
 		dst.RollbackConfig = nil
 	} else {
 		dst.RollbackConfig = new(UpdateConfig)
-		deriveDeepCopy_32(dst.RollbackConfig, src.RollbackConfig)
+		deriveDeepCopy_33(dst.RollbackConfig, src.RollbackConfig)
 	}
 	func() {
 		field := new(Resources)
-		deriveDeepCopy_33(field, &src.Resources)
+		deriveDeepCopy_34(field, &src.Resources)
 		dst.Resources = *field
 	}()
 	if src.RestartPolicy == nil {
 		dst.RestartPolicy = nil
 	} else {
 		dst.RestartPolicy = new(RestartPolicy)
-		deriveDeepCopy_34(dst.RestartPolicy, src.RestartPolicy)
+		deriveDeepCopy_35(dst.RestartPolicy, src.RestartPolicy)
 	}
 	func() {
 		field := new(Placement)
-		deriveDeepCopy_35(field, &src.Placement)
+		deriveDeepCopy_36(field, &src.Placement)
 		dst.Placement = *field
 	}()
 	dst.EndpointMode = src.EndpointMode
@@ -1177,7 +1195,7 @@ func deriveDeepCopy_12(dst, src []DeviceMapping) {
 	for src_i, src_value := range src {
 		func() {
 			field := new(DeviceMapping)
-			deriveDeepCopy_36(field, &src_value)
+			deriveDeepCopy_37(field, &src_value)
 			dst[src_i] = *field
 		}()
 	}
@@ -1226,7 +1244,18 @@ func deriveDeepCopy_14(dst, src map[string][]string) {
 }
 
 // deriveDeepCopy_15 recursively copies the contents of src into dst.
-func deriveDeepCopy_15(dst, src *HealthCheckConfig) {
+func deriveDeepCopy_15(dst, src []DeviceRequest) {
+	for src_i, src_value := range src {
+		func() {
+			field := new(DeviceRequest)
+			deriveDeepCopy_38(field, &src_value)
+			dst[src_i] = *field
+		}()
+	}
+}
+
+// deriveDeepCopy_16 recursively copies the contents of src into dst.
+func deriveDeepCopy_16(dst, src *HealthCheckConfig) {
 	if src.Test == nil {
 		dst.Test = nil
 	} else {
@@ -1284,8 +1313,8 @@ func deriveDeepCopy_15(dst, src *HealthCheckConfig) {
 	}
 }
 
-// deriveDeepCopy_16 recursively copies the contents of src into dst.
-func deriveDeepCopy_16(dst, src *LoggingConfig) {
+// deriveDeepCopy_17 recursively copies the contents of src into dst.
+func deriveDeepCopy_17(dst, src *LoggingConfig) {
 	dst.Driver = src.Driver
 	if src.Options != nil {
 		dst.Options = make(map[string]string, len(src.Options))
@@ -1301,8 +1330,8 @@ func deriveDeepCopy_16(dst, src *LoggingConfig) {
 	}
 }
 
-// deriveDeepCopy_17 recursively copies the contents of src into dst.
-func deriveDeepCopy_17(dst, src map[string]*ServiceNetworkConfig) {
+// deriveDeepCopy_18 recursively copies the contents of src into dst.
+func deriveDeepCopy_18(dst, src map[string]*ServiceNetworkConfig) {
 	for src_key, src_value := range src {
 		if src_value == nil {
 			dst[src_key] = nil
@@ -1311,35 +1340,35 @@ func deriveDeepCopy_17(dst, src map[string]*ServiceNetworkConfig) {
 			dst[src_key] = nil
 		} else {
 			dst[src_key] = new(ServiceNetworkConfig)
-			deriveDeepCopy_37(dst[src_key], src_value)
+			deriveDeepCopy_39(dst[src_key], src_value)
 		}
 	}
 }
 
-// deriveDeepCopy_18 recursively copies the contents of src into dst.
-func deriveDeepCopy_18(dst, src []ServicePortConfig) {
+// deriveDeepCopy_19 recursively copies the contents of src into dst.
+func deriveDeepCopy_19(dst, src []ServicePortConfig) {
 	for src_i, src_value := range src {
 		func() {
 			field := new(ServicePortConfig)
-			deriveDeepCopy_38(field, &src_value)
+			deriveDeepCopy_40(field, &src_value)
 			dst[src_i] = *field
 		}()
 	}
 }
 
-// deriveDeepCopy_19 recursively copies the contents of src into dst.
-func deriveDeepCopy_19(dst, src []ServiceSecretConfig) {
+// deriveDeepCopy_20 recursively copies the contents of src into dst.
+func deriveDeepCopy_20(dst, src []ServiceSecretConfig) {
 	for src_i, src_value := range src {
 		func() {
 			field := new(ServiceSecretConfig)
-			deriveDeepCopy_39(field, &src_value)
+			deriveDeepCopy_41(field, &src_value)
 			dst[src_i] = *field
 		}()
 	}
 }
 
-// deriveDeepCopy_20 recursively copies the contents of src into dst.
-func deriveDeepCopy_20(dst, src map[string]*UlimitsConfig) {
+// deriveDeepCopy_21 recursively copies the contents of src into dst.
+func deriveDeepCopy_21(dst, src map[string]*UlimitsConfig) {
 	for src_key, src_value := range src {
 		if src_value == nil {
 			dst[src_key] = nil
@@ -1348,35 +1377,35 @@ func deriveDeepCopy_20(dst, src map[string]*UlimitsConfig) {
 			dst[src_key] = nil
 		} else {
 			dst[src_key] = new(UlimitsConfig)
-			deriveDeepCopy_40(dst[src_key], src_value)
+			deriveDeepCopy_42(dst[src_key], src_value)
 		}
 	}
 }
 
-// deriveDeepCopy_21 recursively copies the contents of src into dst.
-func deriveDeepCopy_21(dst, src []ServiceVolumeConfig) {
+// deriveDeepCopy_22 recursively copies the contents of src into dst.
+func deriveDeepCopy_22(dst, src []ServiceVolumeConfig) {
 	for src_i, src_value := range src {
 		func() {
 			field := new(ServiceVolumeConfig)
-			deriveDeepCopy_41(field, &src_value)
+			deriveDeepCopy_43(field, &src_value)
 			dst[src_i] = *field
 		}()
 	}
 }
 
-// deriveDeepCopy_22 recursively copies the contents of src into dst.
-func deriveDeepCopy_22(dst, src []ServiceHook) {
+// deriveDeepCopy_23 recursively copies the contents of src into dst.
+func deriveDeepCopy_23(dst, src []ServiceHook) {
 	for src_i, src_value := range src {
 		func() {
 			field := new(ServiceHook)
-			deriveDeepCopy_42(field, &src_value)
+			deriveDeepCopy_44(field, &src_value)
 			dst[src_i] = *field
 		}()
 	}
 }
 
-// deriveDeepCopy_23 recursively copies the contents of src into dst.
-func deriveDeepCopy_23(dst, src *NetworkConfig) {
+// deriveDeepCopy_24 recursively copies the contents of src into dst.
+func deriveDeepCopy_24(dst, src *NetworkConfig) {
 	dst.Name = src.Name
 	dst.Driver = src.Driver
 	if src.DriverOpts != nil {
@@ -1387,7 +1416,7 @@ func deriveDeepCopy_23(dst, src *NetworkConfig) {
 	}
 	func() {
 		field := new(IPAMConfig)
-		deriveDeepCopy_43(field, &src.Ipam)
+		deriveDeepCopy_45(field, &src.Ipam)
 		dst.Ipam = *field
 	}()
 	dst.External = src.External
@@ -1413,8 +1442,8 @@ func deriveDeepCopy_23(dst, src *NetworkConfig) {
 	}
 }
 
-// deriveDeepCopy_24 recursively copies the contents of src into dst.
-func deriveDeepCopy_24(dst, src *VolumeConfig) {
+// deriveDeepCopy_25 recursively copies the contents of src into dst.
+func deriveDeepCopy_25(dst, src *VolumeConfig) {
 	dst.Name = src.Name
 	dst.Driver = src.Driver
 	if src.DriverOpts != nil {
@@ -1438,8 +1467,8 @@ func deriveDeepCopy_24(dst, src *VolumeConfig) {
 	}
 }
 
-// deriveDeepCopy_25 recursively copies the contents of src into dst.
-func deriveDeepCopy_25(dst, src *SecretConfig) {
+// deriveDeepCopy_26 recursively copies the contents of src into dst.
+func deriveDeepCopy_26(dst, src *SecretConfig) {
 	dst.Name = src.Name
 	dst.File = src.File
 	dst.Environment = src.Environment
@@ -1467,8 +1496,8 @@ func deriveDeepCopy_25(dst, src *SecretConfig) {
 	}
 }
 
-// deriveDeepCopy_26 recursively copies the contents of src into dst.
-func deriveDeepCopy_26(dst, src *ConfigObjConfig) {
+// deriveDeepCopy_27 recursively copies the contents of src into dst.
+func deriveDeepCopy_27(dst, src *ConfigObjConfig) {
 	dst.Name = src.Name
 	dst.File = src.File
 	dst.Environment = src.Environment
@@ -1496,41 +1525,41 @@ func deriveDeepCopy_26(dst, src *ConfigObjConfig) {
 	}
 }
 
-// deriveDeepCopy_27 recursively copies the contents of src into dst.
-func deriveDeepCopy_27(dst, src []Trigger) {
+// deriveDeepCopy_28 recursively copies the contents of src into dst.
+func deriveDeepCopy_28(dst, src []Trigger) {
 	for src_i, src_value := range src {
 		func() {
 			field := new(Trigger)
-			deriveDeepCopy_44(field, &src_value)
+			deriveDeepCopy_46(field, &src_value)
 			dst[src_i] = *field
 		}()
 	}
 }
 
-// deriveDeepCopy_28 recursively copies the contents of src into dst.
-func deriveDeepCopy_28(dst, src []WeightDevice) {
+// deriveDeepCopy_29 recursively copies the contents of src into dst.
+func deriveDeepCopy_29(dst, src []WeightDevice) {
 	for src_i, src_value := range src {
 		func() {
 			field := new(WeightDevice)
-			deriveDeepCopy_45(field, &src_value)
+			deriveDeepCopy_47(field, &src_value)
 			dst[src_i] = *field
 		}()
 	}
 }
 
-// deriveDeepCopy_29 recursively copies the contents of src into dst.
-func deriveDeepCopy_29(dst, src []ThrottleDevice) {
+// deriveDeepCopy_30 recursively copies the contents of src into dst.
+func deriveDeepCopy_30(dst, src []ThrottleDevice) {
 	for src_i, src_value := range src {
 		func() {
 			field := new(ThrottleDevice)
-			deriveDeepCopy_46(field, &src_value)
+			deriveDeepCopy_48(field, &src_value)
 			dst[src_i] = *field
 		}()
 	}
 }
 
-// deriveDeepCopy_30 recursively copies the contents of src into dst.
-func deriveDeepCopy_30(dst, src *ServiceConfigObjConfig) {
+// deriveDeepCopy_31 recursively copies the contents of src into dst.
+func deriveDeepCopy_31(dst, src *ServiceConfigObjConfig) {
 	dst.Source = src.Source
 	dst.Target = src.Target
 	dst.UID = src.UID
@@ -1549,8 +1578,8 @@ func deriveDeepCopy_30(dst, src *ServiceConfigObjConfig) {
 	}
 }
 
-// deriveDeepCopy_31 recursively copies the contents of src into dst.
-func deriveDeepCopy_31(dst, src *ServiceDependency) {
+// deriveDeepCopy_32 recursively copies the contents of src into dst.
+func deriveDeepCopy_32(dst, src *ServiceDependency) {
 	dst.Condition = src.Condition
 	dst.Restart = src.Restart
 	if src.Extensions != nil {
@@ -1562,8 +1591,8 @@ func deriveDeepCopy_31(dst, src *ServiceDependency) {
 	dst.Required = src.Required
 }
 
-// deriveDeepCopy_32 recursively copies the contents of src into dst.
-func deriveDeepCopy_32(dst, src *UpdateConfig) {
+// deriveDeepCopy_33 recursively copies the contents of src into dst.
+func deriveDeepCopy_33(dst, src *UpdateConfig) {
 	if src.Parallelism == nil {
 		dst.Parallelism = nil
 	} else {
@@ -1583,19 +1612,19 @@ func deriveDeepCopy_32(dst, src *UpdateConfig) {
 	}
 }
 
-// deriveDeepCopy_33 recursively copies the contents of src into dst.
-func deriveDeepCopy_33(dst, src *Resources) {
+// deriveDeepCopy_34 recursively copies the contents of src into dst.
+func deriveDeepCopy_34(dst, src *Resources) {
 	if src.Limits == nil {
 		dst.Limits = nil
 	} else {
 		dst.Limits = new(Resource)
-		deriveDeepCopy_47(dst.Limits, src.Limits)
+		deriveDeepCopy_49(dst.Limits, src.Limits)
 	}
 	if src.Reservations == nil {
 		dst.Reservations = nil
 	} else {
 		dst.Reservations = new(Resource)
-		deriveDeepCopy_47(dst.Reservations, src.Reservations)
+		deriveDeepCopy_49(dst.Reservations, src.Reservations)
 	}
 	if src.Extensions != nil {
 		dst.Extensions = make(map[string]any, len(src.Extensions))
@@ -1605,8 +1634,8 @@ func deriveDeepCopy_33(dst, src *Resources) {
 	}
 }
 
-// deriveDeepCopy_34 recursively copies the contents of src into dst.
-func deriveDeepCopy_34(dst, src *RestartPolicy) {
+// deriveDeepCopy_35 recursively copies the contents of src into dst.
+func deriveDeepCopy_35(dst, src *RestartPolicy) {
 	dst.Condition = src.Condition
 	if src.Delay == nil {
 		dst.Delay = nil
@@ -1634,8 +1663,8 @@ func deriveDeepCopy_34(dst, src *RestartPolicy) {
 	}
 }
 
-// deriveDeepCopy_35 recursively copies the contents of src into dst.
-func deriveDeepCopy_35(dst, src *Placement) {
+// deriveDeepCopy_36 recursively copies the contents of src into dst.
+func deriveDeepCopy_36(dst, src *Placement) {
 	if src.Constraints == nil {
 		dst.Constraints = nil
 	} else {
@@ -1670,7 +1699,7 @@ func deriveDeepCopy_35(dst, src *Placement) {
 		} else {
 			dst.Preferences = make([]PlacementPreferences, len(src.Preferences))
 		}
-		deriveDeepCopy_48(dst.Preferences, src.Preferences)
+		deriveDeepCopy_50(dst.Preferences, src.Preferences)
 	}
 	dst.MaxReplicas = src.MaxReplicas
 	if src.Extensions != nil {
@@ -1681,8 +1710,8 @@ func deriveDeepCopy_35(dst, src *Placement) {
 	}
 }
 
-// deriveDeepCopy_36 recursively copies the contents of src into dst.
-func deriveDeepCopy_36(dst, src *DeviceMapping) {
+// deriveDeepCopy_37 recursively copies the contents of src into dst.
+func deriveDeepCopy_37(dst, src *DeviceMapping) {
 	dst.Source = src.Source
 	dst.Target = src.Target
 	dst.Permissions = src.Permissions
@@ -1694,8 +1723,56 @@ func deriveDeepCopy_36(dst, src *DeviceMapping) {
 	}
 }
 
-// deriveDeepCopy_37 recursively copies the contents of src into dst.
-func deriveDeepCopy_37(dst, src *ServiceNetworkConfig) {
+// deriveDeepCopy_38 recursively copies the contents of src into dst.
+func deriveDeepCopy_38(dst, src *DeviceRequest) {
+	if src.Capabilities == nil {
+		dst.Capabilities = nil
+	} else {
+		if dst.Capabilities != nil {
+			if len(src.Capabilities) > len(dst.Capabilities) {
+				if cap(dst.Capabilities) >= len(src.Capabilities) {
+					dst.Capabilities = (dst.Capabilities)[:len(src.Capabilities)]
+				} else {
+					dst.Capabilities = make([]string, len(src.Capabilities))
+				}
+			} else if len(src.Capabilities) < len(dst.Capabilities) {
+				dst.Capabilities = (dst.Capabilities)[:len(src.Capabilities)]
+			}
+		} else {
+			dst.Capabilities = make([]string, len(src.Capabilities))
+		}
+		copy(dst.Capabilities, src.Capabilities)
+	}
+	dst.Driver = src.Driver
+	dst.Count = src.Count
+	if src.IDs == nil {
+		dst.IDs = nil
+	} else {
+		if dst.IDs != nil {
+			if len(src.IDs) > len(dst.IDs) {
+				if cap(dst.IDs) >= len(src.IDs) {
+					dst.IDs = (dst.IDs)[:len(src.IDs)]
+				} else {
+					dst.IDs = make([]string, len(src.IDs))
+				}
+			} else if len(src.IDs) < len(dst.IDs) {
+				dst.IDs = (dst.IDs)[:len(src.IDs)]
+			}
+		} else {
+			dst.IDs = make([]string, len(src.IDs))
+		}
+		copy(dst.IDs, src.IDs)
+	}
+	if src.Options != nil {
+		dst.Options = make(map[string]string, len(src.Options))
+		deriveDeepCopy_4(dst.Options, src.Options)
+	} else {
+		dst.Options = nil
+	}
+}
+
+// deriveDeepCopy_39 recursively copies the contents of src into dst.
+func deriveDeepCopy_39(dst, src *ServiceNetworkConfig) {
 	dst.Priority = src.Priority
 	if src.Aliases == nil {
 		dst.Aliases = nil
@@ -1750,8 +1827,8 @@ func deriveDeepCopy_37(dst, src *ServiceNetworkConfig) {
 	}
 }
 
-// deriveDeepCopy_38 recursively copies the contents of src into dst.
-func deriveDeepCopy_38(dst, src *ServicePortConfig) {
+// deriveDeepCopy_40 recursively copies the contents of src into dst.
+func deriveDeepCopy_40(dst, src *ServicePortConfig) {
 	dst.Name = src.Name
 	dst.Mode = src.Mode
 	dst.HostIP = src.HostIP
@@ -1767,8 +1844,8 @@ func deriveDeepCopy_38(dst, src *ServicePortConfig) {
 	}
 }
 
-// deriveDeepCopy_39 recursively copies the contents of src into dst.
-func deriveDeepCopy_39(dst, src *ServiceSecretConfig) {
+// deriveDeepCopy_41 recursively copies the contents of src into dst.
+func deriveDeepCopy_41(dst, src *ServiceSecretConfig) {
 	dst.Source = src.Source
 	dst.Target = src.Target
 	dst.UID = src.UID
@@ -1787,8 +1864,8 @@ func deriveDeepCopy_39(dst, src *ServiceSecretConfig) {
 	}
 }
 
-// deriveDeepCopy_40 recursively copies the contents of src into dst.
-func deriveDeepCopy_40(dst, src *UlimitsConfig) {
+// deriveDeepCopy_42 recursively copies the contents of src into dst.
+func deriveDeepCopy_42(dst, src *UlimitsConfig) {
 	dst.Single = src.Single
 	dst.Soft = src.Soft
 	dst.Hard = src.Hard
@@ -1800,8 +1877,8 @@ func deriveDeepCopy_40(dst, src *UlimitsConfig) {
 	}
 }
 
-// deriveDeepCopy_41 recursively copies the contents of src into dst.
-func deriveDeepCopy_41(dst, src *ServiceVolumeConfig) {
+// deriveDeepCopy_43 recursively copies the contents of src into dst.
+func deriveDeepCopy_43(dst, src *ServiceVolumeConfig) {
 	dst.Type = src.Type
 	dst.Source = src.Source
 	dst.Target = src.Target
@@ -1811,19 +1888,19 @@ func deriveDeepCopy_41(dst, src *ServiceVolumeConfig) {
 		dst.Bind = nil
 	} else {
 		dst.Bind = new(ServiceVolumeBind)
-		deriveDeepCopy_49(dst.Bind, src.Bind)
+		deriveDeepCopy_51(dst.Bind, src.Bind)
 	}
 	if src.Volume == nil {
 		dst.Volume = nil
 	} else {
 		dst.Volume = new(ServiceVolumeVolume)
-		deriveDeepCopy_50(dst.Volume, src.Volume)
+		deriveDeepCopy_52(dst.Volume, src.Volume)
 	}
 	if src.Tmpfs == nil {
 		dst.Tmpfs = nil
 	} else {
 		dst.Tmpfs = new(ServiceVolumeTmpfs)
-		deriveDeepCopy_51(dst.Tmpfs, src.Tmpfs)
+		deriveDeepCopy_53(dst.Tmpfs, src.Tmpfs)
 	}
 	if src.Extensions != nil {
 		dst.Extensions = make(map[string]any, len(src.Extensions))
@@ -1833,8 +1910,8 @@ func deriveDeepCopy_41(dst, src *ServiceVolumeConfig) {
 	}
 }
 
-// deriveDeepCopy_42 recursively copies the contents of src into dst.
-func deriveDeepCopy_42(dst, src *ServiceHook) {
+// deriveDeepCopy_44 recursively copies the contents of src into dst.
+func deriveDeepCopy_44(dst, src *ServiceHook) {
 	if src.Command == nil {
 		dst.Command = nil
 	} else {
@@ -1870,8 +1947,8 @@ func deriveDeepCopy_42(dst, src *ServiceHook) {
 	}
 }
 
-// deriveDeepCopy_43 recursively copies the contents of src into dst.
-func deriveDeepCopy_43(dst, src *IPAMConfig) {
+// deriveDeepCopy_45 recursively copies the contents of src into dst.
+func deriveDeepCopy_45(dst, src *IPAMConfig) {
 	dst.Driver = src.Driver
 	if src.Config == nil {
 		dst.Config = nil
@@ -1889,7 +1966,7 @@ func deriveDeepCopy_43(dst, src *IPAMConfig) {
 		} else {
 			dst.Config = make([]*IPAMPool, len(src.Config))
 		}
-		deriveDeepCopy_52(dst.Config, src.Config)
+		deriveDeepCopy_54(dst.Config, src.Config)
 	}
 	if src.Extensions != nil {
 		dst.Extensions = make(map[string]any, len(src.Extensions))
@@ -1899,8 +1976,8 @@ func deriveDeepCopy_43(dst, src *IPAMConfig) {
 	}
 }
 
-// deriveDeepCopy_44 recursively copies the contents of src into dst.
-func deriveDeepCopy_44(dst, src *Trigger) {
+// deriveDeepCopy_46 recursively copies the contents of src into dst.
+func deriveDeepCopy_46(dst, src *Trigger) {
 	dst.Path = src.Path
 	dst.Action = src.Action
 	dst.Target = src.Target
@@ -1930,8 +2007,8 @@ func deriveDeepCopy_44(dst, src *Trigger) {
 	}
 }
 
-// deriveDeepCopy_45 recursively copies the contents of src into dst.
-func deriveDeepCopy_45(dst, src *WeightDevice) {
+// deriveDeepCopy_47 recursively copies the contents of src into dst.
+func deriveDeepCopy_47(dst, src *WeightDevice) {
 	dst.Path = src.Path
 	dst.Weight = src.Weight
 	if src.Extensions != nil {
@@ -1942,8 +2019,8 @@ func deriveDeepCopy_45(dst, src *WeightDevice) {
 	}
 }
 
-// deriveDeepCopy_46 recursively copies the contents of src into dst.
-func deriveDeepCopy_46(dst, src *ThrottleDevice) {
+// deriveDeepCopy_48 recursively copies the contents of src into dst.
+func deriveDeepCopy_48(dst, src *ThrottleDevice) {
 	dst.Path = src.Path
 	dst.Rate = src.Rate
 	if src.Extensions != nil {
@@ -1954,8 +2031,8 @@ func deriveDeepCopy_46(dst, src *ThrottleDevice) {
 	}
 }
 
-// deriveDeepCopy_47 recursively copies the contents of src into dst.
-func deriveDeepCopy_47(dst, src *Resource) {
+// deriveDeepCopy_49 recursively copies the contents of src into dst.
+func deriveDeepCopy_49(dst, src *Resource) {
 	dst.NanoCPUs = src.NanoCPUs
 	dst.MemoryBytes = src.MemoryBytes
 	dst.Pids = src.Pids
@@ -1975,7 +2052,7 @@ func deriveDeepCopy_47(dst, src *Resource) {
 		} else {
 			dst.Devices = make([]DeviceRequest, len(src.Devices))
 		}
-		deriveDeepCopy_53(dst.Devices, src.Devices)
+		deriveDeepCopy_15(dst.Devices, src.Devices)
 	}
 	if src.GenericResources == nil {
 		dst.GenericResources = nil
@@ -1993,7 +2070,7 @@ func deriveDeepCopy_47(dst, src *Resource) {
 		} else {
 			dst.GenericResources = make([]GenericResource, len(src.GenericResources))
 		}
-		deriveDeepCopy_54(dst.GenericResources, src.GenericResources)
+		deriveDeepCopy_55(dst.GenericResources, src.GenericResources)
 	}
 	if src.Extensions != nil {
 		dst.Extensions = make(map[string]any, len(src.Extensions))
@@ -2003,19 +2080,19 @@ func deriveDeepCopy_47(dst, src *Resource) {
 	}
 }
 
-// deriveDeepCopy_48 recursively copies the contents of src into dst.
-func deriveDeepCopy_48(dst, src []PlacementPreferences) {
+// deriveDeepCopy_50 recursively copies the contents of src into dst.
+func deriveDeepCopy_50(dst, src []PlacementPreferences) {
 	for src_i, src_value := range src {
 		func() {
 			field := new(PlacementPreferences)
-			deriveDeepCopy_55(field, &src_value)
+			deriveDeepCopy_56(field, &src_value)
 			dst[src_i] = *field
 		}()
 	}
 }
 
-// deriveDeepCopy_49 recursively copies the contents of src into dst.
-func deriveDeepCopy_49(dst, src *ServiceVolumeBind) {
+// deriveDeepCopy_51 recursively copies the contents of src into dst.
+func deriveDeepCopy_51(dst, src *ServiceVolumeBind) {
 	dst.SELinux = src.SELinux
 	dst.Propagation = src.Propagation
 	dst.CreateHostPath = src.CreateHostPath
@@ -2027,8 +2104,8 @@ func deriveDeepCopy_49(dst, src *ServiceVolumeBind) {
 	}
 }
 
-// deriveDeepCopy_50 recursively copies the contents of src into dst.
-func deriveDeepCopy_50(dst, src *ServiceVolumeVolume) {
+// deriveDeepCopy_52 recursively copies the contents of src into dst.
+func deriveDeepCopy_52(dst, src *ServiceVolumeVolume) {
 	dst.NoCopy = src.NoCopy
 	dst.Subpath = src.Subpath
 	if src.Extensions != nil {
@@ -2039,8 +2116,8 @@ func deriveDeepCopy_50(dst, src *ServiceVolumeVolume) {
 	}
 }
 
-// deriveDeepCopy_51 recursively copies the contents of src into dst.
-func deriveDeepCopy_51(dst, src *ServiceVolumeTmpfs) {
+// deriveDeepCopy_53 recursively copies the contents of src into dst.
+func deriveDeepCopy_53(dst, src *ServiceVolumeTmpfs) {
 	dst.Size = src.Size
 	dst.Mode = src.Mode
 	if src.Extensions != nil {
@@ -2051,31 +2128,20 @@ func deriveDeepCopy_51(dst, src *ServiceVolumeTmpfs) {
 	}
 }
 
-// deriveDeepCopy_52 recursively copies the contents of src into dst.
-func deriveDeepCopy_52(dst, src []*IPAMPool) {
+// deriveDeepCopy_54 recursively copies the contents of src into dst.
+func deriveDeepCopy_54(dst, src []*IPAMPool) {
 	for src_i, src_value := range src {
 		if src_value == nil {
 			dst[src_i] = nil
 		} else {
 			dst[src_i] = new(IPAMPool)
-			deriveDeepCopy_56(dst[src_i], src_value)
+			deriveDeepCopy_57(dst[src_i], src_value)
 		}
 	}
 }
 
-// deriveDeepCopy_53 recursively copies the contents of src into dst.
-func deriveDeepCopy_53(dst, src []DeviceRequest) {
-	for src_i, src_value := range src {
-		func() {
-			field := new(DeviceRequest)
-			deriveDeepCopy_57(field, &src_value)
-			dst[src_i] = *field
-		}()
-	}
-}
-
-// deriveDeepCopy_54 recursively copies the contents of src into dst.
-func deriveDeepCopy_54(dst, src []GenericResource) {
+// deriveDeepCopy_55 recursively copies the contents of src into dst.
+func deriveDeepCopy_55(dst, src []GenericResource) {
 	for src_i, src_value := range src {
 		func() {
 			field := new(GenericResource)
@@ -2085,8 +2151,8 @@ func deriveDeepCopy_54(dst, src []GenericResource) {
 	}
 }
 
-// deriveDeepCopy_55 recursively copies the contents of src into dst.
-func deriveDeepCopy_55(dst, src *PlacementPreferences) {
+// deriveDeepCopy_56 recursively copies the contents of src into dst.
+func deriveDeepCopy_56(dst, src *PlacementPreferences) {
 	dst.Spread = src.Spread
 	if src.Extensions != nil {
 		dst.Extensions = make(map[string]any, len(src.Extensions))
@@ -2096,8 +2162,8 @@ func deriveDeepCopy_55(dst, src *PlacementPreferences) {
 	}
 }
 
-// deriveDeepCopy_56 recursively copies the contents of src into dst.
-func deriveDeepCopy_56(dst, src *IPAMPool) {
+// deriveDeepCopy_57 recursively copies the contents of src into dst.
+func deriveDeepCopy_57(dst, src *IPAMPool) {
 	dst.Subnet = src.Subnet
 	dst.Gateway = src.Gateway
 	dst.IPRange = src.IPRange
@@ -2115,54 +2181,6 @@ func deriveDeepCopy_56(dst, src *IPAMPool) {
 	}
 }
 
-// deriveDeepCopy_57 recursively copies the contents of src into dst.
-func deriveDeepCopy_57(dst, src *DeviceRequest) {
-	if src.Capabilities == nil {
-		dst.Capabilities = nil
-	} else {
-		if dst.Capabilities != nil {
-			if len(src.Capabilities) > len(dst.Capabilities) {
-				if cap(dst.Capabilities) >= len(src.Capabilities) {
-					dst.Capabilities = (dst.Capabilities)[:len(src.Capabilities)]
-				} else {
-					dst.Capabilities = make([]string, len(src.Capabilities))
-				}
-			} else if len(src.Capabilities) < len(dst.Capabilities) {
-				dst.Capabilities = (dst.Capabilities)[:len(src.Capabilities)]
-			}
-		} else {
-			dst.Capabilities = make([]string, len(src.Capabilities))
-		}
-		copy(dst.Capabilities, src.Capabilities)
-	}
-	dst.Driver = src.Driver
-	dst.Count = src.Count
-	if src.IDs == nil {
-		dst.IDs = nil
-	} else {
-		if dst.IDs != nil {
-			if len(src.IDs) > len(dst.IDs) {
-				if cap(dst.IDs) >= len(src.IDs) {
-					dst.IDs = (dst.IDs)[:len(src.IDs)]
-				} else {
-					dst.IDs = make([]string, len(src.IDs))
-				}
-			} else if len(src.IDs) < len(dst.IDs) {
-				dst.IDs = (dst.IDs)[:len(src.IDs)]
-			}
-		} else {
-			dst.IDs = make([]string, len(src.IDs))
-		}
-		copy(dst.IDs, src.IDs)
-	}
-	if src.Options != nil {
-		dst.Options = make(map[string]string, len(src.Options))
-		deriveDeepCopy_4(dst.Options, src.Options)
-	} else {
-		dst.Options = nil
-	}
-}
-
 // deriveDeepCopy_58 recursively copies the contents of src into dst.
 func deriveDeepCopy_58(dst, src *GenericResource) {
 	if src.DiscreteResourceSpec == nil {
diff --git a/types/types.go b/types/types.go
index dd9705f..d96e206 100644
--- a/types/types.go
+++ b/types/types.go
@@ -82,6 +82,7 @@ type ServiceConfig struct {
 	ExternalLinks   []string                         `yaml:"external_links,omitempty" json:"external_links,omitempty"`
 	ExtraHosts      HostsList                        `yaml:"extra_hosts,omitempty" json:"extra_hosts,omitempty"`
 	GroupAdd        []string                         `yaml:"group_add,omitempty" json:"group_add,omitempty"`
+	Gpus            []DeviceRequest                  `yaml:"gpus,omitempty" json:"gpus,omitempty"`
 	Hostname        string                           `yaml:"hostname,omitempty" json:"hostname,omitempty"`
 	HealthCheck     *HealthCheckConfig               `yaml:"healthcheck,omitempty" json:"healthcheck,omitempty"`
 	Image           string                           `yaml:"image,omitempty" json:"image,omitempty"`
diff --git a/validation/validation.go b/validation/validation.go
index e7cd675..707f247 100644
--- a/validation/validation.go
+++ b/validation/validation.go
@@ -30,6 +30,8 @@ var checks = map[tree.Path]checkerFunc{
 	"configs.*":                       checkFileObject("file", "environment", "content"),
 	"secrets.*":                       checkFileObject("file", "environment"),
 	"services.*.develop.watch.*.path": checkPath,
+	"services.*.deploy.resources.reservations.devices.*": checkDeviceRequest,
+	"services.*.gpus.*": checkDeviceRequest,
 }
 
 func Validate(dict map[string]any) error {
@@ -94,3 +96,13 @@ func checkPath(value any, p tree.Path) error {
 	}
 	return nil
 }
+
+func checkDeviceRequest(value any, p tree.Path) error {
+	v := value.(map[string]any)
+	_, hasCount := v["count"]
+	_, hasIds := v["device_ids"]
+	if hasCount && hasIds {
+		return fmt.Errorf(`%s: "count" and "device_ids" attributes are exclusive`, p)
+	}
+	return nil
+}


Test patch:
diff --git a/loader/loader_test.go b/loader/loader_test.go
index 554ae9f..ec68686 100644
--- a/loader/loader_test.go
+++ b/loader/loader_test.go
@@ -2224,6 +2224,32 @@ services:
 	assert.ErrorContains(t, err, `capabilities is required`)
 }
 
+func TestServiceGpus(t *testing.T) {
+	p, err := loadYAML(`
+name: service-gpus
+services:
+  test:
+    image: redis:alpine
+    gpus:
+      - driver: nvidia
+      - driver: 3dfx
+        device_ids: ["voodoo2"]
+        capabilities: ["directX"]
+`)
+	assert.NilError(t, err)
+	assert.DeepEqual(t, p.Services["test"].Gpus, []types.DeviceRequest{
+		{
+			Driver: "nvidia",
+			Count:  -1,
+		},
+		{
+			Capabilities: []string{"directX"},
+			Driver:       "3dfx",
+			IDs:          []string{"voodoo2"},
+		},
+	})
+}
+
 func TestServicePullPolicy(t *testing.T) {
 	actual, err := loadYAML(`
 name: service-pull-policy