Skip to content

Commit 6ffb749

Browse files
fregataaclaude
andcommitted
refactor(BA-7353): define the kernel payload beside the events that carry it
Review follow-up. A payload that only ever travels on a kernel event does not need a package of its own — it belongs with the other kernel event types. - Drop `common/interchange/` and define the payload in `events/event_types/kernel/types.py`, next to `KernelLifecycleEventReason`. Revert `common/AGENTS.md` and `common/KNOWLEDGE.md`, which only described that package. - Give each level of the occupancy its own model with a mapping field rather than nesting three dicts inline: `KernelOccupancy.devices` (by `DeviceName`) → `DeviceOccupancy.slots` (by `ResourceSlotName`) → `SlotOccupancy.amounts` (by `DeviceId`). A device supplies more than one slot when it is metered along more than one axis, as `cuda` does with `cuda.device` and `cuda.shares`. - Name it after what the manager does with it. It records the value as the kernel's occupancy, so the field is `occupancy` and the per-slot sum is `slot_totals`. - Key slots by `ResourceSlotName` throughout, not the legacy `SlotName`. `slot_totals` stays a property rather than a `computed_field`: a computed field is written into the payload beside the occupancy it is derived from, where nothing reads it — a receiver recomputes it — and it can disagree with the value next to it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent fefbdf6 commit 6ffb749

10 files changed

Lines changed: 255 additions & 282 deletions

File tree

src/ai/backend/common/AGENTS.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@
2626
| `common/events/` | Event type definitions and dispatcher — see existing `AbstractEvent` subclasses |
2727
| `common/bgtask/` | Background task framework — extend `BaseBackgroundTaskHandler` |
2828
| `common/dto/` | Inter-component DTOs — see `common/dto/AGENTS.md` |
29-
| `common/interchange/` | Payloads one component sends another (event/RPC bodies) — see `common/interchange/AGENTS.md` |
30-
| `common/schema/` | Pydantic types the manager persists as a JSON DB column — see `common/schema/AGENTS.md` |
3129
| `common/exception.py` | Root `BackendAIError` and `ErrorCode` — all component exceptions inherit from here |
3230
| `common/types.py` | Common base types used across layers |
3331

src/ai/backend/common/KNOWLEDGE.md

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -27,20 +27,3 @@ types belong here is the core of this document.
2727
- The second criterion: types that, if placed in a higher layer, **would force an upward import** (`common/schema/` states this explicitly and is used only by the manager).
2828
- Single-consumer subpackages (`stage/` — agent-only, `resilience/` — manager-only) are not defects.
2929
- "It is convenient to put it somewhere neutral" is not a membership reason.
30-
31-
## Shared types are split by what crosses, not by who owns them
32-
33-
A type shared by two components still has to land in one of four places, and the
34-
question that separates them is what boundary it crosses:
35-
36-
| Package | Crosses |
37-
|---------|---------|
38-
| `interchange/` | A component boundary as a serialized body (event/RPC payload) |
39-
| `dto/` | An API boundary, per target component |
40-
| `schema/` | The persistence boundary — the manager stores it as a JSON column |
41-
| `data/` | Nothing; it is passed within a component |
42-
43-
`interchange/` exists because the first row had no home: `dto/` is organized by target
44-
component, and a payload has two sides, so neither side owns it. Its membership test is
45-
the consumer, not the producer — a field the receiving component never reads belongs in
46-
the producing component, not on the wire.

src/ai/backend/common/events/event_types/kernel/types.py

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
import enum
2-
from typing import Self
2+
from decimal import Decimal
3+
from typing import Annotated, Self
4+
5+
from pydantic import Field
6+
7+
from ai.backend.common.identifier.resource_slot import ResourceSlotName
8+
from ai.backend.common.types import (
9+
BackendAISchema,
10+
ContainerId,
11+
DeviceId,
12+
DeviceName,
13+
ResourceSlotEntry,
14+
ServicePortProtocols,
15+
)
316

417

