diff --git a/frontend/src/pages/Offers/List/index.tsx b/frontend/src/pages/Offers/List/index.tsx index e24851028..559698b56 100644 --- a/frontend/src/pages/Offers/List/index.tsx +++ b/frontend/src/pages/Offers/List/index.tsx @@ -48,7 +48,7 @@ const getRequestParams = ({ env: {}, resources: { // cpu/memory/disk should match ResourcesSpec.unconstrained() used by `dstack offer` CLI command - cpu: { min: 1 }, + cpu: { count: { min: 1 } }, memory: { min: 0.0 }, disk: null, gpu: { diff --git a/frontend/src/types/gpu.d.ts b/frontend/src/types/gpu.d.ts index 507386aea..3ca543e36 100644 --- a/frontend/src/types/gpu.d.ts +++ b/frontend/src/types/gpu.d.ts @@ -22,8 +22,13 @@ declare interface IGPUSpecRequest { compute_capability?: any[]; } +declare interface ICPUSpecRequest { + arch?: 'x86' | 'arm' | null; + count?: TRange | number | string; +} + declare interface IResourcesSpecRequest { - cpu?: TRange | number | string; + cpu?: ICPUSpecRequest | number | string; memory?: TRange | number | string; shm_size?: number | string; gpu?: IGPUSpecRequest | number | string; diff --git a/frontend/src/types/run.d.ts b/frontend/src/types/run.d.ts index b64d03e3a..4f39d76ed 100644 --- a/frontend/src/types/run.d.ts +++ b/frontend/src/types/run.d.ts @@ -49,7 +49,7 @@ declare type TRange = { min?: number; max?: number }; declare type TResourceRequest = { gpu?: TGPUResources | string | number; - cpu?: string | number | TRange; + cpu?: string | number | ICPUSpecRequest; memory?: string | number | TRange; shm_size?: string | number; disk?: diff --git a/src/dstack/_internal/cli/models/presets.py b/src/dstack/_internal/cli/models/presets.py index 500bcf42b..597e9eae9 100644 --- a/src/dstack/_internal/cli/models/presets.py +++ b/src/dstack/_internal/cli/models/presets.py @@ -14,7 +14,7 @@ from dstack._internal.core.models.common import CoreModel from dstack._internal.core.models.configurations import ServiceConfiguration from dstack._internal.core.models.profiles import ProfileParams -from dstack._internal.core.models.resources import CPUSpec, ResourcesSpec +from dstack._internal.core.models.resources import ResourcesSpec class PresetBenchmarkWorkload(CoreModel): @@ -159,7 +159,7 @@ class PresetListOutput(CoreModel): def _validate_exact_resources(resources: ResourcesSpec) -> None: - cpu = CPUSpec.model_validate(resources.cpu) + cpu = resources.cpu if not _is_exact(cpu.count) or not _is_exact(resources.memory): raise ValueError("preset validation resources must be exact") if resources.disk is None or not _is_exact(resources.disk.size): diff --git a/src/dstack/_internal/cli/services/configurators/run.py b/src/dstack/_internal/cli/services/configurators/run.py index df2d0b35d..13afb3024 100644 --- a/src/dstack/_internal/cli/services/configurators/run.py +++ b/src/dstack/_internal/cli/services/configurators/run.py @@ -59,7 +59,6 @@ from dstack._internal.core.models.repos import RepoHeadWithCreds from dstack._internal.core.models.repos.base import Repo from dstack._internal.core.models.repos.remote import RemoteRepo, RemoteRepoCreds -from dstack._internal.core.models.resources import CPUSpec from dstack._internal.core.models.runs import JobStatus, JobSubmission, RunPlan, RunSpec, RunStatus from dstack._internal.core.services.diff import diff_models from dstack._internal.core.services.repos import get_repo_creds_and_default_branch @@ -533,8 +532,7 @@ def validate_cpu_arch_and_image(self, conf: RunConfigurationT) -> None: """ Infers `resources.cpu.arch` if not set, requires `image` if the architecture is ARM. """ - # TODO: Remove in 0.20. Use conf.resources.cpu directly - cpu_spec = CPUSpec.model_validate(conf.resources.cpu) + cpu_spec = conf.resources.cpu arch = cpu_spec.arch if arch is None: gpu_spec = conf.resources.gpu diff --git a/src/dstack/_internal/core/backends/base/offers.py b/src/dstack/_internal/core/backends/base/offers.py index cb3f974f8..33d745c1b 100644 --- a/src/dstack/_internal/core/backends/base/offers.py +++ b/src/dstack/_internal/core/backends/base/offers.py @@ -16,7 +16,7 @@ InstanceType, Resources, ) -from dstack._internal.core.models.resources import DEFAULT_DISK, CPUSpec, GPUSpec, Memory, Range +from dstack._internal.core.models.resources import DEFAULT_DISK, GPUSpec, Memory, Range from dstack._internal.core.models.runs import Job, Requirements, Run from dstack._internal.utils.common import get_or_error @@ -170,8 +170,7 @@ def requirements_to_query_filter(req: Optional[Requirements]) -> gpuhunt.QueryFi res = req.resources if res.cpu: - # TODO: Remove in 0.20. Use res.cpu directly - cpu = CPUSpec.model_validate(res.cpu) + cpu = res.cpu q.cpu_arch = cpu.arch q.min_cpu = cpu.count.min q.max_cpu = cpu.count.max diff --git a/src/dstack/_internal/core/backends/kubernetes/resources.py b/src/dstack/_internal/core/backends/kubernetes/resources.py index cff54cbdd..393d2b786 100644 --- a/src/dstack/_internal/core/backends/kubernetes/resources.py +++ b/src/dstack/_internal/core/backends/kubernetes/resources.py @@ -26,7 +26,7 @@ InstanceType, Resources, ) -from dstack._internal.core.models.resources import CPUSpec, Memory, ResourcesSpec +from dstack._internal.core.models.resources import Memory, ResourcesSpec from dstack._internal.utils import docker as docker_utils from dstack._internal.utils.common import get_or_error from dstack._internal.utils.logging import get_logger @@ -179,7 +179,6 @@ class ResourceRequests(ResourceRequestsLimits): @classmethod def from_resources_spec(cls, spec: ResourcesSpec) -> Self: - assert isinstance(spec.cpu, CPUSpec) cpu = spec.cpu.count.min or 0 memory_mib: int = 0 if spec.memory.min is not None: @@ -223,7 +222,6 @@ def from_kubernetes_map(cls, map_: Mapping[str, str]) -> Self: class ResourceLimits(ResourceRequestsLimits): @classmethod def from_resources_spec(cls, spec: ResourcesSpec) -> Self: - assert isinstance(spec.cpu, CPUSpec) cpu = spec.cpu.count.max memory_mib: Optional[int] = None if spec.memory.max is not None: @@ -236,7 +234,6 @@ def from_resources_spec(cls, spec: ResourcesSpec) -> Self: if spec.gpu is not None: # GPU resources cannot be overcommitted, limit must be equal to request gpu = spec.gpu.count.min or 0 - assert isinstance(spec.cpu, CPUSpec) return cls( cpu=cpu, memory_mib=memory_mib, diff --git a/src/dstack/_internal/core/backends/slurm/resources.py b/src/dstack/_internal/core/backends/slurm/resources.py index ea7ae5140..5f3fc23d8 100644 --- a/src/dstack/_internal/core/backends/slurm/resources.py +++ b/src/dstack/_internal/core/backends/slurm/resources.py @@ -7,7 +7,6 @@ from dstack._internal.core.models.instances import Gpu from dstack._internal.core.models.resources import ( DEFAULT_MEMORY_SIZE, - CPUSpec, Memory, ResourcesSpec, ) @@ -118,7 +117,6 @@ class RequestedResources: def get_requested_resources_from_resources_spec(spec: ResourcesSpec) -> RequestedResources: - assert isinstance(spec.cpu, CPUSpec) # 1 is the default value of --cpus-per-task cpu_count = spec.cpu.count.min or 1 diff --git a/src/dstack/_internal/core/models/resources.py b/src/dstack/_internal/core/models/resources.py index 92f7ac4c6..c6b113858 100644 --- a/src/dstack/_internal/core/models/resources.py +++ b/src/dstack/_internal/core/models/resources.py @@ -9,10 +9,7 @@ Field, GetCoreSchemaHandler, GetJsonSchemaHandler, - SerializerFunctionWrapHandler, - Tag, field_validator, - model_serializer, model_validator, ) from pydantic.json_schema import JsonSchemaValue @@ -227,11 +224,9 @@ def parse(cls, v: Any) -> Any: # Range and min/max dict - for backward compatibility if isinstance(v, Range): return {"arch": None, "count": v} - # A subset rather than exactly {"min", "max"}: `ResourcesSpec` serializes `cpu` down to its - # count for old clients, and under `exclude_none=True` that leaves just `{"min": ...}`. - # Requiring both keys made the round trip land on the `Range[int]` arm of `ResourcesSpec.cpu` - # instead of coming back as a `CPUSpec`. `arch`/`count` are the only `CPUSpec` fields, so a - # mapping of min/max is unambiguously a range. + # `arch` and `count` are the only `CPUSpec` fields, so a mapping of `min`/`max` is + # unambiguously a count range. A subset rather than exactly `{"min", "max"}`, because a + # half-open range may omit the other key. if isinstance(v, Mapping) and v and v.keys() <= {"min", "max"}: return {"arch": None, "count": v} return v @@ -395,20 +390,7 @@ def _parse(cls, v: Any) -> Any: class ResourcesSpec(CoreModel): - # TODO: remove `Range[int]` in 0.20. It is kept only for backward compatibility. - cpu: Annotated[ - Union[ - # `Tag` only names the arm in validation errors. Without it the `loc` of a bad `cpu` - # spells out the whole wrapped schema — - # `cpu.function-before[parse(), function-before[parse(), ... CPUSpec]].count` — which - # is what `dstack apply` shows the user. - Annotated[CPUSpec, Tag("CPUSpec")], - Annotated[Range[int], Tag("Range[int]")], - ], - # `CPUSpec` and `Range[int]` both accept a bare int/str, so the arm has to be picked by - # declaration order rather than by pydantic v2's "smart" union resolution. - Field(description="The CPU requirements", union_mode="left_to_right"), - ] = CPUSpec() + cpu: Annotated[CPUSpec, Field(description="The CPU requirements")] = CPUSpec() memory: Annotated[Range[Memory], Field(description="The RAM size (e.g., `8GB`)")] = ( DEFAULT_MEMORY_SIZE ) @@ -435,8 +417,7 @@ def unconstrained(cls) -> "ResourcesSpec": ) def pretty_format(self) -> str: - # TODO: Remove in 0.20. Use self.cpu directly - cpu = CPUSpec.model_validate(self.cpu) + cpu = self.cpu resources: Dict[str, Any] = dict(cpu_arch=cpu.arch, cpus=cpu.count, memory=self.memory) if self.gpu: gpu = self.gpu @@ -452,18 +433,3 @@ def pretty_format(self) -> str: resources.update(disk_size=self.disk.size) res = pretty_resources(**resources) return res - - @model_serializer(mode="wrap") - def _serialize(self, handler: SerializerFunctionWrapHandler) -> Dict[str, Any]: - res = handler(self) - self._update_serialized_cpu(res) - return res - - # TODO: Remove in 0.20. Added for backward compatibility. - def _update_serialized_cpu(self, values: Dict): - cpu = values.get("cpu") - if cpu: - arch = cpu.get("arch") - count = cpu.get("count") - if count and arch in [None, gpuhunt.CPUArchitecture.X86.value]: - values["cpu"] = count diff --git a/src/dstack/_internal/server/services/requirements/combine.py b/src/dstack/_internal/server/services/requirements/combine.py index 4ad168586..090f9c89d 100644 --- a/src/dstack/_internal/server/services/requirements/combine.py +++ b/src/dstack/_internal/server/services/requirements/combine.py @@ -171,7 +171,7 @@ def _combine_tags(value1: dict[str, str], value2: dict[str, str]) -> dict[str, s def _combine_resources(value1: ResourcesSpec, value2: ResourcesSpec) -> ResourcesSpec: return ResourcesSpec( - cpu=_combine_cpu(value1.cpu, value2.cpu), # type: ignore[attr-defined] + cpu=_combine_cpu(value1.cpu, value2.cpu), memory=_combine_memory(value1.memory, value2.memory), shm_size=_combine_shm_size_optional(value1.shm_size, value2.shm_size), gpu=_combine_gpu_optional(value1.gpu, value2.gpu), diff --git a/src/dstack/_internal/server/services/resources.py b/src/dstack/_internal/server/services/resources.py index 176445d46..f439713d4 100644 --- a/src/dstack/_internal/server/services/resources.py +++ b/src/dstack/_internal/server/services/resources.py @@ -2,12 +2,11 @@ import gpuhunt -from dstack._internal.core.models.resources import CPUSpec, ResourcesSpec +from dstack._internal.core.models.resources import ResourcesSpec def set_resources_defaults(resources: ResourcesSpec) -> None: - # TODO: Remove in 0.20. Use resources.cpu directly - cpu = CPUSpec.model_validate(resources.cpu) + cpu = resources.cpu if cpu.arch is None: gpu = resources.gpu if ( @@ -19,7 +18,6 @@ def set_resources_defaults(resources: ResourcesSpec) -> None: cpu.arch = gpuhunt.CPUArchitecture.ARM else: cpu.arch = gpuhunt.CPUArchitecture.X86 - resources.cpu = cpu def set_gpu_vendor_default( diff --git a/src/tests/_internal/core/models/test_resources.py b/src/tests/_internal/core/models/test_resources.py index 60227deef..9043b15af 100644 --- a/src/tests/_internal/core/models/test_resources.py +++ b/src/tests/_internal/core/models/test_resources.py @@ -158,13 +158,26 @@ def test_range_object(self): "count": {"min": 1, "max": 2}, } - def test_range_dict(self): - assert CPUSpec.model_validate({"min": 1, "max": 2}).model_dump() == { + @pytest.mark.parametrize( + ["value", "expected_min", "expected_max"], + [ + pytest.param({"min": 1, "max": 2}, 1, 2, id="closed"), + pytest.param({"min": 1}, 1, None, id="min-only"), + pytest.param({"max": 2}, None, 2, id="max-only"), + # An empty mapping is not a range: it falls through to the `CPUSpec` defaults instead + # of an empty `count` range, which `Range` rejects. + pytest.param({}, DEFAULT_CPU_COUNT.min, DEFAULT_CPU_COUNT.max, id="empty"), + ], + ) + def test_range_dict( + self, value: dict, expected_min: Optional[int], expected_max: Optional[int] + ): + assert CPUSpec.model_validate(value).model_dump() == { "arch": None, - "count": {"min": 1, "max": 2}, + "count": {"min": expected_min, "max": expected_max}, } - def test_valid_dict(self): + def test_valid_dict_with_all_fields(self): assert CPUSpec.model_validate( {"arch": "ARM", "count": {"min": 1, "max": 2}} ).model_dump() == { @@ -172,6 +185,12 @@ def test_valid_dict(self): "count": {"min": 1, "max": 2}, } + def test_valid_dict_no_arch_half_open_count_range(self): + assert CPUSpec.model_validate({"count": {"max": 2}}).model_dump() == { + "arch": None, + "count": {"min": None, "max": 2}, + } + def test_invalid_dict(self): with pytest.raises(ValidationError): CPUSpec.model_validate({"arch": "x86", "min": 1, "max": 2}) diff --git a/src/tests/_internal/pydantic_compat/fixtures/parsing/api_request/apply_run_plan_request.values.json b/src/tests/_internal/pydantic_compat/fixtures/parsing/api_request/apply_run_plan_request.values.json index a2e4c0247..4ab590301 100644 --- a/src/tests/_internal/pydantic_compat/fixtures/parsing/api_request/apply_run_plan_request.values.json +++ b/src/tests/_internal/pydantic_compat/fixtures/parsing/api_request/apply_run_plan_request.values.json @@ -42,8 +42,11 @@ "reservation": null, "resources": { "cpu": { - "max": null, - "min": 2 + "arch": null, + "count": { + "max": null, + "min": 2 + } }, "disk": { "size": { diff --git a/src/tests/_internal/pydantic_compat/fixtures/parsing/api_response/run_plan.values.json b/src/tests/_internal/pydantic_compat/fixtures/parsing/api_response/run_plan.values.json index df8614566..6b21b39ee 100644 --- a/src/tests/_internal/pydantic_compat/fixtures/parsing/api_response/run_plan.values.json +++ b/src/tests/_internal/pydantic_compat/fixtures/parsing/api_response/run_plan.values.json @@ -38,8 +38,11 @@ "reservation": null, "resources": { "cpu": { - "max": null, - "min": 2 + "arch": null, + "count": { + "max": null, + "min": 2 + } }, "disk": { "size": { @@ -151,8 +154,11 @@ "reservation": null, "resources": { "cpu": { - "max": null, - "min": 2 + "arch": null, + "count": { + "max": null, + "min": 2 + } }, "disk": { "size": { diff --git a/src/tests/_internal/pydantic_compat/fixtures/parsing/config/dev_environment.values.json b/src/tests/_internal/pydantic_compat/fixtures/parsing/config/dev_environment.values.json index c79cf2d91..4e2d833b6 100644 --- a/src/tests/_internal/pydantic_compat/fixtures/parsing/config/dev_environment.values.json +++ b/src/tests/_internal/pydantic_compat/fixtures/parsing/config/dev_environment.values.json @@ -33,8 +33,11 @@ "reservation": null, "resources": { "cpu": { - "max": null, - "min": 2 + "arch": null, + "count": { + "max": null, + "min": 2 + } }, "disk": { "size": { diff --git a/src/tests/_internal/pydantic_compat/fixtures/parsing/config/fleet.values.json b/src/tests/_internal/pydantic_compat/fixtures/parsing/config/fleet.values.json index 66cac1767..ec05ffeef 100644 --- a/src/tests/_internal/pydantic_compat/fixtures/parsing/config/fleet.values.json +++ b/src/tests/_internal/pydantic_compat/fixtures/parsing/config/fleet.values.json @@ -17,8 +17,11 @@ "reservation": null, "resources": { "cpu": { - "max": null, - "min": 2 + "arch": null, + "count": { + "max": null, + "min": 2 + } }, "disk": { "size": { diff --git a/src/tests/_internal/pydantic_compat/fixtures/parsing/config/service.values.json b/src/tests/_internal/pydantic_compat/fixtures/parsing/config/service.values.json index cf387dcd9..0877be2a2 100644 --- a/src/tests/_internal/pydantic_compat/fixtures/parsing/config/service.values.json +++ b/src/tests/_internal/pydantic_compat/fixtures/parsing/config/service.values.json @@ -46,8 +46,11 @@ "reservation": null, "resources": { "cpu": { - "max": null, - "min": 2 + "arch": null, + "count": { + "max": null, + "min": 2 + } }, "disk": { "size": { diff --git a/src/tests/_internal/pydantic_compat/fixtures/parsing/config/task.values.json b/src/tests/_internal/pydantic_compat/fixtures/parsing/config/task.values.json index d6cf38dd1..e108427f1 100644 --- a/src/tests/_internal/pydantic_compat/fixtures/parsing/config/task.values.json +++ b/src/tests/_internal/pydantic_compat/fixtures/parsing/config/task.values.json @@ -37,8 +37,11 @@ "reservation": null, "resources": { "cpu": { - "max": 8, - "min": 2 + "arch": null, + "count": { + "max": 8, + "min": 2 + } }, "disk": { "size": { diff --git a/src/tests/_internal/pydantic_compat/fixtures/parsing/db/fleet_spec.values.json b/src/tests/_internal/pydantic_compat/fixtures/parsing/db/fleet_spec.values.json index da0793eb4..00eed1fc2 100644 --- a/src/tests/_internal/pydantic_compat/fixtures/parsing/db/fleet_spec.values.json +++ b/src/tests/_internal/pydantic_compat/fixtures/parsing/db/fleet_spec.values.json @@ -25,8 +25,11 @@ "reservation": null, "resources": { "cpu": { - "max": null, - "min": 8 + "arch": null, + "count": { + "max": null, + "min": 8 + } }, "disk": { "size": { diff --git a/src/tests/_internal/pydantic_compat/fixtures/parsing/db/job_spec.values.json b/src/tests/_internal/pydantic_compat/fixtures/parsing/db/job_spec.values.json index 3c95dddd3..b8525981b 100644 --- a/src/tests/_internal/pydantic_compat/fixtures/parsing/db/job_spec.values.json +++ b/src/tests/_internal/pydantic_compat/fixtures/parsing/db/job_spec.values.json @@ -32,8 +32,11 @@ "reservation": null, "resources": { "cpu": { - "max": 8, - "min": 2 + "arch": null, + "count": { + "max": 8, + "min": 2 + } }, "disk": { "size": { diff --git a/src/tests/_internal/pydantic_compat/fixtures/parsing/db/requirements.values.json b/src/tests/_internal/pydantic_compat/fixtures/parsing/db/requirements.values.json index 41fc54a76..b42b52deb 100644 --- a/src/tests/_internal/pydantic_compat/fixtures/parsing/db/requirements.values.json +++ b/src/tests/_internal/pydantic_compat/fixtures/parsing/db/requirements.values.json @@ -5,8 +5,11 @@ "reservation": "cr-0abc123def4567890", "resources": { "cpu": { - "max": 64, - "min": 8 + "arch": null, + "count": { + "max": 64, + "min": 8 + } }, "disk": { "size": { diff --git a/src/tests/_internal/pydantic_compat/fixtures/parsing/db/run_spec.values.json b/src/tests/_internal/pydantic_compat/fixtures/parsing/db/run_spec.values.json index 21b666119..0619e3dff 100644 --- a/src/tests/_internal/pydantic_compat/fixtures/parsing/db/run_spec.values.json +++ b/src/tests/_internal/pydantic_compat/fixtures/parsing/db/run_spec.values.json @@ -38,8 +38,11 @@ "reservation": null, "resources": { "cpu": { - "max": 8, - "min": 2 + "arch": null, + "count": { + "max": 8, + "min": 2 + } }, "disk": { "size": { diff --git a/src/tests/_internal/pydantic_compat/fixtures/schema/configuration.json b/src/tests/_internal/pydantic_compat/fixtures/schema/configuration.json index 33d399b4b..0e3b9af5e 100644 --- a/src/tests/_internal/pydantic_compat/fixtures/schema/configuration.json +++ b/src/tests/_internal/pydantic_compat/fixtures/schema/configuration.json @@ -699,8 +699,11 @@ "$ref": "#/$defs/ResourcesSpec", "default": { "cpu": { - "max": null, - "min": 2 + "arch": null, + "count": { + "max": null, + "min": 2 + } }, "disk": { "size": { @@ -2584,8 +2587,11 @@ "$ref": "#/$defs/ResourcesSpec", "default": { "cpu": { - "max": null, - "min": 2 + "arch": null, + "count": { + "max": null, + "min": 2 + } }, "disk": { "size": { @@ -2763,9 +2769,6 @@ }, { "type": "string" - }, - { - "$ref": "#/$defs/Range_int_" } ], "default": { @@ -2775,8 +2778,7 @@ "min": 2 } }, - "description": "The CPU requirements", - "title": "Cpu" + "description": "The CPU requirements" }, "disk": { "anyOf": [ @@ -3964,8 +3966,11 @@ "$ref": "#/$defs/ResourcesSpec", "default": { "cpu": { - "max": null, - "min": 2 + "arch": null, + "count": { + "max": null, + "min": 2 + } }, "disk": { "size": { @@ -4723,8 +4728,11 @@ "$ref": "#/$defs/ResourcesSpec", "default": { "cpu": { - "max": null, - "min": 2 + "arch": null, + "count": { + "max": null, + "min": 2 + } }, "disk": { "size": { diff --git a/src/tests/_internal/pydantic_compat/fixtures/serialization/api_request/apply_fleet_plan_request.json b/src/tests/_internal/pydantic_compat/fixtures/serialization/api_request/apply_fleet_plan_request.json index 3a61a13fa..dba71a9f8 100644 --- a/src/tests/_internal/pydantic_compat/fixtures/serialization/api_request/apply_fleet_plan_request.json +++ b/src/tests/_internal/pydantic_compat/fixtures/serialization/api_request/apply_fleet_plan_request.json @@ -31,8 +31,11 @@ "reservation": "test-reservation", "resources": { "cpu": { - "max": 8, - "min": 2 + "arch": null, + "count": { + "max": 8, + "min": 2 + } }, "disk": { "size": { diff --git a/src/tests/_internal/pydantic_compat/fixtures/serialization/api_request/apply_run_plan_request.json b/src/tests/_internal/pydantic_compat/fixtures/serialization/api_request/apply_run_plan_request.json index 1703c61ae..22fdc9c24 100644 --- a/src/tests/_internal/pydantic_compat/fixtures/serialization/api_request/apply_run_plan_request.json +++ b/src/tests/_internal/pydantic_compat/fixtures/serialization/api_request/apply_run_plan_request.json @@ -38,8 +38,11 @@ "reservation": null, "resources": { "cpu": { - "max": 8, - "min": 2 + "arch": null, + "count": { + "max": 8, + "min": 2 + } }, "disk": { "size": { diff --git a/src/tests/_internal/pydantic_compat/fixtures/serialization/api_response/fleet.json b/src/tests/_internal/pydantic_compat/fixtures/serialization/api_response/fleet.json index ead4a0950..dbe8fb9a1 100644 --- a/src/tests/_internal/pydantic_compat/fixtures/serialization/api_response/fleet.json +++ b/src/tests/_internal/pydantic_compat/fixtures/serialization/api_response/fleet.json @@ -59,8 +59,11 @@ "reservation": "test-reservation", "resources": { "cpu": { - "max": 8, - "min": 2 + "arch": null, + "count": { + "max": 8, + "min": 2 + } }, "disk": { "size": { diff --git a/src/tests/_internal/pydantic_compat/fixtures/serialization/api_response/fleet_plan.json b/src/tests/_internal/pydantic_compat/fixtures/serialization/api_response/fleet_plan.json index b9f81fa33..07ae03e4c 100644 --- a/src/tests/_internal/pydantic_compat/fixtures/serialization/api_response/fleet_plan.json +++ b/src/tests/_internal/pydantic_compat/fixtures/serialization/api_response/fleet_plan.json @@ -60,8 +60,11 @@ "reservation": "test-reservation", "resources": { "cpu": { - "max": 8, - "min": 2 + "arch": null, + "count": { + "max": 8, + "min": 2 + } }, "disk": { "size": { diff --git a/src/tests/_internal/pydantic_compat/fixtures/serialization/api_response/run_plan.json b/src/tests/_internal/pydantic_compat/fixtures/serialization/api_response/run_plan.json index e5a910e81..21191b28b 100644 --- a/src/tests/_internal/pydantic_compat/fixtures/serialization/api_response/run_plan.json +++ b/src/tests/_internal/pydantic_compat/fixtures/serialization/api_response/run_plan.json @@ -38,8 +38,11 @@ "reservation": "test-reservation", "resources": { "cpu": { - "max": 8, - "min": 2 + "arch": null, + "count": { + "max": 8, + "min": 2 + } }, "disk": { "size": { @@ -152,8 +155,11 @@ "reservation": null, "resources": { "cpu": { - "max": 8, - "min": 2 + "arch": null, + "count": { + "max": 8, + "min": 2 + } }, "disk": { "size": { diff --git a/src/tests/_internal/pydantic_compat/fixtures/serialization/db/fleet_spec.json b/src/tests/_internal/pydantic_compat/fixtures/serialization/db/fleet_spec.json index b746b99f2..3350ae469 100644 --- a/src/tests/_internal/pydantic_compat/fixtures/serialization/db/fleet_spec.json +++ b/src/tests/_internal/pydantic_compat/fixtures/serialization/db/fleet_spec.json @@ -27,8 +27,11 @@ "reservation": "test-reservation", "resources": { "cpu": { - "max": 8, - "min": 2 + "arch": null, + "count": { + "max": 8, + "min": 2 + } }, "disk": { "size": { diff --git a/src/tests/_internal/pydantic_compat/fixtures/serialization/db/job_spec.json b/src/tests/_internal/pydantic_compat/fixtures/serialization/db/job_spec.json index 5f4662b98..d7bdb897c 100644 --- a/src/tests/_internal/pydantic_compat/fixtures/serialization/db/job_spec.json +++ b/src/tests/_internal/pydantic_compat/fixtures/serialization/db/job_spec.json @@ -32,8 +32,11 @@ "reservation": "test-reservation", "resources": { "cpu": { - "max": 8, - "min": 2 + "arch": null, + "count": { + "max": 8, + "min": 2 + } }, "disk": { "size": { diff --git a/src/tests/_internal/pydantic_compat/fixtures/serialization/db/requirements.json b/src/tests/_internal/pydantic_compat/fixtures/serialization/db/requirements.json index 24b8cb2a3..3c7974918 100644 --- a/src/tests/_internal/pydantic_compat/fixtures/serialization/db/requirements.json +++ b/src/tests/_internal/pydantic_compat/fixtures/serialization/db/requirements.json @@ -5,8 +5,11 @@ "reservation": "test-reservation", "resources": { "cpu": { - "max": 8, - "min": 2 + "arch": null, + "count": { + "max": 8, + "min": 2 + } }, "disk": { "size": { diff --git a/src/tests/_internal/pydantic_compat/fixtures/serialization/db/run_spec.json b/src/tests/_internal/pydantic_compat/fixtures/serialization/db/run_spec.json index 8de08fc8f..27ac4eaf9 100644 --- a/src/tests/_internal/pydantic_compat/fixtures/serialization/db/run_spec.json +++ b/src/tests/_internal/pydantic_compat/fixtures/serialization/db/run_spec.json @@ -34,8 +34,11 @@ "reservation": null, "resources": { "cpu": { - "max": 8, - "min": 2 + "arch": null, + "count": { + "max": 8, + "min": 2 + } }, "disk": { "size": { diff --git a/src/tests/_internal/pydantic_compat/fixtures/serialization/runner/submit_body.json b/src/tests/_internal/pydantic_compat/fixtures/serialization/runner/submit_body.json index f429b9f54..efa06c624 100644 --- a/src/tests/_internal/pydantic_compat/fixtures/serialization/runner/submit_body.json +++ b/src/tests/_internal/pydantic_compat/fixtures/serialization/runner/submit_body.json @@ -66,8 +66,11 @@ "reservation": null, "resources": { "cpu": { - "max": 8, - "min": 2 + "arch": null, + "count": { + "max": 8, + "min": 2 + } }, "disk": { "size": { @@ -154,8 +157,11 @@ "reservation": null, "resources": { "cpu": { - "max": 8, - "min": 2 + "arch": null, + "count": { + "max": 8, + "min": 2 + } }, "disk": { "size": { diff --git a/src/tests/_internal/pydantic_compat/test_serialization.py b/src/tests/_internal/pydantic_compat/test_serialization.py index efc51f443..fc57e0dcf 100644 --- a/src/tests/_internal/pydantic_compat/test_serialization.py +++ b/src/tests/_internal/pydantic_compat/test_serialization.py @@ -18,15 +18,12 @@ import json from typing import Any, Callable, Union -import gpuhunt import pytest from pydantic import BaseModel from dstack._internal.core.models.common import CoreModel -from dstack._internal.core.models.configurations import TaskConfiguration from dstack._internal.core.models.fleets import FleetConfiguration, FleetNodesSpec from dstack._internal.core.models.repos.remote import RemoteRunRepoData -from dstack._internal.core.models.resources import CPUSpec, Range, ResourcesSpec from dstack._internal.core.models.volumes import RunpodVolumeConfiguration, VolumeSpec from dstack._internal.server.utils.routers import CustomJSONResponse from tests._internal.pydantic_compat import backend_factories, factories @@ -227,60 +224,6 @@ def test_the_api_fixture_exercises_the_hack(self): assert nodes.min == nodes.target, "fixture must hit the target == min branch" -class TestResourcesSpecCPUCompatHack: - @pytest.mark.parametrize( - ("arch", "expected"), - [ - pytest.param(None, {"min": 2, "max": 8}, id="unspecified-architecture"), - pytest.param( - gpuhunt.CPUArchitecture.X86, - {"min": 2, "max": 8}, - id="x86-architecture", - ), - pytest.param( - gpuhunt.CPUArchitecture.ARM, - {"arch": "arm", "count": {"min": 2, "max": 8}}, - id="arm-architecture", - ), - ], - ) - def test_dict_and_json_apply_the_same_override(self, arch, expected): - resources = ResourcesSpec( - cpu=CPUSpec(arch=arch, count=Range[int](min=2, max=8)), - ) - - assert json.loads(resources.model_dump_json())["cpu"] == expected - # CoreModel.json() must call the overridden dict(); this assertion would expose a drift - # even if only one of the two methods retained the compatibility rewrite. - assert canonicalize(json.dumps(resources.model_dump()["cpu"])) == canonicalize( - json.dumps(expected) - ) - - @pytest.mark.parametrize( - ("arch", "expected"), - [ - pytest.param(None, {"min": 2, "max": 8}, id="unspecified-architecture"), - pytest.param( - gpuhunt.CPUArchitecture.ARM, - {"arch": "arm", "count": {"min": 2, "max": 8}}, - id="arm-architecture", - ), - ], - ) - def test_override_is_applied_when_nested(self, arch, expected): - configuration = TaskConfiguration( - commands=["echo hi"], - resources=ResourcesSpec( - cpu=CPUSpec(arch=arch, count=Range[int](min=2, max=8)), - ), - ) - - assert json.loads(configuration.model_dump_json())["resources"]["cpu"] == expected - assert canonicalize( - json.dumps(configuration.model_dump()["resources"]["cpu"]) - ) == canonicalize(json.dumps(expected)) - - class TestFieldSerializationFilters: def test_submit_body_nested_field_includes_are_preserved(self): body = json.loads(factories.submit_body().json_for_runner()) diff --git a/src/tests/_internal/server/routers/test_runs.py b/src/tests/_internal/server/routers/test_runs.py index 46d530fff..277d6cca6 100644 --- a/src/tests/_internal/server/routers/test_runs.py +++ b/src/tests/_internal/server/routers/test_runs.py @@ -182,7 +182,7 @@ def get_dev_env_run_plan_dict( "type": "dev-environment", "name": None, "resources": { - "cpu": {"min": 2, "max": None}, + "cpu": {"arch": "x86", "count": {"min": 2, "max": None}}, "memory": {"min": 8.0, "max": None}, "disk": None, "gpu": None, @@ -291,7 +291,7 @@ def get_dev_env_run_plan_dict( "registry_auth": None, "requirements": { "resources": { - "cpu": {"min": 2, "max": None}, + "cpu": {"arch": "x86", "count": {"min": 2, "max": None}}, "memory": {"min": 8.0, "max": None}, "disk": None, "gpu": None, @@ -434,7 +434,7 @@ def get_dev_env_run_dict( "name": None, "type": "dev-environment", "resources": { - "cpu": {"min": 2, "max": None}, + "cpu": {"arch": "x86", "count": {"min": 2, "max": None}}, "memory": {"min": 8.0, "max": None}, "disk": None, "gpu": None, @@ -538,7 +538,7 @@ def get_dev_env_run_dict( "registry_auth": None, "requirements": { "resources": { - "cpu": {"min": 2, "max": None}, + "cpu": {"arch": "x86", "count": {"min": 2, "max": None}}, "memory": {"min": 8.0, "max": None}, "disk": None, "gpu": None,