Skip to content

Commit 60ad1ef

Browse files
authored
Drop support for ResourcesSpec.cpu as Range[int] (#4091)
Breaks compatibility with clients < 0.19.8 (2025-05-07). CPUSpec.parse still supports and must continue to support infinitely parsing min/max ranges for old ResourceSpecs stored in the DB.
1 parent 6510087 commit 60ad1ef

35 files changed

Lines changed: 185 additions & 185 deletions

File tree

frontend/src/pages/Offers/List/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ const getRequestParams = ({
4848
env: {},
4949
resources: {
5050
// cpu/memory/disk should match ResourcesSpec.unconstrained() used by `dstack offer` CLI command
51-
cpu: { min: 1 },
51+
cpu: { count: { min: 1 } },
5252
memory: { min: 0.0 },
5353
disk: null,
5454
gpu: {

frontend/src/types/gpu.d.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,13 @@ declare interface IGPUSpecRequest {
2222
compute_capability?: any[];
2323
}
2424

25+
declare interface ICPUSpecRequest {
26+
arch?: 'x86' | 'arm' | null;
27+
count?: TRange | number | string;
28+
}
29+
2530
declare interface IResourcesSpecRequest {
26-
cpu?: TRange | number | string;
31+
cpu?: ICPUSpecRequest | number | string;
2732
memory?: TRange | number | string;
2833
shm_size?: number | string;
2934
gpu?: IGPUSpecRequest | number | string;

frontend/src/types/run.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ declare type TRange = { min?: number; max?: number };
4949

5050
declare type TResourceRequest = {
5151
gpu?: TGPUResources | string | number;
52-
cpu?: string | number | TRange;
52+
cpu?: string | number | ICPUSpecRequest;
5353
memory?: string | number | TRange;
5454
shm_size?: string | number;
5555
disk?:

src/dstack/_internal/cli/models/presets.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from dstack._internal.core.models.common import CoreModel
1515
from dstack._internal.core.models.configurations import ServiceConfiguration
1616
from dstack._internal.core.models.profiles import ProfileParams
17-
from dstack._internal.core.models.resources import CPUSpec, ResourcesSpec
17+
from dstack._internal.core.models.resources import ResourcesSpec
1818

1919

2020
class PresetBenchmarkWorkload(CoreModel):
@@ -159,7 +159,7 @@ class PresetListOutput(CoreModel):
159159

160160

161161
def _validate_exact_resources(resources: ResourcesSpec) -> None:
162-
cpu = CPUSpec.model_validate(resources.cpu)
162+
cpu = resources.cpu
163163
if not _is_exact(cpu.count) or not _is_exact(resources.memory):
164164
raise ValueError("preset validation resources must be exact")
165165
if resources.disk is None or not _is_exact(resources.disk.size):

src/dstack/_internal/cli/services/configurators/run.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@
5959
from dstack._internal.core.models.repos import RepoHeadWithCreds
6060
from dstack._internal.core.models.repos.base import Repo
6161
from dstack._internal.core.models.repos.remote import RemoteRepo, RemoteRepoCreds
62-
from dstack._internal.core.models.resources import CPUSpec
6362
from dstack._internal.core.models.runs import JobStatus, JobSubmission, RunPlan, RunSpec, RunStatus
6463
from dstack._internal.core.services.diff import diff_models
6564
from dstack._internal.core.services.repos import get_repo_creds_and_default_branch
@@ -525,8 +524,7 @@ def validate_cpu_arch_and_image(self, conf: RunConfigurationT) -> None:
525524
"""
526525
Infers `resources.cpu.arch` if not set, requires `image` if the architecture is ARM.
527526
"""
528-
# TODO: Remove in 0.20. Use conf.resources.cpu directly
529-
cpu_spec = CPUSpec.model_validate(conf.resources.cpu)
527+
cpu_spec = conf.resources.cpu
530528
arch = cpu_spec.arch
531529
if arch is None:
532530
gpu_spec = conf.resources.gpu

src/dstack/_internal/core/backends/base/offers.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
InstanceType,
1717
Resources,
1818
)
19-
from dstack._internal.core.models.resources import DEFAULT_DISK, CPUSpec, GPUSpec, Memory, Range
19+
from dstack._internal.core.models.resources import DEFAULT_DISK, GPUSpec, Memory, Range
2020
from dstack._internal.core.models.runs import Job, Requirements, Run
2121
from dstack._internal.utils.common import get_or_error
2222

@@ -170,8 +170,7 @@ def requirements_to_query_filter(req: Optional[Requirements]) -> gpuhunt.QueryFi
170170

171171
res = req.resources
172172
if res.cpu:
173-
# TODO: Remove in 0.20. Use res.cpu directly
174-
cpu = CPUSpec.model_validate(res.cpu)
173+
cpu = res.cpu
175174
q.cpu_arch = cpu.arch
176175
q.min_cpu = cpu.count.min
177176
q.max_cpu = cpu.count.max

src/dstack/_internal/core/backends/kubernetes/resources.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
InstanceType,
2727
Resources,
2828
)
29-
from dstack._internal.core.models.resources import CPUSpec, Memory, ResourcesSpec
29+
from dstack._internal.core.models.resources import Memory, ResourcesSpec
3030
from dstack._internal.utils import docker as docker_utils
3131
from dstack._internal.utils.common import get_or_error
3232
from dstack._internal.utils.logging import get_logger
@@ -179,7 +179,6 @@ class ResourceRequests(ResourceRequestsLimits):
179179

180180
@classmethod
181181
def from_resources_spec(cls, spec: ResourcesSpec) -> Self:
182-
assert isinstance(spec.cpu, CPUSpec)
183182
cpu = spec.cpu.count.min or 0
184183
memory_mib: int = 0
185184
if spec.memory.min is not None:
@@ -223,7 +222,6 @@ def from_kubernetes_map(cls, map_: Mapping[str, str]) -> Self:
223222
class ResourceLimits(ResourceRequestsLimits):
224223
@classmethod
225224
def from_resources_spec(cls, spec: ResourcesSpec) -> Self:
226-
assert isinstance(spec.cpu, CPUSpec)
227225
cpu = spec.cpu.count.max
228226
memory_mib: Optional[int] = None
229227
if spec.memory.max is not None:
@@ -236,7 +234,6 @@ def from_resources_spec(cls, spec: ResourcesSpec) -> Self:
236234
if spec.gpu is not None:
237235
# GPU resources cannot be overcommitted, limit must be equal to request
238236
gpu = spec.gpu.count.min or 0
239-
assert isinstance(spec.cpu, CPUSpec)
240237
return cls(
241238
cpu=cpu,
242239
memory_mib=memory_mib,

src/dstack/_internal/core/backends/slurm/resources.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
from dstack._internal.core.models.instances import Gpu
88
from dstack._internal.core.models.resources import (
99
DEFAULT_MEMORY_SIZE,
10-
CPUSpec,
1110
Memory,
1211
ResourcesSpec,
1312
)
@@ -118,7 +117,6 @@ class RequestedResources:
118117

119118

120119
def get_requested_resources_from_resources_spec(spec: ResourcesSpec) -> RequestedResources:
121-
assert isinstance(spec.cpu, CPUSpec)
122120
# 1 is the default value of --cpus-per-task
123121
cpu_count = spec.cpu.count.min or 1
124122

src/dstack/_internal/core/models/resources.py

Lines changed: 5 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,7 @@
99
Field,
1010
GetCoreSchemaHandler,
1111
GetJsonSchemaHandler,
12-
SerializerFunctionWrapHandler,
13-
Tag,
1412
field_validator,
15-
model_serializer,
1613
model_validator,
1714
)
1815
from pydantic.json_schema import JsonSchemaValue
@@ -227,11 +224,9 @@ def parse(cls, v: Any) -> Any:
227224
# Range and min/max dict - for backward compatibility
228225
if isinstance(v, Range):
229226
return {"arch": None, "count": v}
230-
# A subset rather than exactly {"min", "max"}: `ResourcesSpec` serializes `cpu` down to its
231-
# count for old clients, and under `exclude_none=True` that leaves just `{"min": ...}`.
232-
# Requiring both keys made the round trip land on the `Range[int]` arm of `ResourcesSpec.cpu`
233-
# instead of coming back as a `CPUSpec`. `arch`/`count` are the only `CPUSpec` fields, so a
234-
# mapping of min/max is unambiguously a range.
227+
# `arch` and `count` are the only `CPUSpec` fields, so a mapping of `min`/`max` is
228+
# unambiguously a count range. A subset rather than exactly `{"min", "max"}`, because a
229+
# half-open range may omit the other key.
235230
if isinstance(v, Mapping) and v and v.keys() <= {"min", "max"}:
236231
return {"arch": None, "count": v}
237232
return v
@@ -395,20 +390,7 @@ def _parse(cls, v: Any) -> Any:
395390

396391

397392
class ResourcesSpec(CoreModel):
398-
# TODO: remove `Range[int]` in 0.20. It is kept only for backward compatibility.
399-
cpu: Annotated[
400-
Union[
401-
# `Tag` only names the arm in validation errors. Without it the `loc` of a bad `cpu`
402-
# spells out the whole wrapped schema —
403-
# `cpu.function-before[parse(), function-before[parse(), ... CPUSpec]].count` — which
404-
# is what `dstack apply` shows the user.
405-
Annotated[CPUSpec, Tag("CPUSpec")],
406-
Annotated[Range[int], Tag("Range[int]")],
407-
],
408-
# `CPUSpec` and `Range[int]` both accept a bare int/str, so the arm has to be picked by
409-
# declaration order rather than by pydantic v2's "smart" union resolution.
410-
Field(description="The CPU requirements", union_mode="left_to_right"),
411-
] = CPUSpec()
393+
cpu: Annotated[CPUSpec, Field(description="The CPU requirements")] = CPUSpec()
412394
memory: Annotated[Range[Memory], Field(description="The RAM size (e.g., `8GB`)")] = (
413395
DEFAULT_MEMORY_SIZE
414396
)
@@ -435,8 +417,7 @@ def unconstrained(cls) -> "ResourcesSpec":
435417
)
436418

437419
def pretty_format(self) -> str:
438-
# TODO: Remove in 0.20. Use self.cpu directly
439-
cpu = CPUSpec.model_validate(self.cpu)
420+
cpu = self.cpu
440421
resources: Dict[str, Any] = dict(cpu_arch=cpu.arch, cpus=cpu.count, memory=self.memory)
441422
if self.gpu:
442423
gpu = self.gpu
@@ -452,18 +433,3 @@ def pretty_format(self) -> str:
452433
resources.update(disk_size=self.disk.size)
453434
res = pretty_resources(**resources)
454435
return res
455-
456-
@model_serializer(mode="wrap")
457-
def _serialize(self, handler: SerializerFunctionWrapHandler) -> Dict[str, Any]:
458-
res = handler(self)
459-
self._update_serialized_cpu(res)
460-
return res
461-
462-
# TODO: Remove in 0.20. Added for backward compatibility.
463-
def _update_serialized_cpu(self, values: Dict):
464-
cpu = values.get("cpu")
465-
if cpu:
466-
arch = cpu.get("arch")
467-
count = cpu.get("count")
468-
if count and arch in [None, gpuhunt.CPUArchitecture.X86.value]:
469-
values["cpu"] = count

src/dstack/_internal/server/services/requirements/combine.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ def _combine_tags(value1: dict[str, str], value2: dict[str, str]) -> dict[str, s
171171

172172
def _combine_resources(value1: ResourcesSpec, value2: ResourcesSpec) -> ResourcesSpec:
173173
return ResourcesSpec(
174-
cpu=_combine_cpu(value1.cpu, value2.cpu), # type: ignore[attr-defined]
174+
cpu=_combine_cpu(value1.cpu, value2.cpu),
175175
memory=_combine_memory(value1.memory, value2.memory),
176176
shm_size=_combine_shm_size_optional(value1.shm_size, value2.shm_size),
177177
gpu=_combine_gpu_optional(value1.gpu, value2.gpu),

0 commit comments

Comments
 (0)