gpu4agents 0.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gpu4agents/__init__.py +5 -0
- gpu4agents/__main__.py +4 -0
- gpu4agents/agent/__init__.py +1 -0
- gpu4agents/agent/install.py +173 -0
- gpu4agents/agent/jobs.py +193 -0
- gpu4agents/agent/mcp.py +646 -0
- gpu4agents/agent/skill/SKILL.md +68 -0
- gpu4agents/cli/__init__.py +0 -0
- gpu4agents/cli/_common.py +131 -0
- gpu4agents/cli/account.py +330 -0
- gpu4agents/cli/agent.py +74 -0
- gpu4agents/cli/inspect.py +207 -0
- gpu4agents/cli/lifecycle.py +127 -0
- gpu4agents/cli/main.py +163 -0
- gpu4agents/cli/providers.py +96 -0
- gpu4agents/cli/run.py +38 -0
- gpu4agents/cli/setup.py +164 -0
- gpu4agents/cli/status.py +224 -0
- gpu4agents/cli/timer.py +143 -0
- gpu4agents/cli/validate.py +57 -0
- gpu4agents/core/__init__.py +0 -0
- gpu4agents/core/beacon.py +133 -0
- gpu4agents/core/config.py +647 -0
- gpu4agents/core/credentials.py +95 -0
- gpu4agents/core/durations.py +47 -0
- gpu4agents/core/engine.py +1335 -0
- gpu4agents/core/errors.py +94 -0
- gpu4agents/core/events.py +117 -0
- gpu4agents/core/execution.py +169 -0
- gpu4agents/core/health.py +62 -0
- gpu4agents/core/hosts.py +58 -0
- gpu4agents/core/model_inspect.py +368 -0
- gpu4agents/core/models.py +313 -0
- gpu4agents/core/paths.py +42 -0
- gpu4agents/core/policy.py +264 -0
- gpu4agents/core/state.py +406 -0
- gpu4agents/core/sweep.py +478 -0
- gpu4agents/core/tool_parsers.py +63 -0
- gpu4agents/providers/__init__.py +4 -0
- gpu4agents/providers/base.py +137 -0
- gpu4agents/providers/registry.py +63 -0
- gpu4agents/providers/vast/__init__.py +3 -0
- gpu4agents/providers/vast/client.py +255 -0
- gpu4agents/providers/vast/config.py +56 -0
- gpu4agents/providers/vast/mapper.py +292 -0
- gpu4agents/providers/vast/provider.py +438 -0
- gpu4agents/runtime/__init__.py +6 -0
- gpu4agents/runtime/base.py +184 -0
- gpu4agents/runtime/vllm.py +295 -0
- gpu4agents/watchdog/__init__.py +3 -0
- gpu4agents/watchdog/agent_body.py +959 -0
- gpu4agents/watchdog/builder.py +248 -0
- gpu4agents-0.5.0.dist-info/METADATA +157 -0
- gpu4agents-0.5.0.dist-info/RECORD +59 -0
- gpu4agents-0.5.0.dist-info/WHEEL +4 -0
- gpu4agents-0.5.0.dist-info/entry_points.txt +2 -0
- gpu4agents-0.5.0.dist-info/licenses/LICENSE +21 -0
- jambu_gpu/__init__.py +9 -0
- jambu_gpu/__main__.py +4 -0
gpu4agents/core/state.py
ADDED
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
"""Observed runtime state (spec section 9).
|
|
2
|
+
|
|
3
|
+
jambu.yaml is the desired-state SoT; ``.gpu/state.json`` is what we last
|
|
4
|
+
observed. This module owns reading/writing it atomically, plus the on-disk
|
|
5
|
+
process lock that keeps two CLI invocations from provisioning twice.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import fcntl
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import tempfile
|
|
14
|
+
import time
|
|
15
|
+
import uuid
|
|
16
|
+
from contextlib import contextmanager
|
|
17
|
+
from dataclasses import asdict, dataclass, field
|
|
18
|
+
from datetime import datetime, timezone
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any, Iterable, Iterator, Optional
|
|
21
|
+
|
|
22
|
+
from .errors import LockBusy, StateError
|
|
23
|
+
from .models import Instance, InstanceState
|
|
24
|
+
from .policy import Workload as PolicyWorkload
|
|
25
|
+
|
|
26
|
+
STATE_VERSION = 1
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def utc_now() -> float:
|
|
30
|
+
return time.time()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def to_iso(ts: Optional[float]) -> Optional[str]:
|
|
34
|
+
if ts is None:
|
|
35
|
+
return None
|
|
36
|
+
return datetime.fromtimestamp(ts, tz=timezone.utc).isoformat(timespec="seconds").replace(
|
|
37
|
+
"+00:00", "Z"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def from_iso(value: Any) -> Optional[float]:
|
|
42
|
+
if value is None or value == "":
|
|
43
|
+
return None
|
|
44
|
+
if isinstance(value, (int, float)):
|
|
45
|
+
return float(value)
|
|
46
|
+
text = str(value).replace("Z", "+00:00")
|
|
47
|
+
try:
|
|
48
|
+
return datetime.fromisoformat(text).timestamp()
|
|
49
|
+
except ValueError as exc:
|
|
50
|
+
raise StateError(f"invalid timestamp in state file: {value!r}") from exc
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class WorkloadRecord:
|
|
55
|
+
id: str
|
|
56
|
+
label: str = ""
|
|
57
|
+
command: list[str] = field(default_factory=list)
|
|
58
|
+
state: str = "running" # running | queued | finished | failed
|
|
59
|
+
started_at: float = field(default_factory=utc_now)
|
|
60
|
+
last_heartbeat: float = field(default_factory=utc_now)
|
|
61
|
+
finished_at: Optional[float] = None
|
|
62
|
+
returncode: Optional[int] = None
|
|
63
|
+
pid: Optional[int] = None
|
|
64
|
+
|
|
65
|
+
def to_policy(self) -> PolicyWorkload:
|
|
66
|
+
return PolicyWorkload(
|
|
67
|
+
id=self.id,
|
|
68
|
+
started_at=self.started_at,
|
|
69
|
+
last_heartbeat=self.last_heartbeat,
|
|
70
|
+
label=self.label,
|
|
71
|
+
state=self.state,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def to_dict(self) -> dict[str, Any]:
|
|
75
|
+
data = asdict(self)
|
|
76
|
+
data["started_at"] = to_iso(self.started_at)
|
|
77
|
+
data["last_heartbeat"] = to_iso(self.last_heartbeat)
|
|
78
|
+
data["finished_at"] = to_iso(self.finished_at)
|
|
79
|
+
return data
|
|
80
|
+
|
|
81
|
+
@classmethod
|
|
82
|
+
def from_dict(cls, data: dict[str, Any]) -> "WorkloadRecord":
|
|
83
|
+
return cls(
|
|
84
|
+
id=data["id"],
|
|
85
|
+
label=data.get("label", ""),
|
|
86
|
+
command=list(data.get("command") or []),
|
|
87
|
+
state=data.get("state", "running"),
|
|
88
|
+
started_at=from_iso(data.get("started_at")) or utc_now(),
|
|
89
|
+
last_heartbeat=from_iso(data.get("last_heartbeat")) or utc_now(),
|
|
90
|
+
finished_at=from_iso(data.get("finished_at")),
|
|
91
|
+
returncode=data.get("returncode"),
|
|
92
|
+
pid=data.get("pid"),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass
|
|
97
|
+
class WatchdogInfo:
|
|
98
|
+
installed: bool = False
|
|
99
|
+
url: Optional[str] = None
|
|
100
|
+
token: Optional[str] = None
|
|
101
|
+
port: Optional[int] = None
|
|
102
|
+
last_contact: Optional[float] = None
|
|
103
|
+
last_error: Optional[str] = None
|
|
104
|
+
|
|
105
|
+
def to_dict(self) -> dict[str, Any]:
|
|
106
|
+
data = asdict(self)
|
|
107
|
+
data["last_contact"] = to_iso(self.last_contact)
|
|
108
|
+
return data
|
|
109
|
+
|
|
110
|
+
@classmethod
|
|
111
|
+
def from_dict(cls, data: dict[str, Any]) -> "WatchdogInfo":
|
|
112
|
+
return cls(
|
|
113
|
+
installed=bool(data.get("installed")),
|
|
114
|
+
url=data.get("url"),
|
|
115
|
+
token=data.get("token"),
|
|
116
|
+
port=data.get("port"),
|
|
117
|
+
last_contact=from_iso(data.get("last_contact")),
|
|
118
|
+
last_error=data.get("last_error"),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass
|
|
123
|
+
class RuntimeState:
|
|
124
|
+
provider: str = ""
|
|
125
|
+
instance_id: Optional[str] = None
|
|
126
|
+
fingerprint: Optional[str] = None
|
|
127
|
+
state: str = InstanceState.DESTROYED.value
|
|
128
|
+
created_at: Optional[float] = None
|
|
129
|
+
last_activity: Optional[float] = None
|
|
130
|
+
locked: bool = False
|
|
131
|
+
locked_until: Optional[float] = None
|
|
132
|
+
lock_reason: str = ""
|
|
133
|
+
starting: bool = False
|
|
134
|
+
model_id: Optional[str] = None
|
|
135
|
+
endpoint: Optional[str] = None
|
|
136
|
+
hourly_cost_usd: Optional[float] = None
|
|
137
|
+
session_cost_usd: float = 0.0
|
|
138
|
+
# Billable-time bookkeeping (spec section 25): session_cost_usd must
|
|
139
|
+
# reflect actual running time, not wall-clock time since creation - a
|
|
140
|
+
# stopped instance costs nothing in compute-hours even if `status` is
|
|
141
|
+
# checked hours later. `billed_seconds` accumulates completed billable
|
|
142
|
+
# periods (e.g. before a stop); `running_since` marks an open one.
|
|
143
|
+
billed_seconds: float = 0.0
|
|
144
|
+
running_since: Optional[float] = None
|
|
145
|
+
# A session is one continuous running period of the instance: it starts
|
|
146
|
+
# at creation and again at every restart. max_lifetime and the session
|
|
147
|
+
# budget are measured from here, so a restarted instance gets a fresh
|
|
148
|
+
# bound instead of being killed for the hours it spent stopped.
|
|
149
|
+
session_started_at: Optional[float] = None
|
|
150
|
+
# When the instance was first observed STOPPED; drives lifecycle.stopped_ttl
|
|
151
|
+
# on providers where a stopped instance still bills (storage).
|
|
152
|
+
stopped_since: Optional[float] = None
|
|
153
|
+
# The key the model server demands (vLLM --api-key): runtime.api_key, or
|
|
154
|
+
# one generated per instance so a public endpoint is never open to anyone.
|
|
155
|
+
api_key: Optional[str] = None
|
|
156
|
+
instance: Optional[Instance] = None
|
|
157
|
+
workloads: dict[str, WorkloadRecord] = field(default_factory=dict)
|
|
158
|
+
watchdog: WatchdogInfo = field(default_factory=WatchdogInfo)
|
|
159
|
+
|
|
160
|
+
# -- helpers ------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
def has_instance(self) -> bool:
|
|
163
|
+
return bool(self.instance_id) and self.state != InstanceState.DESTROYED.value
|
|
164
|
+
|
|
165
|
+
def touch(self) -> None:
|
|
166
|
+
self.last_activity = utc_now()
|
|
167
|
+
|
|
168
|
+
def accrued_billable_seconds(self, now: Optional[float] = None) -> float:
|
|
169
|
+
"""Total seconds actually spent in a billable-compute state.
|
|
170
|
+
|
|
171
|
+
Unlike ``now - created_at``, this excludes time spent stopped -
|
|
172
|
+
checking `status` long after an instance auto-stopped must not keep
|
|
173
|
+
inflating the reported cost.
|
|
174
|
+
"""
|
|
175
|
+
now = now if now is not None else utc_now()
|
|
176
|
+
open_period = max(0.0, now - self.running_since) if self.running_since is not None else 0.0
|
|
177
|
+
return self.billed_seconds + open_period
|
|
178
|
+
|
|
179
|
+
def mark_billable(self, is_billable: bool, now: Optional[float] = None) -> None:
|
|
180
|
+
"""Record a transition into/out of a billable-compute state.
|
|
181
|
+
|
|
182
|
+
Call this every time reconciliation observes the current state -
|
|
183
|
+
it's a no-op unless billable-ness actually changed since the last call.
|
|
184
|
+
"""
|
|
185
|
+
now = now if now is not None else utc_now()
|
|
186
|
+
was_billable = self.running_since is not None
|
|
187
|
+
if is_billable and not was_billable:
|
|
188
|
+
self.running_since = now
|
|
189
|
+
elif not is_billable and was_billable:
|
|
190
|
+
self.billed_seconds += max(0.0, now - self.running_since)
|
|
191
|
+
self.running_since = None
|
|
192
|
+
|
|
193
|
+
def mark_stopped(self, now: Optional[float] = None, ran_at: Iterable[Optional[float]] = ()) -> None:
|
|
194
|
+
"""Record that the instance is (still) stopped; keeps the earliest time.
|
|
195
|
+
|
|
196
|
+
``ran_at`` are times the instance was evidently running. One newer than
|
|
197
|
+
``stopped_since`` means it ran (and stopped again) while this CLI was
|
|
198
|
+
not looking, so the stop clock restarts instead of destroying a fresh stop.
|
|
199
|
+
"""
|
|
200
|
+
now = now if now is not None else utc_now()
|
|
201
|
+
if self.stopped_since is None:
|
|
202
|
+
self.stopped_since = now
|
|
203
|
+
return
|
|
204
|
+
evidence = [
|
|
205
|
+
self.session_started_at,
|
|
206
|
+
self.last_activity,
|
|
207
|
+
self.watchdog.last_contact,
|
|
208
|
+
*ran_at,
|
|
209
|
+
]
|
|
210
|
+
# The state file keeps whole seconds; a sub-second lead is rounding.
|
|
211
|
+
if any(ts is not None and ts > self.stopped_since + 1.0 for ts in evidence):
|
|
212
|
+
self.stopped_since = now
|
|
213
|
+
|
|
214
|
+
def mark_session_start(self, now: Optional[float] = None) -> None:
|
|
215
|
+
self.session_started_at = now if now is not None else utc_now()
|
|
216
|
+
self.stopped_since = None
|
|
217
|
+
|
|
218
|
+
def pending_workloads(self) -> list[WorkloadRecord]:
|
|
219
|
+
return [w for w in self.workloads.values() if w.state in ("running", "queued")]
|
|
220
|
+
|
|
221
|
+
def prune_workloads(self, keep: int = 25) -> None:
|
|
222
|
+
finished = sorted(
|
|
223
|
+
(w for w in self.workloads.values() if w.state not in ("running", "queued")),
|
|
224
|
+
key=lambda w: w.finished_at or w.started_at,
|
|
225
|
+
)
|
|
226
|
+
for record in finished[:-keep] if len(finished) > keep else []:
|
|
227
|
+
self.workloads.pop(record.id, None)
|
|
228
|
+
|
|
229
|
+
def clear_instance(self, state: str = InstanceState.DESTROYED.value) -> None:
|
|
230
|
+
# Close out any open billable period and freeze the final total into
|
|
231
|
+
# session_cost_usd (the historical record of the finished session)
|
|
232
|
+
# before resetting the counters for whatever instance comes next.
|
|
233
|
+
self.mark_billable(False)
|
|
234
|
+
if self.hourly_cost_usd:
|
|
235
|
+
self.session_cost_usd = round(self.hourly_cost_usd * self.billed_seconds / 3600.0, 4)
|
|
236
|
+
self.instance_id = None
|
|
237
|
+
self.instance = None
|
|
238
|
+
self.endpoint = None
|
|
239
|
+
self.api_key = None
|
|
240
|
+
self.state = state
|
|
241
|
+
self.starting = False
|
|
242
|
+
self.locked = False
|
|
243
|
+
self.locked_until = None
|
|
244
|
+
self.watchdog = WatchdogInfo()
|
|
245
|
+
self.billed_seconds = 0.0
|
|
246
|
+
self.running_since = None
|
|
247
|
+
self.session_started_at = None
|
|
248
|
+
self.stopped_since = None
|
|
249
|
+
for record in self.workloads.values():
|
|
250
|
+
if record.state in ("running", "queued"):
|
|
251
|
+
record.state = "failed"
|
|
252
|
+
record.finished_at = utc_now()
|
|
253
|
+
|
|
254
|
+
# -- serialization ------------------------------------------------------
|
|
255
|
+
|
|
256
|
+
def to_dict(self) -> dict[str, Any]:
|
|
257
|
+
return {
|
|
258
|
+
"version": STATE_VERSION,
|
|
259
|
+
"provider": self.provider,
|
|
260
|
+
"instance_id": self.instance_id,
|
|
261
|
+
"fingerprint": self.fingerprint,
|
|
262
|
+
"state": self.state,
|
|
263
|
+
"created_at": to_iso(self.created_at),
|
|
264
|
+
"last_activity": to_iso(self.last_activity),
|
|
265
|
+
"locked": self.locked,
|
|
266
|
+
"locked_until": to_iso(self.locked_until),
|
|
267
|
+
"lock_reason": self.lock_reason,
|
|
268
|
+
"starting": self.starting,
|
|
269
|
+
"model_id": self.model_id,
|
|
270
|
+
"endpoint": self.endpoint,
|
|
271
|
+
"hourly_cost_usd": self.hourly_cost_usd,
|
|
272
|
+
"session_cost_usd": round(self.session_cost_usd, 4),
|
|
273
|
+
"billed_seconds": round(self.billed_seconds, 1),
|
|
274
|
+
"running_since": to_iso(self.running_since),
|
|
275
|
+
"session_started_at": to_iso(self.session_started_at),
|
|
276
|
+
"stopped_since": to_iso(self.stopped_since),
|
|
277
|
+
"api_key": self.api_key,
|
|
278
|
+
"instance": self.instance.to_dict() if self.instance else None,
|
|
279
|
+
"workloads": {k: v.to_dict() for k, v in self.workloads.items()},
|
|
280
|
+
"watchdog": self.watchdog.to_dict(),
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
@classmethod
|
|
284
|
+
def from_dict(cls, data: dict[str, Any]) -> "RuntimeState":
|
|
285
|
+
instance_data = data.get("instance")
|
|
286
|
+
return cls(
|
|
287
|
+
provider=data.get("provider", ""),
|
|
288
|
+
instance_id=data.get("instance_id"),
|
|
289
|
+
fingerprint=data.get("fingerprint"),
|
|
290
|
+
state=data.get("state", InstanceState.DESTROYED.value),
|
|
291
|
+
created_at=from_iso(data.get("created_at")),
|
|
292
|
+
last_activity=from_iso(data.get("last_activity")),
|
|
293
|
+
locked=bool(data.get("locked")),
|
|
294
|
+
locked_until=from_iso(data.get("locked_until")),
|
|
295
|
+
lock_reason=data.get("lock_reason", ""),
|
|
296
|
+
starting=bool(data.get("starting")),
|
|
297
|
+
model_id=data.get("model_id"),
|
|
298
|
+
endpoint=data.get("endpoint"),
|
|
299
|
+
hourly_cost_usd=data.get("hourly_cost_usd"),
|
|
300
|
+
session_cost_usd=float(data.get("session_cost_usd") or 0.0),
|
|
301
|
+
billed_seconds=float(data.get("billed_seconds") or 0.0),
|
|
302
|
+
running_since=from_iso(data.get("running_since")),
|
|
303
|
+
session_started_at=from_iso(data.get("session_started_at")),
|
|
304
|
+
stopped_since=from_iso(data.get("stopped_since")),
|
|
305
|
+
api_key=data.get("api_key"),
|
|
306
|
+
instance=Instance.from_dict(instance_data) if instance_data else None,
|
|
307
|
+
workloads={
|
|
308
|
+
k: WorkloadRecord.from_dict(v) for k, v in (data.get("workloads") or {}).items()
|
|
309
|
+
},
|
|
310
|
+
watchdog=WatchdogInfo.from_dict(data.get("watchdog") or {}),
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
class StateStore:
|
|
315
|
+
"""Atomic reader/writer for ``.gpu/state.json``."""
|
|
316
|
+
|
|
317
|
+
def __init__(self, state_dir: Path) -> None:
|
|
318
|
+
self._depth = 0 # flock is per-open-file-description: keep it reentrant
|
|
319
|
+
self.dir = Path(state_dir)
|
|
320
|
+
self.path = self.dir / "state.json"
|
|
321
|
+
self.lock_path = self.dir / "runtime.lock"
|
|
322
|
+
self.logs_dir = self.dir / "logs"
|
|
323
|
+
|
|
324
|
+
def ensure_dirs(self) -> None:
|
|
325
|
+
self.dir.mkdir(parents=True, exist_ok=True)
|
|
326
|
+
self.logs_dir.mkdir(parents=True, exist_ok=True)
|
|
327
|
+
gitignore = self.dir / ".gitignore"
|
|
328
|
+
if not gitignore.exists():
|
|
329
|
+
gitignore.write_text("*\n")
|
|
330
|
+
|
|
331
|
+
def exists(self) -> bool:
|
|
332
|
+
return self.path.is_file()
|
|
333
|
+
|
|
334
|
+
def load(self) -> RuntimeState:
|
|
335
|
+
if not self.path.is_file():
|
|
336
|
+
return RuntimeState()
|
|
337
|
+
try:
|
|
338
|
+
data = json.loads(self.path.read_text() or "{}")
|
|
339
|
+
except json.JSONDecodeError as exc:
|
|
340
|
+
raise StateError(f"{self.path} is corrupt: {exc}") from exc
|
|
341
|
+
if not data:
|
|
342
|
+
return RuntimeState()
|
|
343
|
+
return RuntimeState.from_dict(data)
|
|
344
|
+
|
|
345
|
+
def save(self, state: RuntimeState) -> None:
|
|
346
|
+
self.ensure_dirs()
|
|
347
|
+
payload = json.dumps(state.to_dict(), indent=2, sort_keys=False) + "\n"
|
|
348
|
+
fd, tmp = tempfile.mkstemp(dir=str(self.dir), prefix=".state-", suffix=".json")
|
|
349
|
+
try:
|
|
350
|
+
with os.fdopen(fd, "w") as handle:
|
|
351
|
+
handle.write(payload)
|
|
352
|
+
handle.flush()
|
|
353
|
+
os.fsync(handle.fileno())
|
|
354
|
+
os.replace(tmp, self.path)
|
|
355
|
+
finally:
|
|
356
|
+
if os.path.exists(tmp):
|
|
357
|
+
os.unlink(tmp)
|
|
358
|
+
|
|
359
|
+
@contextmanager
|
|
360
|
+
def transaction(self, timeout: float = 30.0) -> Iterator[RuntimeState]:
|
|
361
|
+
"""Exclusive read-modify-write. Guarantees `setup` cannot race itself."""
|
|
362
|
+
with self.process_lock(timeout=timeout):
|
|
363
|
+
state = self.load()
|
|
364
|
+
yield state
|
|
365
|
+
self.save(state)
|
|
366
|
+
|
|
367
|
+
@contextmanager
|
|
368
|
+
def process_lock(self, timeout: float = 30.0) -> Iterator[None]:
|
|
369
|
+
if self._depth > 0:
|
|
370
|
+
self._depth += 1
|
|
371
|
+
try:
|
|
372
|
+
yield
|
|
373
|
+
finally:
|
|
374
|
+
self._depth -= 1
|
|
375
|
+
return
|
|
376
|
+
self.ensure_dirs()
|
|
377
|
+
handle = open(self.lock_path, "a+")
|
|
378
|
+
deadline = time.time() + timeout
|
|
379
|
+
try:
|
|
380
|
+
while True:
|
|
381
|
+
try:
|
|
382
|
+
fcntl.flock(handle.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
383
|
+
break
|
|
384
|
+
except BlockingIOError:
|
|
385
|
+
if time.time() >= deadline:
|
|
386
|
+
raise LockBusy(
|
|
387
|
+
"another gpu command is holding the runtime lock "
|
|
388
|
+
f"({self.lock_path}); retry when it finishes"
|
|
389
|
+
) from None
|
|
390
|
+
time.sleep(0.2)
|
|
391
|
+
handle.seek(0)
|
|
392
|
+
handle.truncate()
|
|
393
|
+
handle.write(f"{os.getpid()} {to_iso(utc_now())}\n")
|
|
394
|
+
handle.flush()
|
|
395
|
+
self._depth = 1
|
|
396
|
+
yield
|
|
397
|
+
finally:
|
|
398
|
+
self._depth = 0
|
|
399
|
+
try:
|
|
400
|
+
fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
|
|
401
|
+
finally:
|
|
402
|
+
handle.close()
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def new_workload_id() -> str:
|
|
406
|
+
return uuid.uuid4().hex[:12]
|