Skip to content

Commit 8a65349

Browse files
fregataaclaude
andcommitted
refactor(BA-7315): carry the kernel resource spec as domain types
`KernelResourceSpecData` described itself entirely in built-ins, so nothing in the payload said what a key or a mount actually was. - Type the fields as what they hold: `list[ResourceSlotEntry]` for slots, `dict[DeviceName, dict[ResourceSlotName, dict[DeviceId, str]]]` for allocations, and `list[MountData]` for mounts, which keeps a mount structured instead of flattening it to its `str()` form. `KernelCreationInfo` follows with `KernelId`, `ContainerId`, `DeviceId` and `DeviceName`. - `MountData` mirrors the agent's `Mount` and declares no defaults, so a producer states every field rather than inheriting one. - Omit a slot holding no device allocation from `to_resource_slot()` instead of recording it as zero. This restores what the manager helper it replaced did — a slot absent and a slot present at zero are not the same occupancy. - Drop `KernelResourceSpec.to_json_serializable_dict()` / `to_json()`, whose only caller was a log line, and `AgentRegistry.convert_resource_spec_to_resource_slot()`, which had no caller at all. Slot aggregation now has one implementation rather than three. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent d7adbc2 commit 8a65349

8 files changed

Lines changed: 153 additions & 116 deletions

File tree

src/ai/backend/agent/agent.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2718,7 +2718,7 @@ async def create_kernel(
27182718
"create_kernel(kernel:{}, session:{}) resource spec prepared: {}",
27192719
kernel_id,
27202720
session_id,
2721-
resource_spec.to_json(),
2721+
resource_spec.to_data().model_dump_json(),
27222722
)
27232723

