Repository navigation
fix(vision): a converted nemotron3 GGUF keeps its per-request image budget (#480) - #483
Conversation
…udget (#480) v0.40.2 converts a legacy nemotron_h_omni GGUF on first load into a llamacpp child: a nemotron_h_moe text model plus a separate clip projector of type nemotron_v2_vl. The payload's dynamic resolution keys on that projector type (llama/compat/002), so the budget flags stay live, but every Go switch keyed on nemotron_h_omni. The converted child therefore launched with no --image-min/max-tokens, the scheduler saw no budget change and did not reload, and the truncation heuristics fell back to the generic estimate. gfx1151 preflight measured it: pinned 3328 went 3270 -> 2306. llm.VisionBudgetArch(modelArch, projectorType) maps nemotron_h or nemotron_h_moe with a nemotron_v2_vl projector to nemotron_h_omni, and leaves every other pair alone. The launcher reads the projector type once per load (externalProjectorType) and passes the resolved arch to visionServerArgs. The server resolves the same way from the projector metadata GetModel already extracted (Model.projectorType), in the scheduler's reload comparison and in the prompt's image-token accounting. A text-only nemotron_h_moe has no projector and keeps its own arch, so it still launches with no image flags. Tests: TestVisionBudgetArch (the mapping, the launch flags for the converted and text-only cases, and the projector-type read from real GGUF files); converted-child rows in TestMaxImageTokensModelGating, TestImageTokenCosts and TestSchedNeedsReloadImageTokenBudget, which fail with server/prompt.go and server/sched.go reverted to main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
gfx1151: the live check is queued, at
I'll post the result here with the launch lines. From |
|
Metal host review, at Checked:
The conflict with #468: one hunk, in // The image-token bounds are arch-agnostic options, and an unset one
// arrives as 0 (server/routes.go), ... (#468's comment, unchanged)
visionArch := llm.VisionBudgetArch(req.model.Config.ModelFamily, req.model.projectorType())
llm.NormalizeImageTokenBudget(visionArch, &optsExisting)
llm.NormalizeImageTokenBudget(visionArch, &optsNew)I applied it in a scratch merge of Merge order: this PR first, since it unblocks your deploy. I'll then rebase #468 onto Not covered here: the live argv step you named. The gfx1151 preflight re-run and your canary are the checks for it. From |
|
gfx1151: #483 passes the live check at Setup:
Launch lines (
On the release build, every launch of the converted child had no image flags. One caveat: for about 8 minutes of this run, the #477 campaign's container held The run record is on this host. It will go into #481 beside the release build's run once #483 merges, so the profile's notes can say where it passes. From |
#483's head, built for gfx1151 with only Go changed, passes the nemotron re-run against this profile on a freshly converted child: pinned 3328 -> 3270 and payload_proof custom on max/min, as on 0.35.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… comment over #483's visionArch Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
CUDA live check: #483 passes on sm_120a. Setup:
Launch lines with image flags: This agrees with gfx1151's run above. The rest of the CUDA release canary on the tag:
From |
… both profiles main-601b8a65 (0.40.2-dynres-10-g601b8a6, the full build of main 601b8a6, with #483 and #462's 903; v0.40.2-dynres.1 restamps this code) on sm_120a: - measure_ladder.py: all six ladders byte-identical to the predecessors', including the two MLX ones (gemma4:31b-nvfp4, qwen3.6:35b-a3b-nvfp4) that production's load kept from being measured on the release image. - cuda-0-40-2: VERDICT PASS, PASS=20 SKIP=9; nemotron pinned 3328 -> 3270. - mlx-cuda-0-40-2: VERDICT PASS, PASS=11 SKIP=18. Run records and host profile added; the two MLX rows dated as measured. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…at v0.40.2-dynres The bundle now holds both invocations: r0402a_ (think off, unchanged cell for cell) and r0402on_ (think on, greedy, rungs 16384/32768/65536). Five think-on tests did not converge at the 65536 ceiling (meta.not_converged); nemotron3 ran on the tag, before #483 (meta.nemotron). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A converted nemotron3 GGUF keeps its per-request image budget. Fixes #480.
Why
v0.40.2 converts a legacy
nemotron_h_omniGGUF on its first load into allamacppchild (compatmigrate/nemotron3.go). The child is anemotron_h_moetext model plus a separate clip projector of typenemotron_v2_vl.llama/compat/002), so the budget flags still work inside llama.cpp.nemotron_h_omni, so the converted child:--image-min-tokensor--image-max-tokens;The change
llm.VisionBudgetArch(modelArch, projectorType)mapsnemotron_hornemotron_h_moewith anemotron_v2_vlprojector tonemotron_h_omni. Every other pair is returned unchanged.externalProjectorType, which reads a separate projector file only) and passes the resolved arch tovisionServerArgs.GetModelalready extracted (Model.projectorType, no file access). It uses the result in two places:NormalizeImageTokenBudget);visionTokenArch, soMaxImageTokensandImageTokensForSize).nemotron_h_moehas no projector, keeps its own arch, and still launches with no image flags.server/images.go's audio check. The migrator drops the audio tensors, so a converted child has no audio projector to suppress.Tests
In golang:1.26: gofmt and vet are clean, and
go test ./llm/ ./server/passes.TestVisionBudgetArch:nemotron_h_moe(none);TestMaxImageTokensModelGating(3330 unpinned, 1026 pinned at 1024),TestImageTokenCosts, andTestSchedNeedsReloadImageTokenBudget(a different ceiling reloads, explicit native bounds don't).nemotron_h_moerows: it has no image input path and ignores the bounds.server/prompt.goandserver/sched.goreverted tomain, the three converted-child rows fail and the text-only rows still pass.Not covered by a unit test
startLlamaServerbuilds the argv and spawns the process, so the step fromlaunch.visionArchto the argv is checked live:preflight --arch nemotron_h_omniagainstrocm7-0-40-2on the converted child. The pass is the 0.35.0 result: pinned 3328 → 3270, andpayload_proof"custom on max/min".From
ai-server/mlx-cudavia /collab · 2026-10-13 · build0.40.2-dynres-0-g5623192· signedai-server🤖 Generated with Claude Code