Skip to content

Commit 89d8cd4

Browse files
committed
evidence(containers): the SBSA image RUNS on Tegra -- Orin generates on the GPU
FOLLOWING_AGENTS_PROTOCOL W6's Tegra determination, and it contradicts what this spec predicted. The risk entry said Thor and Orin "are Tegra/L4T with a different CUDA runtime" and that the arm64 image made no claim there. It runs. There is no CUDA-version wall, and no separate Tegra lane is needed: ONE SBSA image serves both arm64 families. Measured on a Jetson AGX Orin Developer Kit (sm_87, L4T R36.4.3, Docker 27.5.1, Kairos immutable OS), running the image built on GB10: validator config + layout verified; /health 200, /version 200, declared healthcheck passed, clean SIGTERM, on --runtime nvidia --gpus all model Qwen3-0.6B (rev c1899de289a04d12100db370d81485cdf75e47ca, 1.5 GB) loads and GENERATES: "The capital of France is" -> " Paris. ..." 24 completion tokens gpu tegrastats GR3D_FREQ 95-97% during a 120-token generation, against 14-15% idle -- decode runs on the Orin GPU, not a CPU fallback That last line is the one /health could not make, on Orin or on GB10. THE INVOCATION DIFFERS, and all three cases fail differently: --gpus all alone REFUSED by the runtime hook --runtime nvidia alone starts, no driver, dies on libcuda.so.1 missing --runtime nvidia --gpus all works The middle case is the trap: the container comes up and dies on a missing library, which reads as a broken image rather than a wrong flag. docs/USAGE.md documented only `--gpus all` and was therefore wrong for every Jetson. Two portability defects the second box exposed, both invisible on GB10 because that box happened to match: - validate-container-image.py used `docker stop --timeout`, which is newer-Docker only; Orin's 27.5.1 rejects it. `-t` is accepted by both. - /models must be READABLE BY UID 1000. On GB10 the model was owned by uid 1000 so mode 0600 worked; on Orin (files owned by 65535) the server initialised CUDA and died on `safetensors: cannot open file`, which reads like a corrupt checkpoint. Now documented. A wrong hypothesis, killed by test rather than shipped: I assumed the injected driver path just was not on the loader search path and tried patching LD_LIBRARY_PATH to include /usr/lib/aarch64-linux-gnu/nvidia. Still not found -- because with --runtime alone the driver is not mounted at all. No Dockerfile change is needed; the nvidia runtime handles ldconfig itself in csv mode. Scope: Orin (sm_87) only. Thor (sm_110) has never been probed and inherits nothing from this. Nothing is published to GHCR. Issues: #170 Following-Agents-Protocol: true AI-Assisted: true Assisted-by: Claude:claude-opus-5 [Claude Code]
1 parent 211ff73 commit 89d8cd4

6 files changed

Lines changed: 126 additions & 18 deletions

File tree

.agents/engine-matrix.md

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

.agents/specs/container-images.md

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -305,12 +305,14 @@ gate passes, and no publish step uses a wildcard.
305305

306306
## Risks and open questions
307307

308-
- **Tegra versus SBSA.** A hosted `ubuntu-24.04-arm` runner produces an SBSA
309-
CUDA build. GB10 is SBSA and is expected to run it; **Thor (`sm_110`) and
310-
Orin (`sm_87`) are Tegra/L4T with a different CUDA runtime**. The arm64 cuda
311-
image is claimed to run on Tegra only once it has run on Thor. If it cannot,
312-
the honest outcome is a recorded boundary or a separate Tegra lane, never a
313-
quiet widening of the arm64 tag.
308+
- **Tegra versus SBSA: MEASURED 2026-08-11, and the earlier prediction here
309+
was WRONG.** This entry used to say the arm64 image made no Tegra claim,
310+
on the reasoning that Thor and Orin run Tegra/L4T with a different CUDA
311+
runtime. The SBSA image RUNS on Jetson AGX Orin (`sm_87`, L4T R36.4.3):
312+
`/health` 200, `/version` 200, in-container healthcheck, clean SIGTERM.
313+
There is no CUDA-version wall. What differs is the INVOCATION, not the
314+
image -- see the Tegra section below. Thor (`sm_110`) is still unprobed
315+
and inherits nothing from this.
314316
- **CUDA runtime redistribution.** Copying `libcudart.so.12` and
315317
`libcublasLt.so.12` out of the toolkit is permitted under the CUDA EULA's
316318
redistributable list, but the exact file list, version, and notice text are a
@@ -486,6 +488,64 @@ Lock discipline, since the box is shared: the build ran outside `gpu.lock`
486488
because it needs no GPU, and only the container run took the lock, blocking --
487489
it queued 16:58:38 -> 17:50:43 behind other users rather than jumping them.
488490

