Skip to content

nemotron omni: video input through the RADIO tower - #940

Merged
ashhart merged 6 commits into
ashhart:mainfrom
MiaAI-Lab:nemotron-omni-video
Oct 18, 2026
Merged

ashhart merged 6 commits into
ashhart:mainfrom
MiaAI-Lab:nemotron-omni-video

Conversation

@MiaAI-Lab

@MiaAI-Lab MiaAI-Lab commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Refs #937. Stacked on #938: this branch contains that PR's two commits; review the last commit only (nemotron omni: video input through the RADIO tower). I will rebase when #938 moves.

Restacked. This branch now sits on #938's new tip (a6660d38, which adds structured output); the video commits are unchanged apart from a doc conflict resolved in docs/recipes/nemotron-3.5-bolt.md. CI is green on 4c5f1748.

An earlier push of this branch failed CI's lean check: zig/src/core/engine_api.zig went to 602 lines (limit 600; the check counts new problems per file, so an offsetting fix elsewhere does not count). The video offer types (VideoSource, FrameSink, PreparedVideo, VideoOffer) now live in zig/src/core/video_offer.zig, re-exported by engine_api.zig under the same names, which is 568 lines; no other file changed meaning.

What this changes

Video input for a Nemotron Omni checkpoint that has a video projection (video_embedder, video_temporal_patch_size 2), through the RADIO tower and the existing video server path. Written and tested on Bolt; Super VL has the same tower and I have no Super VL weights to run it on.

  • families/radio: the processor's video grid rule (_compute_target_patches_video, checked against its outputs for eleven sizes), the tubelet weights and projection (a pair of frames' patches side by side through video_embedder), and video.zig: frames sampled at 2 a second (1 a second above 720 rows, at most 128 there), spread over the clip past the cap, paired (a lone last frame with itself), with the words the processor writes before each pair ("Frame 1 sampled at 0.00 seconds and frame 2 sampled at 0.50 seconds: ", a newline before later pairs). video_token is <SPECIAL_30>, the checkpoint's first spare special token.
  • engine_api / video_offer.zig: VideoOffer.video_text (the text the template writes for a video, swapped for video_token's own text before encoding) and PreparedVideo.labels (words before each group, in place of the trailing time). The tokenizer has no <video> token and splits it by its neighbours (< video >, ><, >\n), so matching ids would refuse valid requests. User text that spells it stays text, as for image markers.
  • server: --vision-max-videos (1 to 16; the engine's max_videos replaces the constant 2 in images.zig).

Receipt

  • Environment: as nemotron: serve NVIDIA Nemotron 3.5 Bolt (nemotron_h_puzzle) on CUDA #938, plus FFmpeg 9.0.2 (tools/zig/build_ffmpeg.sh) and zig build video; libjpeg-turbo 3.1.4.1 for JPEG.
  • Exactness: video prompts are not part of the repo's tools, and --alone / --serial were not run on video requests. On Bolt, measured on the video code as submitted: a clip of a square moving left to right is described as moving from left to right; 4 videos and 50 images in one chat (30,536 prompt tokens, 29 s) answered correctly; a 130 s 720p clip is 36,558 tokens, 36.7 s. The same prompt expands to the same token count as the recipe's earlier implementation (30,536). Text-only requests do not reach this code; the concurrent-equals-solo and drafted-equals-plain results are in nemotron: serve NVIDIA Nemotron 3.5 Bolt (nemotron_h_puzzle) on CUDA #938.
  • Decode and prompt speed: n/a, text-only requests do not reach this code.
  • Tests run: zig build test (added: video grids for eleven sizes against the processor's outputs; pairs, a lone last frame, hashes, labels and a too-small token budget through a fake source; the Omni video config; the offer's max_videos). zig fmt --check build.zig zig: clean. lean_new.py against the base: 517 problems, 517 on the base, 0 new. The tip builds and passes the host suite.
  • Not run: Metal, other CUDA cards, Super VL, video drafted-against-plain and concurrent-against-solo.

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: shown for text in nemotron: serve NVIDIA Nemotron 3.5 Bolt (nemotron_h_puzzle) on CUDA #938, not run for video requests.
  • No precision traded for speed: the tower's arithmetic is unchanged; the video path adds a second projection weight and a different patch width.
  • No new or changed GPU kernel.
  • Prompt processing is no slower: text requests do not reach this code.
  • The description says which platforms I ran: CUDA on one GB10 only.
  • New tests fail before the change and pass after it; the host suite needs no GPU or library.
  • 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.

A request's image tokens were capped at 16,384 on the command line. A model with a 262,144-token window and an engine
that encodes an image as its prompt pass reaches it (no rows buffer for the whole request) can take more, up to the window;
the per-image budget is still the total over the request's images.

@cwschroeder cwschroeder left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CPU review of the video delta at 83c810da3f719c8e6239d46c48587e53c0cc9a17, above #938, against main 46a08c18. Two integration issues are reproduced below.

Marker patch and standalone capacity reproducer. The marker fix is commit f49514e2f44081457a016beb3764fcf21d1bd948; its tests cover both literal spellings and empty/identical aliases. All 372 server CPU tests pass. The separate capacity diagnostic produces two passing controls and two failing capacity assertions in both Debug and Fast.

With the marker patch, the four exact No GPU test commands, whole-tree Zig format check and whitespace check pass locally on macOS arm64 with Zig 0.17.0. The full lean gate remains red: engine_api.zig has 602 lines. I reproduced that on the unchanged PR head; it is also the current remote No GPU failure (run 38901869867). The marker patch adds no further lean problems. Desktop, mobile and format checks on the original PR head are green.

The capacity fixture exercises host preparation/planning, with reduced pixel payload and enough context to isolate the rows limit. It initializes no CUDA context, device buffer or tower. This review does not qualify #938's private CUDA underlay or a GPU/checkpoint run.

Comment thread zig/src/server/images.zig Outdated
const s = try text.tokenString(a, id);
if (s.len > 0) try out.append(a, s);
}
if (v.video) |o| if (o.video_text.len > 0) try out.append(a, o.video_text); // user text that spells it stays text

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Shield the actual video token spelling as well as its alias

RADIO's mark_ids contains only image/open/close, so appending <video> leaves <SPECIAL_30> unshielded. A user quoting that spelling in ordinary text encodes video_token: expand then refuses a text-only request with “1 video placeholders for 0 video parts”. Beside one real video it counts two placeholders. The CPU regression fails on this head and passes when markerText also includes text.tokenString(o.video_token). The linked patch preserves ordinary user text and the template's genuine marker; it also covers empty and identical aliases.

Comment thread zig/src/native/cuda_vision_offer.zig Outdated
.group_open = x.marks.open,
.group_close = x.marks.close,
.markers = &x.mark_ids,
.video_tokens = o.video_tokens,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Bound the combined video/image rows to the Super-VL buffer

This offer exposes the default 16384 video tokens, while Nemotron Omni initializes its shared rows buffer from the default 13312 image_tokens. Actual video preparation and RADIO planning yield 16384 rows for a square 64-second source. A 52-second video fits alone at 13312 rows, but adding one minimal image gives 13568. The server budgets images and videos separately, and the Super-VL lane passes their combined list to Vision.begin, which returns ImageRows before tower execution. Please enforce a combined capacity bound or size the buffer accordingly; clamping video alone leaves the mixed case. The linked CPU fixture includes fitting controls and reproduces both mismatches with sufficient context. Bolt's piecewise path is separate.

Bolt's config.json is a nemotron_h_omni wrapper around a nemotron_h_puzzle text model: per-layer expert counts, IQ2_XXS
experts, NVFP4 Mamba projections, Q8_0 attention. The serve path reads its model type as nemotron_h_puzzle when an omni
config wraps one, and the CUDA host opens it with a lane backend over libbolt.so.

- families/nemotron/bolt_native.zig: the backend (prompt pieces, shared verify windows for up to 16 streams, rollback, the
  LiLiCorr drafts, keyed draws, prompt-cache copies, images through the RADIO tower).
- zig build bolt -Dnvcc -Dbolt-src: builds libbolt.so beside the server; the backend loads it from there and checks its ABI.
- prompt cache rules: a family may set snap_lookahead (Bolt has no head reading ahead: 0).
- docs/recipes/nemotron-3.5-bolt.md: build, limits and receipts.
@MiaAI-Lab
MiaAI-Lab force-pushed the nemotron-omni-video branch from 83c810d to 6a1b40f Compare October 16, 2026 14:25
@MiaAI-Lab
MiaAI-Lab marked this pull request as ready for review October 16, 2026 14:26
MiaAI-Lab added a commit to MiaAI-Lab/TensorFold that referenced this pull request Oct 16, 2026
The server budgets a request's images (--vision-image-tokens) and videos (--vision-video-tokens) apart, but the Omni
lane plans them together into one rows buffer that Vision.init sized from the image budget alone. A 64-second video
(16,384 rows) or a 52-second video beside one image (13,568) therefore failed with ImageRows before the tower ran.

Limits gains video_tokens; the rows buffer holds image_tokens + video_tokens when the checkpoint has a video projection,
and the video offer never exceeds the budget the buffer was sized for. Bolt encodes the images each prompt piece crosses,
so it keeps both totals as server limits only. The test pins the property the sizing relies on: a prepared video never
holds more visual tokens than its budget.

Reported by cwschroeder in the review of ashhart#940.
@MiaAI-Lab

Copy link
Copy Markdown
Contributor Author

Thanks for the review, @cwschroeder, and for the reproducer: both findings were real and are fixed on this branch (07af207d).

1. Marker shielding. I applied your video-marker.patch as it is, with git am, so the commit keeps your authorship (c790c00a). Its three tests run in the server suite and pass; the first two failed on the old head, as you described.

2. Shared rows capacity. Fixed in 07af207d. Vision.Limits gains video_tokens, and Vision.init sizes the rows buffer for image_tokens + video_tokens when the checkpoint has a video projection, so the two budgets the server hands out separately fit the one buffer Vision.begin plans into. The video offer takes min(--vision-video-tokens, the budget the buffer was sized for), so it cannot promise more than the buffer holds. This is the "size the buffer accordingly" option rather than a combined clamp, so the mixed case (52 s plus one image) is covered too. The default Super VL buffer grows from 13,312 to 29,696 rows, about 130 MB more. Bolt's piecewise path keeps both totals as server limits only and is unchanged.

The new test pins the property the sizing relies on: a prepared video never holds more visual tokens than the budget it was given (budgets 16,384, 5,000 and 600), and the smaller budgets fill to within one group. I did not run your run_capacity.py fixture itself, and I have no Super VL weights, so the Super VL lane is not exercised on a GPU by me.

Checks on this head (macOS is not available to me; Linux, Zig 0.17.0): zig fmt --check build.zig zig clean; zig build test passes in full; lean_new.py against 46a08c18 reports 0 new problems (517 on both). engine_api.zig is 568 lines since the video offer types moved to core/video_offer.zig, so the 602-line failure you reproduced on the earlier head is gone.

@cwschroeder cwschroeder left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both reviewed issues are addressed at 07af207d. The adopted marker commit c790c00a preserves the offered patch: all three changed server files are byte-identical to f49514e2. The current server CPU suite passes 377/377 tests.

I rechecked the Super VL budget fix with an updated CPU diagnostic. It copies the actual Vision.init capacity expression and allocation argument, then exercises the real offer, image/video preparation and row planner. All six cases pass in Debug and Fast, including the 64-second video plus image (16,640 rows within 29,696) and a reserved video budget of 512 that clamps the offer. The old budget logic fails four of these invariants while the two original controls pass.

Source review confirms both limits reach the allocation and the offer cannot exceed the reserved video budget. All four current-head CI checks are green; the earlier whole-tree lean blocker is also resolved.

This approval closes the reviewed marker/default-and-mixed Super VL budget findings. The diagnostic does not initialize the loader, tower, row allocation or CUDA context, and supplies no tower/checkpoint numerical or performance qualification. Bolt's separate piecewise path and its non-default budget combinations were not exercised by this diagnostic.

@cwschroeder cwschroeder left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up to my earlier review at 07af207d: a separate Bolt case appears with low, independent image/video budgets. The previously checked Super VL and marker cases remain fixed.

One-line reservation patch and CPU reproduction, candidate cca9f21c. Three video cases return ImageRows on this head; three controls pass. With the patch, all seven tests pass in Debug and Fast, including 72 reservation boundary cases. The fixture calls the actual CLI, offer and RADIO video preparation, and copies the exact allocation expressions and complete imageRuns method; tower/synchronization are inert probes.

Whole-tree format, whitespace and lean comparison pass (517/517, zero new). The actual Bolt.open body also compiles for Linux arm64 without execution. Source review found no issue in the patch. This candidate has not run the complete workflow suite or remote CI, and the CPU fixture does not qualify libbolt, CUDA numerics, full mixed-media/chunk coverage or throughput.

var problem: []const u8 = "";
if (o.vision) |lim| {
// the rows buffer holds one prompt piece's images; a request's token totals are the server's limit alone
tower = (try vision.Vision.init(gpa, io, ctx.d, dir, .{ .image_tokens = @min(lim.image_tokens, image_rows), .max_images = lim.max_images }, &problem)) orelse {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Reserve video rows in Bolt's bounded prompt-piece pool

--vision-image-tokens 16 is accepted independently of the default 16,384 video tokens. This initializer reserves only 16 rows, then the following assignments expose the full video budget. A one-second square clip prepares a 256-row group, so imageRuns returns ImageRows before the tower call. With image budget 256, an eight-second clip fails on a later group in its first piece. The server budgets video parts independently (images.zig:247-260), so the image budget does not reject that video beforehand.

The offered patch passes a video reservation capped to the space left below image_rows. Vision.init includes it only for a checkpoint with a temporal projection; image-only allocations stay unchanged, and the pool remains capped at 16,384 rows.

MiaAI-Lab and others added 4 commits October 16, 2026 21:29
response_format, the guided_* fields and tool_choice "required" or a named function were refused with HTTP 400: the
backend offered no masked draws, so the engine advertised no structured output, and the server could not read an omni
checkpoint's vocab_size (it sits in llm_config).

- bolt_native: verify redraws the rows of a masked window among the tokens their bits allow (rows x words, the grammar's
  layout), first_masked draws the first token again under the first mask, and held() gives the grammar the drafts the
  drafter holds. A masked row with fewer allowed tokens than candidates is drawn from the allowed ones only.
- libbolt (bolt_mask_rows, C ABI 2): a masked variant of the two-stage GPU top-k, which treats a disallowed token as
  minus infinity; only the masked rows are recomputed.
- server/vocab.zig: the vocabulary size comes from text_config, llm_config or the top level, in that order.
- The libbolt binding moves to bolt_api.zig, which keeps bolt_native.zig under 600 lines.
A Nemotron Omni checkpoint with a video projection (video_embedder, video_temporal_patch_size 2) takes videos: frames
sampled at 2 a second (1 above 720 rows, at most 128), sized by the processor's video rule, two to a tubelet whose patches
sit side by side, and labelled "Frame i sampled at t seconds and frame j sampled at u seconds: " before each pair as the
checkpoint's processor writes them.

- radio: video grid rule, tubelet weights and projection, sampling and packing (video.zig), its tests.
- engine_api: VideoOffer.video_text (the template's own placeholder, swapped for video_token before encoding, since
  "<video>" tokenizes by its neighbours) and PreparedVideo.labels (words before each group, no trailing time).
- server: the offer's max_videos limit replaces a constant; --vision-max-videos; user text that spells the video
  placeholder stays text.
The server budgets a request's images (--vision-image-tokens) and videos (--vision-video-tokens) apart, but the Omni
lane plans them together into one rows buffer that Vision.init sized from the image budget alone. A 64-second video
(16,384 rows) or a 52-second video beside one image (13,568) therefore failed with ImageRows before the tower ran.

Limits gains video_tokens; the rows buffer holds image_tokens + video_tokens when the checkpoint has a video projection,
and the video offer never exceeds the budget the buffer was sized for. Bolt encodes the images each prompt piece crosses,
so it keeps both totals as server limits only. The test pins the property the sizing relies on: a prepared video never
holds more visual tokens than its budget.

Reported by cwschroeder in the review of ashhart#940.
@ashhart

ashhart commented Oct 17, 2026

Copy link
Copy Markdown
Owner

Thank you, @MiaAI-Lab, the shared RADIO video preparation and marker fixes are useful additions to the native media path.

We checked 4c5f174. Super VL now reserves both budgets, but bolt_native.zig:458-465 still allocates from image tokens alone before advertising video tokens. With a 16-token image budget, a normal video pair can exceed the actual row pool.

We can repair that bounded-pool accounting and add low-independent-budget/mixed-media regressions at landing. After the combined Bolt base, we will qualify the video delta on it and Super VL separately. Please retain paired-frame references plus video drafted/plain, resumed/fresh and concurrent/solo cases; the current descriptions and inherited text checks leave those device gates open.

Posted by Ash's AI Assistant Synthena

@ashhart ashhart mentioned this pull request Oct 17, 2026
@ashhart
ashhart merged commit 6e4a0b6 into ashhart:main Oct 18, 2026
4 checks passed
@ashhart

ashhart commented Oct 18, 2026

Copy link
Copy Markdown
Owner

Thank you, @MiaAI-Lab, for video, @cwschroeder for marker shielding, and @BobClawblaw for the Nemotron foundation. 1.1.0 includes #940 (6e4a0b649) through the existing RADIO tower and shared Nemotron engine. Frames carry timestamps and a separate video-token budget. Reserved video-marker text stays text. Configuration, frame rate, indices, dimensions and grids are checked before decode/casts, with completion-aware media ownership. Video fidelity, draft/plain, cached/fresh, concurrent/solo and resource results: Host/CUDA contract checks passed; actual Super VL video fidelity and video-cache/concurrency cells remain unrun.

Posted by Ash's AI Assistant Synthena

BlivionIaG pushed a commit to BlivionIaG/TensorFold that referenced this pull request Oct 20, 2026
The server budgets a request's images (--vision-image-tokens) and videos (--vision-video-tokens) apart, but the Omni
lane plans them together into one rows buffer that Vision.init sized from the image budget alone. A 64-second video
(16,384 rows) or a 52-second video beside one image (13,568) therefore failed with ImageRows before the tower ran.

Limits gains video_tokens; the rows buffer holds image_tokens + video_tokens when the checkpoint has a video projection,
and the video offer never exceeds the budget the buffer was sized for. Bolt encodes the images each prompt piece crosses,
so it keeps both totals as server limits only. The test pins the property the sizing relies on: a prepared video never
holds more visual tokens than its budget.

Reported by cwschroeder in the review of ashhart#940.
BlivionIaG pushed a commit to BlivionIaG/TensorFold that referenced this pull request Oct 20, 2026
…he reserved video token shielded in user text

ashhart#940 samples a video's frames into groups the RADIO tower reads as images, each with its time and the words before
it, and budgets video tokens apart from image tokens. It joins our vision offer beside the Qwen, Flash Next and GLM
towers: Nemotron Omni's video comes from its own prepare, bounded by the serve flags' video and frame counts, and
the template's `<video>` becomes the reserved token before encoding.

With it, our checks: the video config, frame rate, frame indices, dimensions and grid are refused before any decode
or cast, prompt spans and row products before any device use, and every owner unwinds on a failed allocation. The
video types stay in engine_vision.zig with ashhart#940's labels and video text added; its video_offer.zig is not taken.
Test names lose their review labels and the video step's filter becomes optional.
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.

3 participants