27242724
# Mount backend-specific intrinsic mounts (e.g., scratch directories)
@@ -3314,7 +3314,7 @@ async def create_kernel(
33143314
# Finally we are done.
33153315
creation_info = KernelCreationInfo(
33163316
id=kernel_id,
3317-
container_id=str(kernel_obj["container_id"]),
3317+
container_id=ContainerId(str(kernel_obj["container_id"])),
33183318
kernel_host=str(kernel_obj["kernel_host"]),
33193319
repl_in_port=kernel_obj["repl_in_port"],
33203320
repl_out_port=kernel_obj["repl_out_port"],
@@ -3328,7 +3328,7 @@ async def create_kernel(
33283328
],
33293329
resource_spec=resource_spec.to_data(),
33303330
attached_devices={
3331-
str(dev_name): [
3331+
dev_name: [
33323332
AttachedDeviceData.model_validate(device) for device in devices
33333333
]
33343334
for dev_name, devices in attached_devices.items()

src/ai/backend/agent/resources.py

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,9 @@
4040
InvalidResourceConfigError,
4141
ResourceOverAllocatedError,
4242
)
43-
from ai.backend.common.data.kernel.types import KernelResourceSpecData
43+
from ai.backend.common.data.kernel.types import KernelResourceSpecData, MountData
4444
from ai.backend.common.etcd import AsyncEtcd
45+
from ai.backend.common.identifier.resource_slot import ResourceSlotName
4546
from ai.backend.common.json import dump_json_str, load_json
4647
from ai.backend.common.plugin import AbstractPlugin, BasePluginContext
4748
from ai.backend.common.types import (
@@ -56,6 +57,7 @@
5657
MountPermission,
5758
MountTypes,
5859
ResourceSlot,
60+
ResourceSlotEntry,
5961
SlotName,
6062
SlotTypes,
6163
aobject,
@@ -271,37 +273,36 @@ def _format_alloc(slot_name: SlotName, alloc: Decimal) -> str:
271273

272274
def to_data(self) -> KernelResourceSpecData:
273275
"""
274-
Render this spec with JSON-representable leaves, as events and logs carry it.
276+
Render this spec as the value events and logs carry it as.
275277
"""
276278
return KernelResourceSpecData(
277-
slots={
278-
str(slot_name): self._format_alloc(SlotName(slot_name), alloc)
279-
for slot_name, alloc in self.slots.items()
280-
},
279+
slots=ResourceSlotEntry.from_resource_slot(self.slots),
281280
allocations={
282-
str(dev_name): {
283-
str(slot_name): {
284-
str(dev_id): self._format_alloc(slot_name, alloc)
281+
dev_name: {
282+
ResourceSlotName(str(slot_name)): {
283+
dev_id: self._format_alloc(slot_name, alloc)
285284
for dev_id, alloc in per_device_alloc.items()
286285
}
287286
for slot_name, per_device_alloc in dev_alloc.items()
288287
}
289288
for dev_name, dev_alloc in self.allocations.items()
290289
},
291290
scratch_disk_size=self.scratch_disk_size,
292-
mounts=[str(mount) for mount in self.mounts],
291+
mounts=[
292+
MountData(
293+
type=mount.type,
294+
source=mount.source,
295+
target=mount.target,
296+
permission=mount.permission,
297+
)
298+
for mount in self.mounts
299+
],
293300
unified_devices=[
294-
(str(device_name), str(slot_name))
301+
(device_name, ResourceSlotName(str(slot_name)))
295302
for device_name, slot_name in self.unified_devices
296303
],
297304
)
298305

299-
def to_json_serializable_dict(self) -> Mapping[str, Any]:
300-
return self.to_data().model_dump(mode="json")
301-
302-
def to_json(self) -> str:
303-
return dump_json_str(self.to_json_serializable_dict())
304-
305306
@classmethod
306307
def __get_pydantic_core_schema__(
307308
cls, source_type: Any, handler: GetCoreSchemaHandler

src/ai/backend/common/data/kernel/types.py

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
from __future__ import annotations
22

3-
import uuid
43
from decimal import Decimal
4+
from pathlib import Path
55

66
from pydantic import Field
77

8+
from ai.backend.common.identifier.resource_slot import ResourceSlotName
89
from ai.backend.common.types import (
910
BackendAISchema,
1011
BinarySize,
12+
ContainerId,
13+
DeviceId,
14+
DeviceName,
15+
KernelId,
16+
MountPermission,
17+
MountTypes,
1118
ResourceSlot,
19+
ResourceSlotEntry,
1220
ServicePortProtocols,
1321
SlotName,
1422
)
@@ -18,6 +26,7 @@
1826
"DeviceCapacityData",
1927
"KernelCreationInfo",
2028
"KernelResourceSpecData",
29+
"MountData",
2130
"ServicePortData",
2231
)
2332

@@ -28,7 +37,7 @@ class DeviceCapacityData(BackendAISchema):
2837

2938

3039
class AttachedDeviceData(BackendAISchema):
31-
device_id: str
40+
device_id: DeviceId
3241
model_name: str
3342
data: DeviceCapacityData = Field(default_factory=DeviceCapacityData)
3443

@@ -41,30 +50,50 @@ class ServicePortData(BackendAISchema):
4150
is_inference: bool = False
4251

4352

53+
class MountData(BackendAISchema):
54+
"""
55+
A mount as the agent realized it on the container.
56+
57+
Mirrors the agent's `Mount`, minus the container-runtime `opts` it never carries on
58+
a resource spec.
59+
"""
60+
61+
type: MountTypes
62+
source: Path | None
63+
target: Path
64+
permission: MountPermission
65+
66+
4467
class KernelResourceSpecData(BackendAISchema):
4568
"""
46-
The agent's resource spec for one kernel, with every leaf JSON-representable.
69+
The agent's resource spec for one kernel.
4770
48-
Allocations and slot amounts are decimal strings rather than numbers: they are
49-
read back as `Decimal`, and a float would lose the exact value on the way.
71+
Per-device allocation amounts are decimal strings rather than numbers: they are read
72+
back as `Decimal`, and a float would lose the exact value on the way.
5073
"""
5174

52-
slots: dict[str, str] = Field(default_factory=dict)
53-
allocations: dict[str, dict[str, dict[str, str]]] = Field(default_factory=dict)
75+
slots: list[ResourceSlotEntry] = Field(default_factory=list)
76+
allocations: dict[DeviceName, dict[ResourceSlotName, dict[DeviceId, str]]] = Field(
77+
default_factory=dict
78+
)
5479
scratch_disk_size: int = 0
55-
mounts: list[str] = Field(default_factory=list)
56-
unified_devices: list[tuple[str, str]] = Field(default_factory=list)
80+
mounts: list[MountData] = Field(default_factory=list)
81+
unified_devices: list[tuple[DeviceName, ResourceSlotName]] = Field(default_factory=list)
5782

5883
def to_resource_slot(self) -> ResourceSlot:
5984
"""
6085
Sum the per-device allocations into the resource slot the manager accounts by.
6186
6287
A byte-valued amount arrives suffixed (`"4g"`), so it is parsed rather than
63-
read as a plain decimal.
88+
read as a plain decimal. A slot holding no device allocation is left out rather
89+
than recorded as zero, which is the distinction a caller reading the result back
90+
as occupancy depends on.
6491
"""
6592
slots = ResourceSlot()
6693
for alloc_map in self.allocations.values():
6794
for slot_name, allocation_by_device in alloc_map.items():
95+
if not allocation_by_device:
96+
continue
6897
total = Decimal(0)
6998
for amount in allocation_by_device.values():
7099
if amount and BinarySize.suffix_map.get(amount[-1].lower()) is not None:
@@ -80,8 +109,8 @@ class KernelCreationInfo(BackendAISchema):
80109
What the agent reports about a kernel once its container is up.
81110
"""
82111

83-
id: uuid.UUID
84-
container_id: str
112+
id: KernelId
113+
container_id: ContainerId
85114
kernel_host: str
86115
repl_in_port: int
87116
repl_out_port: int
@@ -91,4 +120,4 @@ class KernelCreationInfo(BackendAISchema):
91120
agent_addr: str
92121
service_ports: list[ServicePortData] = Field(default_factory=list)
93122
resource_spec: KernelResourceSpecData = Field(default_factory=KernelResourceSpecData)
94-
attached_devices: dict[str, list[AttachedDeviceData]] = Field(default_factory=dict)
123+
attached_devices: dict[DeviceName, list[AttachedDeviceData]] = Field(default_factory=dict)

src/ai/backend/manager/registry.py

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
Sequence,
1313
)
1414
from datetime import datetime, timedelta
15-
from decimal import Decimal
1615
from typing import (
1716
Any,
1817
cast,
@@ -80,11 +79,9 @@
8079
AbuseReport,
8180
AccessKey,
8281
AgentId,
83-
BinarySize,
8482
ClusterMode,
8583
ClusterSSHKeyPair,
8684
CommitStatus,
87-
DeviceId,
8885
HardwareMetadata,
8986
ImageAlias,
9087
ImageRegistry,
@@ -97,7 +94,6 @@
9794
SessionEnqueueingConfig,
9895
SessionId,
9996
SessionTypes,
100-
SlotName,
10197
)
10298
from ai.backend.common.utils import str_to_timedelta
10399
from ai.backend.logging import BraceStyleAdapter
@@ -1251,29 +1247,6 @@ async def enqueue_session(
12511247
startup_command=startup_command,
12521248
)
12531249

1254-
def convert_resource_spec_to_resource_slot(
1255-
self,
1256-
allocations: Mapping[str, Mapping[SlotName, Mapping[DeviceId, str]]],
1257-
) -> ResourceSlot:
1258-
"""
1259-
Convert per-device resource spec allocations (agent-side format)
1260-
back into a resource slot (manager-side format).
1261-
"""
1262-
slots = ResourceSlot()
1263-
for alloc_map in allocations.values():
1264-
for slot_name, allocation_by_device in alloc_map.items():
1265-
total_allocs: list[Decimal] = []
1266-
for allocation in allocation_by_device.values():
1267-
if (
1268-
isinstance(allocation, (BinarySize, str))
1269-
and BinarySize.suffix_map.get(allocation[-1].lower()) is not None
1270-
):
1271-
total_allocs.append(Decimal(BinarySize.from_str(allocation)))
1272-
else: # maybe Decimal("Infinity"), etc.
1273-
total_allocs.append(Decimal(allocation))
1274-
slots[slot_name] = str(sum(total_allocs))
1275-
return slots
1276-
12771250
async def create_cluster_ssh_keypair(self) -> ClusterSSHKeyPair:
12781251
key = rsa.generate_private_key(
12791252
backend=default_backend(),

0 commit comments

Comments
 (0)