491+
### W6 Tegra result: the SBSA image runs on Orin, but not the same way
492+
493+
Measured 2026-08-11 on a Jetson AGX Orin Developer Kit (`sm_87`, L4T R36.4.3,
494+
Docker 27.5.1, Kairos immutable OS), running the SBSA image built on GB10:
495+
496+
```
497+
container image OK lane=cuda
498+
config, layout: verified
499+
boot: /health 200, /version 200, declared healthcheck passed, clean SIGTERM,
500+
on --runtime nvidia --gpus all
501+
```
502+
503+
So one image serves both arm64 families and no separate Tegra lane is needed.
504+
`libcuda.so.1` resolves to `/usr/lib/aarch64-linux-gnu/nvidia/libcuda.so.1`
505+
there, against `/usr/lib/aarch64-linux-gnu/libcuda.so.1` on SBSA; the nvidia
506+
runtime handles that itself, and the image needs no `LD_LIBRARY_PATH` change
507+
(tried, and it was the wrong hypothesis -- the driver simply was not mounted).
508+
509+
**The invocation differs, and all three cases fail differently:**
510+
511+
| flags | Tegra behaviour |
512+
|---|---|
513+
| `--gpus all` alone | REFUSED: "invoking the NVIDIA Container Runtime Hook directly ... is not supported" |
514+
| `--runtime nvidia` alone | starts, no driver mounted, dies on `libcuda.so.1: cannot open shared object file` |
515+
| `--runtime nvidia --gpus all` | driver injected, server runs |
516+
517+
The middle case is the trap: the container comes up and dies on a missing
518+
library, which reads as a broken image rather than a wrong flag.
519+
520+
**Two operator requirements this exposed**, both invisible on GB10 because that
521+
box happened to match:
522+
523+
- `/models` must be READABLE BY UID 1000. The image runs as uid 1000; on GB10
524+
the model was owned by uid 1000 so mode 0600 worked, while on Orin (files
525+
owned by 65535) the server initialised CUDA and then died on
526+
`safetensors: cannot open file`, which looks like a corrupt checkpoint.
527+
- `docker stop --timeout` is newer-Docker only. The validator used it and broke
528+
on Docker 27.5.1; `-t` is accepted by both.
529+
530+
**It generates, and the GPU does the work.** `/health` alone would only prove
531+
the engine came up, so the Orin run was taken further with a real model:
532+
Qwen3-0.6B (HF `Qwen/Qwen3-0.6B`, revision `c1899de289a04d12100db370d81485cdf75e47ca`,
533+
1.5 GB bf16) loaded in the container and served `/v1/completions`:
534+
535+
```
536+
prompt "The capital of France is"
537+
completion " Paris. The capital of France is also the capital of the French Republic..."
538+
usage prompt_tokens 5, completion_tokens 24
539+
```
540+
541+
`tegrastats` during a 120-token generation reads **GR3D_FREQ 95-97%**, against
542+
14-15% at idle -- so decode runs on the Orin GPU rather than falling back to CPU
543+
paths. That is the distinction `/health` cannot make, and the GB10 result did
544+
not make either.
545+
546+
**Scope.** Orin (`sm_87`) only. Thor (`sm_110`) has never been probed and
547+
inherits nothing from this.
548+
489549
### Pull-request scope, and why it is not a hole
490550

491551
A release run builds every lane on both architectures. A pull request builds a

