Skip to content

nemotron cuda: an encode hook on Vision, and a host test that a request encodes each image once - #1104

Closed
plotarmordev wants to merge 3 commits into
ashhart:mainfrom
plotarmordev:nemotron-vision-encode-hook
Closed

plotarmordev wants to merge 3 commits into
ashhart:mainfrom
plotarmordev:nemotron-vision-encode-hook

Conversation

@plotarmordev

Copy link
Copy Markdown
Contributor

What this changes

The follow-up to #1057 you asked for: an encode hook on Vision, so a host test can count encodes per request.

  • zig/src/families/nemotron/cuda_vision.zig: Vision.encoder: ?Encoder = null. An Encoder is a ctx and an
    encode function, as Flash Next's encoder is; when set, Vision.begin hands it the pass's images, planned spans and
    rows buffer in place of Tower.encode. Null (every server) keeps the tower call as it was.
  • zig/src/families/nemotron/cuda_vision_lanes_test.zig (new, in zig build test): runs the actual lane prefill
    (Cuda.backend().prefill, i.e. prefillFn) for a request with 1 image and with 3, twice each, with a counting encoder.
    It asserts one encode pass a request and one encode an image. The engine's own sequence is set to hold fewer rows than
    the prompt, so prefillFrom returns error.ContextFull before its first chunk and nothing past the image setup needs
    a GPU.
  • cuda_vision_test.zig sets the hook to null so its test still reaches the real tower's error.RowShape check.

Stacks on #1057

#1057's fix is not on upstream main yet: main (d1018b3, after the 1.1.1 merge) still calls Vision.begin at
cuda_lanes.zig:299 and again at :312, and main is the only public branch. On main as it is, the new test fails. So
this branch sits on #1057. Its first commit is #1057's change rebased onto current main, with the same
git patch-id --stable as #1057's head e2357b5. Its second commit is the hook and the test. Once #1057 (or your 1.1.2
carry of it) is on main, the first commit drops out and the second applies alone. The test only counts encodes before
the first chunk, so it passes whichever of the two calls the fix keeps.

The test fails without the fix

The new test alone, on the tip with #1057's commit reverted (main's prompt pass plus the hook):

error: 'cuda_vision_lanes_test.test.a lane prompt pass encodes each image of a request once' failed:
       expected 1, found 2
Build Summary: 3/5 steps succeeded (1 failed); 10/11 tests passed (1 failed)

The same step passes on the tip: 5/5 steps succeeded; 11/11 tests passed. The filtered step runs 11 tests, and the
new one is the only one that changes.

Receipt

Raw files: https://github.com/plotarmordev/TensorFold/tree/nemotron-receipts/receipts/nemotron/vision-encode-hook (README.md maps every file; SHA256SUMS).

  • Environment: this branch at 5926103 on main d1018b3. CPU only on one DGX Spark (GB10, aarch64 Linux), zig 0.17.0,
    nvcc 13.0, -j1, a fresh cache for each step (scripts/gates.sh, output in host/gates.txt).
  • Exactness: no GPU run. Servers leave Vision.encoder null, so Vision.begin calls Tower.encode with the same
    arguments as before. The added work is one null check a prompt pass with images.
  • Decode speed before and after: not measured. Decode code is unchanged.
  • Prompt speed before and after: not measured. Only the null check above is added to a prompt pass with images.
  • Tests run, with pass, fail and skip counts:
    • zig build test: 211/211 steps, 2960 of 2974 tests passed, 14 skipped, 0 failed. Main: 211/211 steps, 2959 of
      2973, 14 skipped. The difference is the new test.
    • zig build test-kernel-contract: 65 of 65.
    • zig build test-golden: 306/319 and 26/26 equal to the server goldens, 0 unexpected differences.
    • zig build fuzz-cortheon: 11 of 11.
    • zig fmt --check zig build.zig: clean on main and on the tip.
    • tools/zig/lean_check.py: 444 problems on the tip, 444 on main, 0 new (tools/zig/lean_new.py).
    • zig build fatbins -Dsm=121 (182/182 steps) and zig build native -Doptimize=fast -Dstrip=true (7/7) succeed.
    • The new test alone fails with nemotron cuda: a prompt pass with images runs the RADIO tower once, not twice #1057 reverted and passes on the tip (above).
  • Not run, and why:

Checklist

  • Every token still goes through the lane rounds. No serial path, and nothing that needs drafts off.
  • Drafted output equals "draft": false, a resumed prompt equals a fresh one, and concurrent equals solo. (Serving code is unchanged while the hook is null; no GPU run.)
  • No precision traded for speed. If the bits change, the description says which sums change and why. (No sums change.)
  • A new or changed GPU kernel is registered with the kernel contract (docs/kernel-contract.md); each backend's SUMMARY line is in the receipt. (No kernel changed.)
  • Prompt processing is no slower than the last release. (One null check a prompt pass with images; not measured.)
  • The description says which platforms I ran, Metal M1 to M5 and CUDA, and which I could not.
  • New tests fail before the change, pass after it, and skip cleanly without their dependency. (Fails with nemotron cuda: a prompt pass with images runs the RADIO tower once, not twice #1057 reverted, passes on the tip; it needs no GPU, so it never skips.)
  • Comments and docstrings are one line. No measurements or history in the source.
  • No personal data, machine names, internal hosts or local paths. No AI attribution lines.

…ot twice

The merge of ashhart#897 and ashhart#938 put a `Vision.begin` where Bolt's branch set up its image rows, after the kept-prefix
restore, and kept the one Super VL's images brought after the prompt-rows setup. The second ends the first pass and
encodes every image again, so each image request ran the tower twice before its first chunk. The first call stays.
…st encodes each image once

`Vision.encoder`, when set, takes a prompt pass's planned images in the tower's place, as Flash Next's `encoder` does
(a `ctx` and an `encode` function). The new host test runs the actual lane prefill with a counting encoder and checks
one encode per image per request; it fails with the duplicate `Vision.begin` that ashhart#1057 removed.
@plotarmordev

Copy link
Copy Markdown
Contributor Author

Linux CI crashed in cuda_vision_offer's test: it builds a Vision from undefined, so the new encoder field held garbage and begin called through it. 9184a8c sets it to null there; zig build test now gives 211/211 steps, 2960/2974 passed, 14 skipped. zig-windows fails the same way on main's other open PRs (#1057, #1058), in digest_key.zig and std.DynLib, which this branch doesn't touch.

@ashhart

ashhart commented Oct 22, 2026

Copy link
Copy Markdown
Owner

Thanks @plotarmordev, this is landed on main as 2e81b6c and 4cd72a5, with you as the author. Your first commit's fix, one RADIO tower pass for a prompt with images, was already on main when we carried it (the duplicate Vision.begin went with #1057), so it is not repeated; the encode hook and the host test that runs the real lane prefill and counts one encode per image per request now guard it. The null encoder keeps the tower path unchanged for serving, and the host suite passes on main with both commits.

- Synthema, Ash's AI Assistant.

@ashhart ashhart closed this Oct 22, 2026
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