Skip to content

native: Qwen3.5-2B on CUDA attests the execution witness's first path, every weight byte read once the server is ready - #2956

Open
kotevcode wants to merge 14 commits into
ashhart:mainfrom
kotevcode:execution-witness-cuda
Open

kotevcode wants to merge 14 commits into
ashhart:mainfrom
kotevcode:execution-witness-cuda

Conversation

@kotevcode

@kotevcode kotevcode commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

What this changes

With --execution-witness, native Qwen3.5-2B on CUDA at one rank, every module MLX affine 4-bit and text only now reads known on the path qwen3_5-2b/cuda/tp1/mlx-q4g64/text. Every other engine still reads unsupported_path, and without the flag nothing changes.

Checks on this head

Receipt

2B A B C
Start to /health 200, s (n=3) 4.972 4.350 5.006
/health 200 to known, s (n=3) 20.619
Cold first token, 7,680 tokens, after known, ttft_s (n=3) 0.353 0.352 0.368
Cold first token at /health 200, during the hash, ttft_s (n=3) 0.348 0.348 0.365
Cold first token, ten more alternating rounds, ttft_s (n=10) 0.349 0.360 0.358
Decode, 1 stream, code / chat, tok/s 834.8 / 414.2 834.6 / 414.4 834.0 / 414.0
Decode, 8 streams aggregate, code / chat, tok/s 3286.7 / 2058.2 3288.8 / 2054.4 3284.9 / 2052.3
Decode during the hash, 1 / 8 streams, code, tok/s 834.1 / 3286.2 833.3 / 3286.7 834.0 / 3278.4
27B (unsupported_path, nothing hashed) A B C
Start to /health 200, s (n=3) 52.018 52.139 51.572
Cold first token, 7,680 tokens, ttft_s (n=3) 2.996 2.996 2.996

The first three cold first-token rows put C 15 to 17 ms above A and B; ten more alternating rounds put C 2 ms below B (Mann-Whitney p=0.88) and B 11 ms above A (p=0.45), so I read it as noise between builds, not the hash. Every C start gave the same content and components.engine, and a different witness and instance.

  • Idle reload (two runs): known at epoch 1, then parked; the request that wakes it is answered in about 5 s, through reloading and pending at epoch 2, with the same tokens as before the park. Epoch 2 reads known about 25.7 s after that request, with the same content, components.engine and instance and a new witness.
  • A shard touched after the load gives files_changed (3 of 3); --vision gives feature_excluded with "feature": "--vision"; --tp 2 is refused at startup for this family, as on main.
  • Decode speed before and after, from tools/bench_openai.py: 256 tokens, 3 reps, temperature 0, 9 samples per config: fibonacci-raw 835.1 / 834.2 / 834.4 and gpu-chat-no-think 414.6 / 414.1 / 414.2 tok/s (A / B / C).
  • Prompt speed before and after: tools/bench_cache.py --require-cold, 7,680-token prompt: above, every run cache_state: cold, cached_tokens: 0.
  • Licence of any new weights or drafter: n/a.

Checklist

@kotevcode

Copy link
Copy Markdown
Contributor Author

A question on the weights read: it uses one worker thread, so it takes one core and one stream of disk reads away from serving. On the A100 box in the receipt, 1.6 GiB took about 20 s, limited by the disk (84 MB/s at 28% utilisation, the process at 78% of one core). More workers would finish sooner on a faster disk but compete more with decode. Should it stay at one, or follow a setting?

@ashhart

ashhart commented Nov 11, 2026

Copy link
Copy Markdown
Owner

Thanks @kotevcode. The core of this is right: the shards are stamped before the load, every byte is read once the server is ready with no digest cache, the stamps are checked again around the read, and an idle reload stamps and reads its own engine again. Off, nothing changes. Some asks before it leaves draft:

  1. zig/src/native/cuda_witness.zig:51-66 and :133: the captured kernel set is hashed after ready but never stamped before the load. Besides TENSORFOLD_CUDA_KERNELS, aot.pick uses share/tensorfold/cuda/sm<N> next to the binary by default, and the Linux release archives ship a set there, so a refreshed capture during the window before the hash would read known with kernels that aren't the ones running. Please stamp aot.json and each cubin it names in open and check them in unchanged() (or have a captured set read unsupported_path for now), and change cuda_witness_test.zig:229-231 to expect files_changed, as the shard test does.
  2. zig/src/native/cuda.zig:258: try Witness.open(...) turns any stamping error into a failed load, for every dense Qwen3.5 checkpoint on CUDA, not just the 2B. Opt-in coherent execution witness for exact-response cache qualification #2741 asks for unknown with inference still available; please log one line and read hash_failed, as openBinary/stampText do, and let only OutOfMemory through.
  3. zig/src/server/witness.zig:150-174: start and bump begin the engine's read even when the snapshot can only be feature_excluded (--cortheon, --compact-at, --compact-memory), so those servers read every weight byte at ready and after each reload for nothing. Please skip it when excluded is set.
  4. Device evidence for the path now that it reads known: on the A100 with the flag on, please show a resumed prompt equals a fresh one and drafted output equals "draft": false (both are on by default in your runs). --kv-cache fp8, --prompt-precision int8 and --rope-yarn also reach this path and read known; either run the same checks with them or have them read feature_excluded for now. The second is fine with us.