docs/BENCHMARKS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
| **Structured state record (active)** | v1 scalar + relational + Git-history contracts | No benchmark. At `776c56f1`: 157 imports = 3,231,342 exact bytes; append preserved all 156 wrappers/rows. 95 tests: validator/core 44 (checker 20 + core 24), NOW 18, migration 22, cutover 11. New raw-row mutation guard. | n/a |
99
| **Binary release matrix (ACTIVE; required W1-W11/W13 implemented in #196)** | Eight primary CPU/CUDA/Vulkan/Metal/MLX host tuples | Adaptive x86 tiers, Vulkan 35/35 + cross-device 11/11, and metadata/mutation gates green. **PENDING:** hosted full matrix, matching hardware, tagged publish | n/a |
1010
| **Binary release delivery topology** | #196: read-only build/verify, OIDC attest, protected publish; generated indexes and explicit handoff-authenticated assets | Fixes the zero-binary release path by attaching all eight archive/checksum/provenance triplets plus indexes. Hosted proof pending; W12 diagnostics optional | n/a |
11-
| **Container images (ACTIVE; arm64 cuda RUNTIME-VERIFIED 2026-08-11)** | `ENG-RELEASE-CONTAINERS` ([spec](../.agents/specs/container-images.md)) | cpu amd64 **783 MB**; cuda arm64 **1.71 GB**, `/health`+`/version`+healthcheck+clean SIGTERM **on `--gpus all`** on GB10 `sm_121a`; SIGTERM **0.25 s exit 0** (was 30 s SIGKILL, #312) | n/a |
11+
| **Container images (ACTIVE; arm64 cuda verified on GB10 + Orin 2026-08-11)** | `ENG-RELEASE-CONTAINERS` ([spec](../.agents/specs/container-images.md)) | cpu amd64 783 MB; cuda arm64 **1.71 GB**. GB10 `sm_121a`: `/health`+`/version`+SIGTERM on `--gpus all`. Orin `sm_87` (Tegra): Qwen3-0.6B **generates**, GPU **GR3D 95-97%** | n/a |
1212
| **Developer/row protocol** | Contribution entry point; `ENG-NOW-DERIVED` #374 @`dbd0d51c` | Entry-point gates retained. #374 W1-W5 DONE; benchmark/runtime/parity `VOID`; row specs now carry `## Now` | n/a |
1313
| **LoRA runtime W2** (`LORA-RUNTIME`, #278) | **No number owed:** correctness-only; a grid PENDS the W7 model gate |
1414
| **ARCH audit: ABI is text-only** | 4 capabilities (H3 video, Laguna, Kimi-Linear, DeepSeek-V4) reachable only from `examples/`, none registry-backed. No gate asks whether a CONSUMER can reach a capability. Documentation only |

docs/STATUS.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,10 @@ Cold start: `MEASURED`. Load (#150): 27B bf16 loads **1.54x warm / 1.61x cold**,
3838
Releases: W1-W11/W13 are implemented in #196; local CPU, Vulkan, archive,
3939
metadata and mutation gates are green, while the hosted eight-tuple dry run and
4040
tagged publication stay pending, so no binary is published. Container
41-
images (#170): the cpu (amd64) and cuda (arm64) lanes pass their gates, the
42-
latter RUNTIME-VERIFIED on GB10 `sm_121a` with `--gpus all`; Tegra (Thor, Orin)
43-
is untested and nothing is published.
41+
images (#170): the cpu (amd64) and cuda (arm64) lanes pass their gates. One SBSA
42+
arm64 image serves BOTH families: GB10 `sm_121a`, and Jetson AGX Orin `sm_87`
43+
where Qwen3-0.6B generates with the GPU at 95-97% (Tegra needs
44+
`--runtime nvidia --gpus all`). Thor is unprobed; nothing is published.
4445
See [RELEASES](RELEASES.md).
4546

4647
Protocol (2026-08-09): `776c56f1` has 157 imports = 3,231,342 exact bytes;

docs/USAGE.md

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -549,9 +549,28 @@ docker run --rm --gpus all -p 8000:8000 \
549549
--model /models/Qwen3.6-35B-A3B
550550
```
551551

552-
`/models` is the weights mount and `/cache` is the tokenizer/HF cache; the
553-
container runs as uid 1000, so `/cache` must be writable by it if you bind-mount
554-
one. `ffmpeg` is installed in every lane, so `/v1/videos` works out of the box —
552+
`/models` is the weights mount and `/cache` is the tokenizer/HF cache. The
553+
container runs as **uid 1000**, so `/cache` must be writable by it and the
554+
weights under `/models` must be READABLE by it. A model file with mode `0600`
555+
owned by another uid fails as `safetensors: cannot open file`, which reads like
556+
a corrupt checkpoint rather than a permissions problem.
557+
558+
### On Jetson (Tegra/L4T)
559+
560+
The cuda image is one SBSA build and it runs on Jetson too -- verified on AGX
561+
Orin (`sm_87`, L4T R36.4.3) -- but Tegra needs a **different invocation**:
562+
563+
```sh
564+
docker run --rm --runtime nvidia --gpus all -p 8000:8000 \
565+
-v /path/to/models:/models:ro \
566+
ghcr.io/mudler/vllm.cpp:latest-cuda \
567+
--model /models/your-model
568+
```
569+
570+
`--gpus all` on its own is refused there ("invoking the NVIDIA Container Runtime
571+
Hook directly ... is not supported"), and `--runtime nvidia` on its own starts a
572+
container with no driver, which dies on `libcuda.so.1: cannot open shared object
573+
file`. Both flags together are what works. `ffmpeg` is installed in every lane, so `/v1/videos` works out of the box —
555574
a deliberate difference from the tarballs, which never vendor it because they
556575
are extracted onto a host that already has a `PATH`.
557576

scripts/validate-container-image.py

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,12 @@ def wait_for_health(url: str, timeout: float) -> tuple[bool, str]:
241241

242242

243243
def check_boot(
244-
image: str, model: Path, port: int, timeout: float, gpus: str | None = None
244+
image: str,
245+
model: Path,
246+
port: int,
247+
timeout: float,
248+
gpus: str | None = None,
249+
runtime: str | None = None,
245250
) -> list[str]:
246251
errors: list[str] = []
247252
name = f"vllm-cpp-smoke-{port}"
@@ -251,6 +256,12 @@ def check_boot(
251256
# nothing about the GPU it was built for. The driver still comes from the
252257
# host through the container runtime; the image never carries one.
253258
gpu_args = ["--gpus", gpus] if gpus else []
259+
# Tegra/L4T needs --runtime nvidia and REJECTS --gpus outright:
260+
# "invoking the NVIDIA Container Runtime Hook directly (e.g. specifying
261+
# the docker --gpus flag) is not supported"
262+
# so a GPU lane cannot be validated on Jetson through --gpus alone.
263+
if runtime:
264+
gpu_args = ["--runtime", runtime, *gpu_args]
254265

255266
run(["docker", "rm", "--force", name])
256267
code, output = run(
@@ -292,7 +303,9 @@ def check_boot(
292303

293304
# SIGTERM, not SIGKILL: an image that has to be killed loses in-flight work
294305
# on every ordinary orchestrator restart.
295-
code, output = run(["docker", "stop", "--timeout", "30", name])
306+
# `-t`, not `--timeout`: the long form only exists on newer Docker (the
307+
# Jetson node runs 27.5.1 and rejects it), and `-t` is accepted by both.
308+
code, output = run(["docker", "stop", "-t", "30", name])
296309
if code != 0:
297310
errors.append(f"docker stop failed: {output.strip()}")
298311
else:
@@ -316,6 +329,11 @@ def main() -> int:
316329
parser.add_argument("--version", required=True)
317330
parser.add_argument("--expect-revision")
318331
parser.add_argument("--model", type=Path, help="model directory for the boot smoke")
332+
parser.add_argument(
333+
"--docker-runtime",
334+
help="pass through to `docker run --runtime` (e.g. nvidia). Required on "
335+
"Tegra/L4T, which rejects --gpus",
336+
)
319337
parser.add_argument(
320338
"--gpus",
321339
help="pass through to `docker run --gpus` (e.g. all) so the boot smoke is "
@@ -335,7 +353,12 @@ def main() -> int:
335353
errors.append(f"--model {args.model} is not a directory")
336354
else:
337355
boot_errors = check_boot(
338-
args.image, args.model.resolve(), args.port, args.boot_timeout, args.gpus
356+
args.image,
357+
args.model.resolve(),
358+
args.port,
359+
args.boot_timeout,
360+
args.gpus,
361+
args.docker_runtime,
339362
)
340363
errors += boot_errors
341364
runtime_verified = not boot_errors
@@ -349,7 +372,12 @@ def main() -> int:
349372
print(f"container image OK: {args.image} lane={args.lane} version={args.version}")
350373
print(f" config, layout: verified")
351374
if runtime_verified:
352-
where = f"on --gpus {args.gpus}" if args.gpus else "on CPU paths only (no --gpus)"
375+
selectors = []
376+
if args.docker_runtime:
377+
selectors.append(f"--runtime {args.docker_runtime}")
378+
if args.gpus:
379+
selectors.append(f"--gpus {args.gpus}")
380+
where = f"on {' '.join(selectors)}" if selectors else "on CPU paths only (no GPU)"
353381
print(
354382
f" boot: /health 200, /version 200, declared healthcheck passed, clean "
355383
f"SIGTERM, {where}"

0 commit comments

Comments
 (0)