Skip to content

cuda: splice image rows into the dense engine's prompt chunks - #2768

Merged
ashhart merged 6 commits into
ashhart:mainfrom
laudney:pr-dots-b
Nov 14, 2026
Merged

ashhart merged 6 commits into
ashhart:mainfrom
laudney:pr-dots-b

Conversation

@laudney

@laudney laudney commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor
Before (#2673) After
A dense recipe with an image tower no place for the tower or its rows a recipe may declare a Tower, checked at compile time (checkTower)
The engine's declarations to the host one set (text) text-only: #2673's 20 declarations, unchanged; with a tower: also Options.vision, Loaded.vision, visionBytes
Image rows — each lane owns its request's rows, charged per lane in the page planner; images encode once when the lane begins; a prompt chunk can cut through an image
Image refusals — worded: DenseImageSpan (names --vision-image-tokens), DenseServedWithoutVision, DenseTowerWidth
Served models text unchanged: no recipe declares a tower in this piece (dots.mocr does in piece e)

What this changes

The dense engine read text prompts only, so a recipe with an image tower had nowhere to put its rows. This adds the engine's image path and tests it with a fake tower; no served recipe uses it yet.

  • zig/src/cuda_dense/recipe.zig: the Tower contract (limits, marks, open / prepare / encode / explain with their fallibility, an output width equal to the decoder's hidden size), checked by checkTower.

  • native.zig: Native(R) is one shared core with two variants. The text-only variant keeps exactly cuda: a shared engine for dense decoders, with dots.mocr's text decoder on it #2673's declarations; the tower variant adds the vision options, field and bytes the host reads.

  • images.zig, backend.zig, pass.zig: each lane holds a buffer of the request's image tokens (the first at open, the others at attach). These bytes join every stream's bytes and fixed part, so the planner lowers an automatic lane count and an admitted request never allocates. The lane encodes its images in order when it begins, with a cancel check before each, and each prompt chunk copies the image rows it cuts over its embedding rows.

  • The image span refusal names --vision-image-tokens, the flag the CLI accepts; a host test fails if the words go back to --image-tokens.

  • Model licence: n/a, no weights are read.

  • Issue: Refs Native OCR models on HIP: dots.mocr, GLM-OCR, LightOnOCR-3 and Nemotron Parse, CUDA to follow (tracker) #1927 (item 11). Remaining after this stack: Nemotron Parse and GLM-OCR on this engine.

  • Stacked on: cuda: dots.mocr's vision rotary and patch bias kernels #2767 (piece 1, 4cea19ea3). This PR's commits: 5aad7bf24 feat(cuda): splice image rows into the dense engine's prompt chunks; 61f6e54ec test(cuda): pin the dense engine's tower width refusal; 81041a261 test(cuda): a prompt chunk through an image copies its rows; d9f9a476d docs(cuda): state encode's row count in the Tower contract; 882648a03 style(cuda): move three long trailing comments above their lines. Piece 2 of 7.

  • Conflicts: a local test merge of the stack's top cd1cb0baa into main 6b739dd5e is clean (git merge-tree).

Checks on this head

  • Head commit: 882648a03
  • Review fixes (Ash's 21:08 points) are new commits on this piece, listed above. On this head (Linux x86_64): zig build test-cuda-host, check-native-cuda-host 7/7, test-kernel-contract, zig fmt --check and git diff --check pass; lean 386 / 386, 0 new against piece a. On the GB10 at the stack's head cd1cb0baa: dots-image and dots-text (with --vision and text only) equal their pins; the projection check passes 24 of 24 and fails a planted value two BF16 steps off. The checks below ran on the earlier heads, whose code equals this head's but for these fixes.
  • Lean checker, python3 tools/zig/lean_check.py, on the base (cuda: a shared engine for dense decoders, with dots.mocr's text decoder on it #2673's head 05e5e5fdd) and on this head: 386 / 386, 0 new (also 0 new against piece a).
  • Tests run (Linux x86_64, no GPU):
    • zig build test-cuda-host: 378/378 steps, 0 failed (the test binaries this piece changes re-ran; the others are the parent's and passed there).
    • zig build test-cuda-dense-host: 33/33 passed, uncached. zig build test-dots-mocr-cuda-host: 10/10 passed, uncached.
    • zig build check-native-cuda-host: 7/7 steps. zig build test-kernel-contract: 32/32 steps, 0 failed. zig build compile-cuda-tests (the GB10 runner compiles): 10/10 steps.
    • zig fmt --check build.zig zig, git diff --check: pass.
  • CI: pending on this head.
  • zig build test test-golden (Linux x86_64) on the stack's top 6bb38b3a1: 662/665 steps, 8,692/8,760 tests passed, 67 skipped; the one failure is cuda_disk_test, whose write deadline fails on the base too under load.
  • Not run, and why: the GB10 device checks: the receipts of pieces d and e.

New host tests (in test-cuda-dense-host and test-cuda-host): the tower contract with a fake tower and each wrong declaration refused by name; both Native variants (the text-only one keeps exactly #2673's 20 declarations); the host's offer and options read through the real cuda_vision_offer.zig; per-lane bytes and image spans; chunk-cut intersections; lane drops on every failure path, each with a red control; the refusal words.

Receipt

n/a: no served recipe declares a tower in this piece, so no image reaches inference. The text path's shared core changes; its exactness and speed are measured on the stack's head in piece e's receipt (dots.mocr text unchanged against #2673; Qwen3.8-27B MLX tokens identical and speed within run-to-run spread against main).

Checklist

  • The title and description match the head commit, including the receipt and the stack.
  • New tests fail before the change, pass after it, and skip cleanly without their dependency.
  • Comments and docstrings are one line. No measurements or history in the source.
  • No personal data, machine names, internal hosts or local paths in the diff or its commits. No AI attribution lines.

@ashhart

ashhart commented Nov 10, 2026

Copy link
Copy Markdown
Owner

Thanks @laudney, this is a careful piece. The text-only variant keeps exactly #2673's declarations, and a test shows text-only bytes and layout unchanged; images are bounded by the request's token budget and each extra lane is charged in the page planner, with tests for both. A few small things:

  1. A test that fails if the width check in openTower (zig/src/cuda_dense/native.zig:183, t.width != hidden) is removed. Right now only the refusal's wording is tested.
  2. A host test on the fake device where a prompt chunk crosses an image and the expected device copies are seen, so it goes red if chunk passes no injects or the forward pass skips splice.
  3. A line in the Tower contract in recipe.zig saying encode writes exactly image.tokens rows at out, since the engine's buffer bound depends on it.
  4. Three trailing comments in images_test.zig (lines 41, 52 and 106) run past 120 columns; please move them above their lines.
  • Synthena (Ash's Agent)

@laudney
laudney force-pushed the pr-dots-b branch 2 times, most recently from 04ea94b to 882648a Compare November 11, 2026 08:14
@laudney

laudney commented Nov 11, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, all four are in:

  1. 61f6e54ec: a host test opens a fake tower whose rows are not the decoder's width and expects the refusal; with the t.width != hidden check removed it fails (openTower is now pub on Core only; Native's declarations are unchanged).
  2. 81041a261: on the fake device, a prompt chunk that cuts an image copies exactly the image rows it covers (the fake now records device-to-device copies; image marks at 7 and 20). It fails if chunk passes no images, and fails if the forward pass skips splice.
  3. d9f9a476d: the Tower contract in recipe.zig says encode writes exactly image.tokens rows at out.
  4. 882648a03: the three trailing comments in images_test.zig sit above their lines.

@ashhart

ashhart commented Nov 11, 2026

Copy link
Copy Markdown
Owner

Thanks @laudney, all four are in, and each new test fails with its fix removed and passes at this head. Ready from our side.

  • Synthena (Ash's Agent)

laudney and others added 6 commits November 14, 2026 03:00
The dense engine read text prompts only, so a recipe with an image
tower had nowhere to put its rows. A recipe may now declare a Tower,
checked at compile time against the contract in recipe.zig; each
lane owns a buffer of the request's image tokens, the first one at
open and the others at attach, and these bytes join every stream's
bytes and fixed part so the page planner shrinks an automatic count.
The lane encodes its images in order when it begins, with a cancel
check before each, and each prompt chunk copies the image rows that
it cuts over its embedding rows. Native(R) is now one shared core
with two variants: the text-only one keeps the old decl set
exactly, and the tower one adds the vision options, field and bytes
that the host reads. Host tests drive the tower variant with a fake
tower, also through the host's own offer and options code, and pin
that the image span refusal names --vision-image-tokens, the flag
the CLI accepts.
Only the words of DenseTowerWidth were tested, so dropping the width
check in openTower would pass the host suite. openTower is now
public, and a host test opens the fake tower against a decoder one
row wider and expects the refusal, closing the tower first if it
opens, then opens it at the decoder's width and without --vision.
No test drove a chunk that cuts an image through the backend, so a
chunk that passed no injects or a forward pass that skipped the
splice would stay green. The fake device now records its
device-to-device copies, and a host test runs two chunks cut inside
an image on a quiet recipe (no layer work): each must copy that
image's rows from the lane's feature rows to the chunk's embedding
rows, at the right row, offset and length.
The engine sizes each lane's feature rows by the request's image
tokens and copies image.tokens rows an image from them, so the Tower
contract now says that encode writes exactly image.tokens rows of
the tower's width at out.
Three trailing comments in images_test.zig ran past 120 columns;
each now sits on its own line above the code it describes.
@ashhart
ashhart merged commit 90b4fae into ashhart:main Nov 14, 2026
2 of 10 checks passed
@ashhart

ashhart commented Nov 14, 2026

Copy link
Copy Markdown
Owner

Thank you @laudney. This is merged into main with your commits under your own name, so you're listed as a TensorFold contributor. It ships in the next release, credited to you in the changelog and the release notes.

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