poly-hammer-worker 0.1.1.dev9__tar.gz → 0.1.1.dev11__tar.gz
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.
- {poly_hammer_worker-0.1.1.dev9 → poly_hammer_worker-0.1.1.dev11}/PKG-INFO +1 -1
- {poly_hammer_worker-0.1.1.dev9 → poly_hammer_worker-0.1.1.dev11}/poly_hammer_worker/docker_runner.py +15 -25
- {poly_hammer_worker-0.1.1.dev9 → poly_hammer_worker-0.1.1.dev11}/poly_hammer_worker/image_discovery.py +59 -9
- {poly_hammer_worker-0.1.1.dev9 → poly_hammer_worker-0.1.1.dev11}/poly_hammer_worker/worker_loop.py +41 -12
- {poly_hammer_worker-0.1.1.dev9 → poly_hammer_worker-0.1.1.dev11}/pyproject.toml +1 -1
- {poly_hammer_worker-0.1.1.dev9 → poly_hammer_worker-0.1.1.dev11}/.gitignore +0 -0
- {poly_hammer_worker-0.1.1.dev9 → poly_hammer_worker-0.1.1.dev11}/README.md +0 -0
- {poly_hammer_worker-0.1.1.dev9 → poly_hammer_worker-0.1.1.dev11}/poly_hammer_worker/__init__.py +0 -0
- {poly_hammer_worker-0.1.1.dev9 → poly_hammer_worker-0.1.1.dev11}/poly_hammer_worker/api_client.py +0 -0
- {poly_hammer_worker-0.1.1.dev9 → poly_hammer_worker-0.1.1.dev11}/poly_hammer_worker/banner.py +0 -0
- {poly_hammer_worker-0.1.1.dev9 → poly_hammer_worker-0.1.1.dev11}/poly_hammer_worker/cli.py +0 -0
- {poly_hammer_worker-0.1.1.dev9 → poly_hammer_worker-0.1.1.dev11}/poly_hammer_worker/hardware.py +0 -0
- {poly_hammer_worker-0.1.1.dev9 → poly_hammer_worker-0.1.1.dev11}/poly_hammer_worker/logging_config.py +0 -0
- {poly_hammer_worker-0.1.1.dev9 → poly_hammer_worker-0.1.1.dev11}/uv.lock +0 -0
{poly_hammer_worker-0.1.1.dev9 → poly_hammer_worker-0.1.1.dev11}/poly_hammer_worker/docker_runner.py
RENAMED
|
@@ -37,15 +37,6 @@ class DockerRunError(Exception):
|
|
|
37
37
|
super().__init__(f"Container exited with code {exit_code}: {stderr}")
|
|
38
38
|
|
|
39
39
|
|
|
40
|
-
def resolve_image_tag(docker_image: str, cuda_version: str) -> str:
|
|
41
|
-
"""Build the fully-qualified image reference.
|
|
42
|
-
|
|
43
|
-
The backend stores bare image names (e.g. ``ph-hy-motion``).
|
|
44
|
-
This appends the CUDA version tag matching the local GPU.
|
|
45
|
-
"""
|
|
46
|
-
return f"{docker_image}:cuda{cuda_version}"
|
|
47
|
-
|
|
48
|
-
|
|
49
40
|
class InsufficientDiskError(Exception):
|
|
50
41
|
"""Raised when there isn't enough disk space to pull an image."""
|
|
51
42
|
|
|
@@ -300,6 +291,7 @@ def run_inference_container(
|
|
|
300
291
|
job_payload: dict,
|
|
301
292
|
on_progress: Callable[[float], None] | None = None,
|
|
302
293
|
on_status: Callable[[str], None] | None = None,
|
|
294
|
+
on_log: Callable[[str], None] | None = None,
|
|
303
295
|
) -> dict:
|
|
304
296
|
"""Run an inference container and return the result dict.
|
|
305
297
|
|
|
@@ -315,6 +307,8 @@ def run_inference_container(
|
|
|
315
307
|
Called with progress float (0.0–1.0) as the container reports progress.
|
|
316
308
|
on_status:
|
|
317
309
|
Called with human-readable status strings (pulling, running, etc.).
|
|
310
|
+
on_log:
|
|
311
|
+
Called with each line of container output (both stdout and stderr).
|
|
318
312
|
|
|
319
313
|
Returns
|
|
320
314
|
-------
|
|
@@ -324,14 +318,12 @@ def run_inference_container(
|
|
|
324
318
|
------
|
|
325
319
|
DockerRunError — If the container exits non-zero or emits an error message.
|
|
326
320
|
"""
|
|
327
|
-
stdin_bytes = json.dumps(job_payload).encode()
|
|
328
|
-
|
|
329
321
|
if on_status:
|
|
330
322
|
on_status(f"Creating container from {image_ref}")
|
|
331
323
|
|
|
332
324
|
container = client.containers.create(
|
|
333
325
|
image_ref,
|
|
334
|
-
|
|
326
|
+
environment={"JOB_PAYLOAD": json.dumps(job_payload)},
|
|
335
327
|
detach=True,
|
|
336
328
|
device_requests=[
|
|
337
329
|
docker.types.DeviceRequest(count=-1, capabilities=[["gpu"]]),
|
|
@@ -344,26 +336,26 @@ def run_inference_container(
|
|
|
344
336
|
try:
|
|
345
337
|
container.start()
|
|
346
338
|
|
|
347
|
-
# Attach and pipe job JSON into stdin
|
|
348
|
-
sock = container.attach_socket(params={"stdin": 1, "stream": 1})
|
|
349
|
-
sock._sock.sendall(stdin_bytes)
|
|
350
|
-
sock._sock.shutdown(1) # EOF on stdin
|
|
351
|
-
sock.close()
|
|
352
|
-
|
|
353
339
|
if on_status:
|
|
354
340
|
on_status("Inference running …")
|
|
355
341
|
|
|
356
|
-
# Stream stdout for JSON-line progress
|
|
342
|
+
# Stream both stdout and stderr for JSON-line progress + container logs
|
|
343
|
+
stderr_lines: list[str] = []
|
|
357
344
|
for raw_line in container.logs(
|
|
358
|
-
stream=True, follow=True, stdout=True, stderr=
|
|
345
|
+
stream=True, follow=True, stdout=True, stderr=True
|
|
359
346
|
):
|
|
360
347
|
line = raw_line.decode("utf-8", errors="replace").strip()
|
|
361
348
|
if not line:
|
|
362
349
|
continue
|
|
350
|
+
|
|
351
|
+
if on_log:
|
|
352
|
+
on_log(line)
|
|
353
|
+
|
|
363
354
|
try:
|
|
364
355
|
msg = json.loads(line)
|
|
365
356
|
except json.JSONDecodeError:
|
|
366
|
-
|
|
357
|
+
# Non-JSON output is stderr or unstructured stdout
|
|
358
|
+
stderr_lines.append(line)
|
|
367
359
|
continue
|
|
368
360
|
|
|
369
361
|
msg_type = msg.get("type")
|
|
@@ -384,10 +376,8 @@ def run_inference_container(
|
|
|
384
376
|
raise DockerRunError(exit_code, error_msg)
|
|
385
377
|
|
|
386
378
|
if exit_code != 0:
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
)[-2000:]
|
|
390
|
-
raise DockerRunError(exit_code, stderr_logs)
|
|
379
|
+
stderr_tail = "\n".join(stderr_lines)[-2000:]
|
|
380
|
+
raise DockerRunError(exit_code, stderr_tail)
|
|
391
381
|
|
|
392
382
|
if result_msg is None:
|
|
393
383
|
raise DockerRunError(
|
|
@@ -10,7 +10,12 @@ Required labels (image is skipped if any are missing):
|
|
|
10
10
|
- ai.poly-hammer.portal-compatible = "true"
|
|
11
11
|
- ai.poly-hammer.service-id
|
|
12
12
|
- ai.poly-hammer.model-ids (comma-separated model IDs)
|
|
13
|
-
|
|
13
|
+
|
|
14
|
+
Optional labels:
|
|
15
|
+
- ai.poly-hammer.min-cuda-version Minimum host CUDA version required.
|
|
16
|
+
When set **and** *host_cuda_version* is passed to
|
|
17
|
+
:func:`discover_local_images`, images whose minimum exceeds the
|
|
18
|
+
host capability are skipped with a warning.
|
|
14
19
|
|
|
15
20
|
Per-model labels (optional, looked up for each model ID):
|
|
16
21
|
- ai.poly-hammer.model.<id>.name
|
|
@@ -32,17 +37,22 @@ _REQUIRED_LABELS = frozenset(
|
|
|
32
37
|
"portal-compatible",
|
|
33
38
|
"service-id",
|
|
34
39
|
"model-ids",
|
|
35
|
-
"cuda-version",
|
|
36
40
|
}
|
|
37
41
|
)
|
|
38
42
|
|
|
39
43
|
|
|
40
|
-
def discover_local_images(
|
|
44
|
+
def discover_local_images(
|
|
45
|
+
client: docker.DockerClient,
|
|
46
|
+
host_cuda_version: str | None = None,
|
|
47
|
+
) -> list[dict[str, Any]]:
|
|
41
48
|
"""Scan local Docker images for portal-compatible OCI labels.
|
|
42
49
|
|
|
43
50
|
Returns a list of dicts matching the ``WorkerImageInfo`` schema — one
|
|
44
51
|
entry per Docker image. Each entry contains a ``models`` list with
|
|
45
52
|
per-model metadata (name, hardware requirements).
|
|
53
|
+
|
|
54
|
+
If *host_cuda_version* is provided (e.g. ``"13.1"``), images whose
|
|
55
|
+
``ai.poly-hammer.min-cuda-version`` label exceeds it are skipped.
|
|
46
56
|
"""
|
|
47
57
|
results: list[dict[str, Any]] = []
|
|
48
58
|
|
|
@@ -77,8 +87,23 @@ def discover_local_images(client: docker.DockerClient) -> list[dict[str, Any]]:
|
|
|
77
87
|
continue
|
|
78
88
|
|
|
79
89
|
image_ref = _best_tag(image)
|
|
90
|
+
if not image_ref:
|
|
91
|
+
logger.debug("Skipping untagged image %s", image.short_id)
|
|
92
|
+
continue
|
|
93
|
+
|
|
80
94
|
service_id = ph_labels["service-id"]
|
|
81
|
-
|
|
95
|
+
min_cuda_version: str | None = ph_labels.get("min-cuda-version")
|
|
96
|
+
|
|
97
|
+
# Host compatibility check: skip images that need a newer CUDA
|
|
98
|
+
if min_cuda_version and host_cuda_version:
|
|
99
|
+
if _parse_version(min_cuda_version) > _parse_version(host_cuda_version):
|
|
100
|
+
logger.warning(
|
|
101
|
+
"Image %s requires CUDA >= %s but host has %s — skipping",
|
|
102
|
+
image_ref,
|
|
103
|
+
min_cuda_version,
|
|
104
|
+
host_cuda_version,
|
|
105
|
+
)
|
|
106
|
+
continue
|
|
82
107
|
|
|
83
108
|
# Image size in GB (Docker reports bytes)
|
|
84
109
|
image_size_gb = round(image.attrs.get("Size", 0) / (1024**3), 2)
|
|
@@ -106,7 +131,7 @@ def discover_local_images(client: docker.DockerClient) -> list[dict[str, Any]]:
|
|
|
106
131
|
{
|
|
107
132
|
"image_ref": image_ref,
|
|
108
133
|
"service_id": service_id,
|
|
109
|
-
"
|
|
134
|
+
"min_cuda_version": min_cuda_version,
|
|
110
135
|
"image_size_gb": image_size_gb,
|
|
111
136
|
"models": models,
|
|
112
137
|
}
|
|
@@ -121,13 +146,12 @@ def discover_local_images(client: docker.DockerClient) -> list[dict[str, Any]]:
|
|
|
121
146
|
return results
|
|
122
147
|
|
|
123
148
|
|
|
124
|
-
def _best_tag(image: Any) -> str:
|
|
125
|
-
"""Return the most useful tag for an image, or
|
|
149
|
+
def _best_tag(image: Any) -> str | None:
|
|
150
|
+
"""Return the most useful tag for an image, or None if untagged."""
|
|
126
151
|
tags = image.tags
|
|
127
152
|
if tags:
|
|
128
153
|
return tags[0]
|
|
129
|
-
|
|
130
|
-
return short_id.replace("sha256:", "") if short_id else "<untagged>"
|
|
154
|
+
return None
|
|
131
155
|
|
|
132
156
|
|
|
133
157
|
def _parse_int(value: str) -> int:
|
|
@@ -136,3 +160,29 @@ def _parse_int(value: str) -> int:
|
|
|
136
160
|
return int(value)
|
|
137
161
|
except (ValueError, TypeError):
|
|
138
162
|
return 0
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _parse_version(version: str) -> tuple[int, ...]:
|
|
166
|
+
"""Parse a dotted version string into a tuple of ints for comparison."""
|
|
167
|
+
try:
|
|
168
|
+
return tuple(int(p) for p in version.split("."))
|
|
169
|
+
except (ValueError, TypeError):
|
|
170
|
+
return (0,)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def resolve_image_for_model(
|
|
174
|
+
available_images: list[dict[str, Any]], model_id: str
|
|
175
|
+
) -> str | None:
|
|
176
|
+
"""Find the local Docker image reference that supports a given model ID.
|
|
177
|
+
|
|
178
|
+
Searches the list returned by :func:`discover_local_images` for the
|
|
179
|
+
first image whose ``models`` list contains the requested *model_id*.
|
|
180
|
+
|
|
181
|
+
Returns the ``image_ref`` (e.g. ``"hy-motion:1.0"``) or ``None`` if
|
|
182
|
+
no matching image is found.
|
|
183
|
+
"""
|
|
184
|
+
for img in available_images:
|
|
185
|
+
for model in img.get("models", []):
|
|
186
|
+
if model.get("model_id") == model_id:
|
|
187
|
+
return img.get("image_ref")
|
|
188
|
+
return None
|
{poly_hammer_worker-0.1.1.dev9 → poly_hammer_worker-0.1.1.dev11}/poly_hammer_worker/worker_loop.py
RENAMED
|
@@ -22,10 +22,10 @@ from poly_hammer_worker.docker_runner import (
|
|
|
22
22
|
InsufficientDiskError,
|
|
23
23
|
ensure_image,
|
|
24
24
|
get_free_disk_gb,
|
|
25
|
-
resolve_image_tag,
|
|
26
25
|
run_inference_container,
|
|
27
26
|
)
|
|
28
27
|
from poly_hammer_worker.hardware import detect_hardware
|
|
28
|
+
from poly_hammer_worker.image_discovery import resolve_image_for_model
|
|
29
29
|
|
|
30
30
|
logger = logging.getLogger(__name__)
|
|
31
31
|
|
|
@@ -87,7 +87,9 @@ def run_worker_loop(
|
|
|
87
87
|
docker_client = docker.from_env()
|
|
88
88
|
|
|
89
89
|
# ---- Image discovery (initial) -------------------------------------------
|
|
90
|
-
available_images = discover_local_images(
|
|
90
|
+
available_images = discover_local_images(
|
|
91
|
+
docker_client, host_cuda_version=cuda_version
|
|
92
|
+
)
|
|
91
93
|
if available_images:
|
|
92
94
|
total_models = sum(len(img["models"]) for img in available_images)
|
|
93
95
|
console.print(
|
|
@@ -132,7 +134,9 @@ def run_worker_loop(
|
|
|
132
134
|
nonlocal _last_disk_check, available_images
|
|
133
135
|
while not shutdown.is_set():
|
|
134
136
|
# Re-discover images on each heartbeat (cheap — Docker API list)
|
|
135
|
-
available_images = discover_local_images(
|
|
137
|
+
available_images = discover_local_images(
|
|
138
|
+
docker_client, host_cuda_version=cuda_version
|
|
139
|
+
)
|
|
136
140
|
|
|
137
141
|
# Throttled disk space check
|
|
138
142
|
now = time.monotonic()
|
|
@@ -217,7 +221,7 @@ def run_worker_loop(
|
|
|
217
221
|
portal=portal,
|
|
218
222
|
docker_client=docker_client,
|
|
219
223
|
job=job,
|
|
220
|
-
|
|
224
|
+
available_images=available_images,
|
|
221
225
|
auto_cleanup=auto_cleanup,
|
|
222
226
|
registry_url=registry_url,
|
|
223
227
|
console=console,
|
|
@@ -234,21 +238,26 @@ def _execute_job(
|
|
|
234
238
|
portal: PortalClient,
|
|
235
239
|
docker_client: docker.DockerClient,
|
|
236
240
|
job: dict,
|
|
237
|
-
|
|
241
|
+
available_images: list[dict],
|
|
238
242
|
console: Console,
|
|
239
243
|
auto_cleanup: bool = False,
|
|
240
244
|
registry_url: str | None = None,
|
|
241
245
|
) -> None:
|
|
242
246
|
"""Ensure image (local-first, optional registry pull), run container, report outcome."""
|
|
243
247
|
job_id = str(job["job_id"])
|
|
244
|
-
|
|
248
|
+
model_id = job.get("model_id", "")
|
|
245
249
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
250
|
+
image_ref = resolve_image_for_model(available_images, model_id)
|
|
251
|
+
if not image_ref:
|
|
252
|
+
portal.fail_job(
|
|
253
|
+
job_id,
|
|
254
|
+
f"No local Docker image found for model '{model_id}'. "
|
|
255
|
+
"Build the appropriate image and restart the worker.",
|
|
256
|
+
"NO_IMAGE",
|
|
257
|
+
)
|
|
258
|
+
console.print(f"[red] No local image for model '{model_id}' — skipped[/red]")
|
|
249
259
|
return
|
|
250
260
|
|
|
251
|
-
image_ref = resolve_image_tag(docker_image, cuda_version)
|
|
252
261
|
start_time = time.monotonic()
|
|
253
262
|
|
|
254
263
|
try:
|
|
@@ -262,22 +271,42 @@ def _execute_job(
|
|
|
262
271
|
)
|
|
263
272
|
|
|
264
273
|
# ---- Run container ----------------------------------------------------
|
|
274
|
+
container_logger = logging.getLogger(f"{__name__}.container")
|
|
275
|
+
|
|
276
|
+
def _on_log(line: str) -> None:
|
|
277
|
+
container_logger.info(line)
|
|
278
|
+
console.print(f" [dim]│ {line}[/dim]", highlight=False)
|
|
279
|
+
|
|
265
280
|
result = run_inference_container(
|
|
266
281
|
docker_client,
|
|
267
282
|
image_ref,
|
|
268
283
|
job_payload=job,
|
|
269
284
|
on_progress=lambda p: _on_progress(portal, job_id, p, console),
|
|
270
285
|
on_status=lambda s: console.print(f" [dim]{s}[/dim]"),
|
|
286
|
+
on_log=_on_log,
|
|
271
287
|
)
|
|
272
288
|
|
|
273
289
|
elapsed = time.monotonic() - start_time
|
|
274
290
|
|
|
275
291
|
# ---- Report completion ------------------------------------------------
|
|
292
|
+
# The container uploads to S3 via the presigned URL but doesn't
|
|
293
|
+
# echo the key back in its result JSON. Fall back to the s3_key
|
|
294
|
+
# from the job payload so the backend can store it.
|
|
295
|
+
result_url = result.get("result_url") or job.get("s3_key", "")
|
|
296
|
+
|
|
297
|
+
# Build result_metadata: use explicit field if present, otherwise
|
|
298
|
+
# collect useful top-level fields the container emitted.
|
|
299
|
+
result_metadata = result.get("result_metadata") or {
|
|
300
|
+
k: v
|
|
301
|
+
for k, v in result.items()
|
|
302
|
+
if k not in ("type", "status", "result_url", "result_metadata")
|
|
303
|
+
}
|
|
304
|
+
|
|
276
305
|
portal.complete_job(
|
|
277
306
|
job_id,
|
|
278
307
|
elapsed_seconds=elapsed,
|
|
279
|
-
result_url=
|
|
280
|
-
result_metadata=
|
|
308
|
+
result_url=result_url,
|
|
309
|
+
result_metadata=result_metadata,
|
|
281
310
|
)
|
|
282
311
|
console.print(f" [green]Completed in {elapsed:.1f}s[/green]")
|
|
283
312
|
|
|
File without changes
|
|
File without changes
|
{poly_hammer_worker-0.1.1.dev9 → poly_hammer_worker-0.1.1.dev11}/poly_hammer_worker/__init__.py
RENAMED
|
File without changes
|
{poly_hammer_worker-0.1.1.dev9 → poly_hammer_worker-0.1.1.dev11}/poly_hammer_worker/api_client.py
RENAMED
|
File without changes
|
{poly_hammer_worker-0.1.1.dev9 → poly_hammer_worker-0.1.1.dev11}/poly_hammer_worker/banner.py
RENAMED
|
File without changes
|
|
File without changes
|
{poly_hammer_worker-0.1.1.dev9 → poly_hammer_worker-0.1.1.dev11}/poly_hammer_worker/hardware.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|