Skip to content

Commit 2904699

Browse files
committed
fix: Add skeleton
1 parent 57291b6 commit 2904699

File tree

17 files changed

+641
-37
lines changed

17 files changed

+641
-37
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
python_sources(name="src")
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import uuid
2+
from dataclasses import dataclass
3+
from datetime import datetime
4+
from typing import TYPE_CHECKING, Optional
5+
6+
from ai.backend.common.types import (
7+
ClusterMode,
8+
ResourceSlot,
9+
SessionResult,
10+
SessionTypes,
11+
VFolderMount,
12+
)
13+
14+
if TYPE_CHECKING:
15+
from ai.backend.manager.models.kernel import KernelStatus
16+
17+
18+
@dataclass
19+
class KernelData:
20+
# --- identity & session ---
21+
id: uuid.UUID
22+
session_id: uuid.UUID
23+
session_creation_id: Optional[str]
24+
session_name: Optional[str]
25+
session_type: SessionTypes
26+
27+
# --- cluster info ---
28+
cluster_mode: ClusterMode
29+
cluster_size: int
30+
cluster_role: str
31+
cluster_idx: int
32+
local_rank: int
33+
cluster_hostname: str
34+
35+
# --- uid / gid ---
36+
uid: Optional[int]
37+
main_gid: Optional[int]
38+
gids: Optional[list[int]]
39+
40+
# --- ownership / auth ---
41+
scaling_group: Optional[str]
42+
agent: Optional[str]
43+
agent_addr: Optional[str]
44+
domain_name: str
45+
group_id: uuid.UUID
46+
user_uuid: uuid.UUID
47+
access_key: Optional[str]
48+
49+
# --- image & registry ---
50+
image: Optional[str]
51+
architecture: str
52+
registry: Optional[str]
53+
tag: Optional[str]
54+
container_id: Optional[str]
55+
56+
# --- resources ---
57+
occupied_slots: ResourceSlot
58+
requested_slots: ResourceSlot
59+
occupied_shares: dict
60+
environ: Optional[list[str]]
61+
mounts: Optional[list[str]]
62+
mount_map: dict
63+
vfolder_mounts: Optional[list[VFolderMount]]
64+
attached_devices: dict
65+
resource_opts: dict
66+
bootstrap_script: Optional[str]
67+
68+
# --- networking ---
69+
kernel_host: Optional[str]
70+
repl_in_port: int
71+
repl_out_port: int
72+
stdin_port: int
73+
stdout_port: int
74+
service_ports: Optional[dict]
75+
preopen_ports: Optional[list[int]]
76+
use_host_network: bool
77+
78+
# --- lifecycle timestamps ---
79+
created_at: datetime
80+
terminated_at: Optional[datetime]
81+
starts_at: Optional[datetime]
82+
83+
# --- runtime status ---
84+
status: "KernelStatus"
85+
status_changed: Optional[datetime]
86+
status_info: Optional[str]
87+
status_data: Optional[dict]
88+
status_history: Optional[dict]
89+
90+
# --- callbacks & commands ---
91+
callback_url: Optional[str]
92+
startup_command: Optional[str]
93+
94+
# --- result & logs ---
95+
result: SessionResult
96+
internal_data: Optional[dict]
97+
container_log: Optional[bytes]
98+
99+
# --- metrics ---
100+
num_queries: int
101+
last_stat: Optional[dict]

src/ai/backend/manager/data/session/types.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from ai.backend.common.data.vfolder.types import VFolderMountData
77
from ai.backend.common.types import (
8+
AccessKey,
89
ClusterMode,
910
SessionResult,
1011
SessionTypes,
@@ -32,9 +33,10 @@ class SessionData:
3233
created_at: datetime
3334
status: "SessionStatus"
3435
result: SessionResult
36+
num_queries: int
3537
creation_id: Optional[str]
3638
name: Optional[str]
37-
access_key: Optional[str]
39+
access_key: Optional[AccessKey]
3840
agent_ids: Optional[list[str]]
3941
images: Optional[list[str]]
4042
tag: Optional[str]
@@ -52,7 +54,9 @@ class SessionData:
5254
status_history: Optional[dict[str, Any]]
5355
callback_url: Optional[str]
5456
startup_command: Optional[str]
55-
num_queries: Optional[int]
5657
last_stat: Optional[dict[str, Any]]
5758
network_type: Optional[NetworkType]
5859
network_id: Optional[str]
60+
61+
# Loaded from relationship
62+
service_ports: Optional[str]

src/ai/backend/manager/models/base.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import yarl
3838
from aiodataloader import DataLoader
3939
from aiotools import apartial
40+
from dateutil.parser import isoparse
4041
from graphene.types import Scalar
4142
from graphene.types.scalars import MAX_INT, MIN_INT
4243
from graphql import Undefined
@@ -1355,6 +1356,13 @@ async def populate_fixture(
13551356
async with engine.begin() as conn:
13561357
# Apply typedecorator manually for required columns
13571358
for col in table.columns:
1359+
if isinstance(col.type, sa.sql.sqltypes.DateTime):
1360+
for row in rows:
1361+
if col.name in row:
1362+
if row[col.name] is not None:
1363+
row[col.name] = isoparse(row[col.name])
1364+
else:
1365+
row[col.name] = None
13581366
if isinstance(col.type, EnumType):
13591367
for row in rows:
13601368
if col.name in row:
@@ -1363,12 +1371,19 @@ async def populate_fixture(
13631371
for row in rows:
13641372
if col.name in row:
13651373
row[col.name] = col.type._enum_cls(row[col.name])
1366-
elif isinstance(
1367-
col.type, (StructuredJSONObjectColumn, StructuredJSONObjectListColumn)
1368-
):
1374+
elif isinstance(col.type, (StructuredJSONObjectColumn)):
13691375
for row in rows:
13701376
if col.name in row:
13711377
row[col.name] = col.type._schema.from_json(row[col.name])
1378+
elif isinstance(col.type, (StructuredJSONObjectListColumn)):
1379+
for row in rows:
1380+
if col.name in row and row[col.name] is not None:
1381+
row[col.name] = [
1382+
item
1383+
if isinstance(item, col.type._schema)
1384+
else col.type._schema.from_json(item)
1385+
for item in row[col.name]
1386+
]
13721387

13731388
match op_mode:
13741389
case FixtureOpModes.INSERT:

src/ai/backend/manager/models/gql_models/session.py

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
SessionResult,
3232
VFolderMount,
3333
)
34+
from ai.backend.manager.data.session.types import SessionData
3435
from ai.backend.manager.defs import DEFAULT_ROLE
3536
from ai.backend.manager.idle import ReportInfo
3637
from ai.backend.manager.models.kernel import KernelRow
@@ -343,6 +344,65 @@ def from_row(
343344
result.permissions = [] if permissions is None else permissions
344345
return result
345346

347+
@classmethod
348+
def from_dataclass(
349+
cls,
350+
ctx: GraphQueryContext,
351+
session_data: SessionData,
352+
*,
353+
permissions: Optional[Iterable[ComputeSessionPermission]] = None,
354+
) -> Self:
355+
status_history = session_data.status_history or {}
356+
raw_scheduled_at = status_history.get(SessionStatus.SCHEDULED.name)
357+
if not session_data.vfolder_mounts:
358+
vfolder_mounts = []
359+
else:
360+
vfolder_mounts = [vf.vfid.folder_id for vf in session_data.vfolder_mounts]
361+
362+
result = cls(
363+
# identity
364+
id=session_data.id, # auto-converted to Relay global ID
365+
row_id=session_data.id,
366+
tag=session_data.tag,
367+
name=session_data.name,
368+
type=session_data.session_type,
369+
cluster_template=None,
370+
cluster_mode=session_data.cluster_mode,
371+
cluster_size=session_data.cluster_size,
372+
priority=session_data.priority,
373+
# ownership
374+
domain_name=session_data.domain_name,
375+
project_id=session_data.group_id,
376+
user_id=session_data.user_uuid,
377+
access_key=session_data.access_key,
378+
# status
379+
status=session_data.status.name,
380+
# status_changed=row.status_changed, # FIXME: generated attribute
381+
status_info=session_data.status_info,
382+
status_data=session_data.status_data,
383+
status_history=status_history,
384+
created_at=session_data.created_at,
385+
starts_at=session_data.starts_at,
386+
terminated_at=session_data.terminated_at,
387+
scheduled_at=datetime.fromisoformat(raw_scheduled_at)
388+
if raw_scheduled_at is not None
389+
else None,
390+
startup_command=session_data.startup_command,
391+
result=session_data.result.name,
392+
# resources
393+
agent_ids=session_data.agent_ids,
394+
scaling_group=session_data.scaling_group_name,
395+
vfolder_mounts=vfolder_mounts,
396+
occupied_slots=session_data.occupying_slots,
397+
requested_slots=session_data.requested_slots,
398+
image_references=session_data.images,
399+
service_ports=session_data.service_ports,
400+
# statistics
401+
num_queries=session_data.num_queries,
402+
)
403+
result.permissions = [] if permissions is None else permissions
404+
return result
405+
346406
async def resolve_idle_checks(self, info: graphene.ResolveInfo) -> dict[str, Any] | None:
347407
graph_ctx: GraphQueryContext = info.context
348408
loader = graph_ctx.dataloader_manager.get_loader_by_func(
@@ -708,7 +768,7 @@ async def mutate_and_get_payload(
708768
)
709769

710770
return ModifyComputeSession(
711-
ComputeSessionNode.from_row(graph_ctx, result.session_row),
771+
ComputeSessionNode.from_dataclass(graph_ctx, result.session_data),
712772
input.get("client_mutation_id"),
713773
)
714774

src/ai/backend/manager/models/kernel.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
VFolderMount,
4040
)
4141
from ai.backend.logging import BraceStyleAdapter
42+
from ai.backend.manager.data.kernel.types import KernelData
4243

4344
from ..api.exceptions import (
4445
BackendError,
@@ -542,6 +543,73 @@ class KernelRow(Base):
542543
group_row = relationship("GroupRow", back_populates="kernels")
543544
user_row = relationship("UserRow", back_populates="kernels")
544545

546+
@classmethod
547+
def from_dataclass(cls, data: KernelData) -> KernelRow:
548+
raise NotImplementedError("KernelRow.from_dataclass() is not implemented.")
549+
550+
def to_dataclass(self) -> KernelData:
551+
return KernelData(
552+
id=self.id,
553+
session_id=self.session_id,
554+
session_creation_id=self.session_creation_id,
555+
session_name=self.session_name,
556+
session_type=self.session_type,
557+
cluster_mode=self.cluster_mode,
558+
cluster_size=self.cluster_size,
559+
cluster_role=self.cluster_role,
560+
cluster_idx=self.cluster_idx,
561+
local_rank=self.local_rank,
562+
cluster_hostname=self.cluster_hostname,
563+
uid=self.uid,
564+
main_gid=self.main_gid,
565+
gids=self.gids,
566+
scaling_group=self.scaling_group,
567+
agent=self.agent_row.to_dataclass() if getattr(self, "agent_row", None) else None,
568+
agent_addr=self.agent_addr,
569+
domain_name=self.domain_name,
570+
group_id=self.group_id,
571+
user_uuid=self.user_uuid,
572+
access_key=self.access_key,
573+
image=self.image,
574+
architecture=self.architecture,
575+
registry=self.registry,
576+
tag=self.tag,
577+
container_id=self.container_id,
578+
occupied_slots=self.occupied_slots,
579+
requested_slots=self.requested_slots,
580+
occupied_shares=self.occupied_shares,
581+
environ=self.environ,
582+
mounts=self.mounts,
583+
mount_map=self.mount_map,
584+
vfolder_mounts=self.vfolder_mounts,
585+
attached_devices=self.attached_devices,
586+
resource_opts=self.resource_opts,
587+
bootstrap_script=self.bootstrap_script,
588+
kernel_host=self.kernel_host or self.agent_addr,
589+
repl_in_port=self.repl_in_port,
590+
repl_out_port=self.repl_out_port,
591+
stdin_port=self.stdin_port,
592+
stdout_port=self.stdout_port,
593+
service_ports=self.service_ports,
594+
preopen_ports=self.preopen_ports,
595+
use_host_network=self.use_host_network,
596+
created_at=self.created_at,
597+
terminated_at=self.terminated_at,
598+
starts_at=self.starts_at,
599+
status=self.status,
600+
status_changed=self.status_changed,
601+
status_info=self.status_info,
602+
status_data=self.status_data,
603+
status_history=self.status_history,
604+
callback_url=str(self.callback_url) if self.callback_url else None,
605+
startup_command=self.startup_command,
606+
result=self.result,
607+
internal_data=self.internal_data,
608+
container_log=self.container_log,
609+
num_queries=self.num_queries,
610+
last_stat=self.last_stat,
611+
)
612+
545613
@property
546614
def image_ref(self) -> ImageRef | None:
547615
return self.image_row.image_ref if self.image_row else None

0 commit comments

Comments
 (0)