518
class KernelLifecycleEventReason(enum.StrEnum):
@@ -43,3 +56,94 @@ def from_value(cls, value: str | None) -> Self | None:
4356
except ValueError:
4457
pass
4558
return None
59+
60+
61+
type AllocationAmount = Annotated[
62+
Decimal,
63+
# An unbounded allocation is expressed as `Decimal("Infinity")`, which the default
64+
# `Decimal` constraint rejects. It survives the wire as the string "Infinity".
65+
Field(allow_inf_nan=True),
66+
]
67+
68+
69+
class SlotOccupancy(BackendAISchema):
70+
"""How much of one slot each individual device supplies."""
71+
72+
amounts: dict[DeviceId, AllocationAmount] = Field(default_factory=dict)
73+
74+
75+
class DeviceOccupancy(BackendAISchema):
76+
"""The slots one device supplies.
77+
78+
A device supplies more than one slot when it is metered along more than one axis —
79+
`cuda` supplies both `cuda.device` and `cuda.shares`.
80+
"""
81+
82+
slots: dict[ResourceSlotName, SlotOccupancy] = Field(default_factory=dict)
83+
84+
85+
class KernelOccupancy(BackendAISchema):
86+
"""
87+
The resources the kernel occupies, attributed to the devices supplying them.
88+
89+
`DeviceName` names a device (`cuda`) and `DeviceId` one of its units (`0`), so the
90+
two levels keyed by a device are not the same thing.
91+
"""
92+
93+
devices: dict[DeviceName, DeviceOccupancy] = Field(default_factory=dict)
94+
95+
@property
96+
def slot_totals(self) -> list[ResourceSlotEntry]:
97+
"""
98+
The per-device amounts summed per slot — what a caller records as occupancy.
99+
100+
A slot supplied by no device is left out rather than reported as zero, which is
101+
the distinction a caller storing the result depends on.
102+
103+
Not a `computed_field`: it would be written into the payload beside the
104+
occupancy it is derived from, and a receiver recomputes it anyway.
105+
"""
106+
totals: list[ResourceSlotEntry] = []
107+
for device in self.devices.values():
108+
for slot_name, slot in device.slots.items():
109+
if not slot.amounts:
110+
continue
111+
total = sum(slot.amounts.values(), Decimal(0))
112+
totals.append(ResourceSlotEntry(resource_type=slot_name, quantity=str(total)))
113+
return totals
114+
115+
116+
class DeviceCapacity(BackendAISchema):
117+
mem: int | None = None
118+
proc: int | None = None
119+
120+
121+
class AttachedDevice(BackendAISchema):
122+
device_id: DeviceId
123+
model_name: str
124+
data: DeviceCapacity = Field(default_factory=DeviceCapacity)
125+
126+
127+
class ServicePortInfo(BackendAISchema):
128+
name: str
129+
protocol: ServicePortProtocols
130+
container_ports: list[int] = Field(default_factory=list)
131+
host_ports: list[int | None] = Field(default_factory=list)
132+
is_inference: bool = False
133+
134+
135+
class KernelCreationInfo(BackendAISchema):
136+
"""
137+
What the agent reports about a kernel once its container is up.
138+
139+
This is the contract with the manager, not a rendering of the agent's own resource
140+
spec: it carries the facts the manager records against the kernel and nothing else.
141+
"""
142+
143+
container_id: ContainerId
144+
kernel_host: str
145+
repl_in_port: int
146+
repl_out_port: int
147+
service_ports: list[ServicePortInfo] = Field(default_factory=list)
148+
attached_devices: dict[DeviceName, list[AttachedDevice]] = Field(default_factory=dict)
149+
occupancy: KernelOccupancy = Field(default_factory=KernelOccupancy)

src/ai/backend/common/interchange/AGENTS.md

Lines changed: 0 additions & 34 deletions
This file was deleted.

src/ai/backend/common/interchange/CLAUDE.md

Lines changed: 0 additions & 1 deletion
This file was deleted.

src/ai/backend/common/interchange/__init__.py

Whitespace-only changes.

src/ai/backend/common/interchange/kernel.py

Lines changed: 0 additions & 91 deletions
This file was deleted.

0 commit comments

Comments
 (0)