On your question: keep one worker and no setting. It's off the request path and bounded by the checkpoint, and your decode-during-hash rows are level. We'll run a GB10 check on our side once these are in.

  • Synthena (Ash's Agent)

@kotevcode

Copy link
Copy Markdown
Contributor Author

Thanks. All four are in 5043d29:

  1. Witness.open takes the captured set's folder (aot.pick's result, so TENSORFOLD_CUDA_KERNELS or the share/tensorfold/cuda/sm<N> set beside the binary). It stamps aot.json and each cubins/<hash>.cubin it names, next to the shards, and unchanged() checks them all. The second check now runs after the kept components (which read the cubins) as well as the shards. cuda_witness_test.zig expects files_changed after the cubin rewrite, and a witness reopened over the new cubin gives a different digest.
  2. cuda.zig calls Witness.openOrFailed. Only OutOfMemory fails the load. Any other stamping error logs one line and gives a witness that reads hash_failed and never reads anything. New test: a malformed aot.json reads hash_failed.
  3. start and bump skip the engine's read when excluded is set. New test: the hook's begin count is 1 at ready and 2 after a reload on a plain server, and 0 and 0 with --cortheon.
  4. --kv-cache other than bf16, --prompt-precision other than bf16 and --rope-yarn now read feature_excluded with the flag's name, and docs/api.md lists them.

One worker, no setting: kept. test-execution-witness-cuda-host 18/18, test-execution-witness-host 34/34, test-cuda-host, test-server-cpu, test-model-idle-host and test-disk-identity-host pass; pr_check --pr 2956: 0 errors, lean 345/345.

@ashhart

ashhart commented Nov 12, 2026

Copy link
Copy Markdown
Owner

Thanks @kotevcode, 5043d29 covers the code asks:

  1. Witness.open stamps the captured set that aot.pick chose: aot.json and every cubin it names. unchanged() now runs again after the kept components have read those cubins, and the cubin test expects files_changed.
  2. openOrFailed lets only OutOfMemory fail the load. A file it can't stamp logs one line and reads hash_failed, and the malformed aot.json test pins that.
  3. start and bump skip the engine's read on an excluded server, and the begin counts (1, then 2 after a reload; 0 with --cortheon) show it.
  4. --kv-cache, --prompt-precision and --rope-yarn read feature_excluded. All three default to bf16/unset, so a default server still takes the path.

The PR's own CI passed on this head. Merged onto today's main here, test-execution-witness-cuda-host and test-disk-identity-host pass. The only conflict was CHANGELOG.md.

Before it leaves draft:

  1. Please restack it on server: --execution-witness serves a read-scoped snapshot of what runs, or why it is unknown #2901's current head, bd15bd6, and on native: the CUDA kept-state identity is written once through any hasher, its keys unchanged #2903's head. It still carries 3067c2b, which doesn't have the _TOKEN filter or its docs/api.md line.
  2. The first half of ask 4 was device evidence on this path with the flag on: a resumed prompt equal to a fresh one, and drafted output equal to "draft": false. Your server: "execution_witness": true binds a reply to the witness at admission and at its end, with its prompt, resolved request and tokens #2958 receipt has both on 2B, run on this PR's previous head with server: "execution_witness": true binds a reply to the witness at admission and at its end, with its prompt, resolved request and tokens #2958's commits on top. The review commit here doesn't touch generation, so that's enough for us. Please link it from this description and update the checklist line that still says they weren't run.
  3. Optional, your call: the malformed aot.json test passes, but its hash_failed warning goes to stderr, so zig build's summary lists test-execution-witness-cuda-host under "failed command" next to real failures. Keeping that warning out of the test's output would keep the summary clean.

Once it's restacked and out of draft, we'll run the GB10 check we promised.

  • Synthena (Ash's Agent)

@kotevcode
kotevcode force-pushed the execution-witness-cuda branch from 5043d29 to b7a99ed Compare November 12, 2026 16:48
@kotevcode

Copy link
Copy Markdown
Contributor Author

Restacked as asked, onto #2901's head bd15bd6 and #2903's current commits; it's still on #2901's base, not main. #2903's two commits (a7bbda5, 7795508) change the same lines, with the same messages, as the copies here, which are now 93282c1 and c1c1d28. The rebase had no conflicts, and the branch's diff over #2901 is unchanged.

I also took the stray warning out of the build summary (b7a99ed). The malformed aot.json test now lowers std.testing.log_level to .err around openOrFailed, as lane_activity_test and checkpoint_metal do. Production still logs the hash_failed warning, and test-execution-witness-cuda-host no longer shows up under "failed command".

The description now links the #2958 A100 receipt (https://gist.github.com/kotevcode/5b4d26cd284c1f04878862f4c1611b93) for resumed = fresh, and for drafted = "draft": false, with the flag on.

test-execution-witness-cuda-host 18/18, test-execution-witness-host 34/34, test-disk-identity-host 35/35, and test-cuda-host passes on the Mac host (6339 pass, 38 skip, across its 390 steps). fmt, git diff --check and pr_check are clean.

@kotevcode
kotevcode marked this pull request as ready for review November 12, 2026 16:48
@ashhart

ashhart commented Nov 12, 2026

Copy link
Copy Markdown
Owner

Thanks @kotevcode. The restack is clean. #2903's two commits come over with the same changes, and #2901's _TOKEN filter is now included. The hash_failed warning is out of the build summary. The description now links the #2958 A100 receipt, which shows resumed equals fresh and drafted equals "draft": false with the flag on. No more asks from us.

As promised, we'll run the GB10 check on this head next: Qwen3.5-2B MLX 4-bit with --execution-witness, against main and against the flag off. We'll check for known on qwen3_5-2b/cuda/tp1/mlx-q4g64/text with a captured kernel set in place, files_changed after a shard is touched, equal token_sha256 at 1 and 8 streams, and level decode, including while the hash runs. We'll post the numbers here.

It lands after #2896, #2901 and #2903, in that order. GitHub won't start CI while the stack conflicts with main in CHANGELOG.md, so we'll resolve that when we land, as on #2901.

  • Synthena (Ash's Agent)

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants