llm: force f32 cuBLAS compute for qwen25vl runners - #215
Conversation
PR #214 localized the qwen2.5vl:3b poison-image garbage ('?'x31, poisoned slot, quant-independent) to the vision tower/merger's f16-weight matmuls running as fp16-accumulate cuBLAS GEMMs on CUDA/HIP, while the healthy CPU path accumulates fp32. Stock ggml at pin b10488 ships a process-wide override, GGML_CUDA_CUBLAS_COMPUTE_TYPE; a runner serves exactly one model, so setting =f32 in the qwen25vl runner's subprocess env scopes the fix to that family with no payload patch, no behavior change for any other model, and Go-only deployability. An operator-set value always wins (=f16 reproduces stock behavior for A/B). The per-op form (compat patch 904 setting GGML_PREC_F32 on the clip graph) stays shelved on fix/clip-mm-prec-f32 as the upstreamable fix and the fallback if a future pin drops the env knob; llama/README.md's update checklist now carries that hazard. See docs/maxusai/qwen25vl-cublas-f32-env.md; probe shape and decision table in docs/maxusai/clip-mm-prec-f32-validation.md (its step 0 exercises this exact mechanism). Prepared ahead of the #214 release A/B; no GPU probe run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PR #214 comment 5421905441: the poison class ships in the latest release (HHXH on 0.33.0) and GGML_CUDA_CUBLAS_COMPUTE_TYPE=f32 heals it there (HH); three-way A/B is causal (f32/bf16 heal, f16 reproduces). bf16 healing confirms range-not-mantissa — cuBLAS bf16 GEMMs accumulate fp32 — so the gate keeps f32 (CPU-matching) and bf16 stays the operator-selectable perf alternative. Runbook step 0 resolved to "ship the gate"; remaining validation is the gated-fork-build dress rehearsal and preflight. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Reviewing as consolidator. The causal confirmation is what elevates this above a mitigation, and the shape of the fix is the right trade.
The localisation is consistent with what #214 could support and no further: fp16-accumulate cuBLAS GEMMs in the vision tower/merger, against a CPU backend that always accumulates fp32, explaining quant-independence and CPU-health without needing either to be assumed. Why the env shape is right hereThree properties, and the middle one is the one I care about most:
Keying on arch and letting an operator value win follows the pattern #180/#186 settled for the memory limit and #212 for the thrash check: derived default, explicit override, never silently overwritten. Three knobs now share that shape, which is worth more than any of them individually. The perf note is honest and I would keep it visibleVision encode pays f32 GEMMs; text ~nil for quantized tags; Two things I would want before deploy
Shelving compat patch 904 as the upstreamable form and the fallback if a pin drops the knob — with the hazard recorded in |
Dress rehearsal on a gated build: HHH / HXX / HHH — gate heals, override reproduces, bf16 also healsBuilt
Also demonstrated in passing: the Go-only binary swap deploy path works — a branch-built Remaining before deploy: standard preflight on a fully built image. 🤖 Generated with Claude Code |
Turing check: gate heals on the 2080 Ti too, and bf16 is safe (but earns nothing) on sm_75Same rig as the dress rehearsal above — branch-built
🤖 Generated with Claude Code |
…ywhere Dress rehearsal completed on both failing GPU generations (PR #215 comments 5422486826, 5422669657): gate default HHH with f32 verified in the runner env on Blackwell and Turing; =f16 override wins and reproduces stock garbage (Blackwell sticky HXX, Turing recovering HXH); =bf16 override wins and heals on both including sm_75, at timing parity with f32. f32 stays the default — closest to CPU numerics, bf16 buys nothing measured. Remaining before deploy: preflight on a full image. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Gate validated against the shareable synthetic trigger — evidence now rests on public artifacts onlyFollowing #214's synthetic-repro update (
Two observations beyond the pass:
#214's mechanism comment lands on exactly this PR's shape as the fix's final form: "the multimodal matmuls of this family cannot safely run fp16 accumulation… 🤖 Generated with Claude Code |
A run-once, profile-keyed defect-class canary (#214 / PR #215): render the synthetic 1.06x-fp16-ceiling checkerboard (pixel-identical to docs/maxusai/make_poison_repro_image.py — stdlib PNG writer, verified pixel-equal against the PIL render), send it to qwen2.5vl:3b-q4_K_M on a fresh slot, require a healthy decode, then a text follow-up on the same slot to require no poisoning residue. Profiles without a [poison.<profile>] entry SKIP; recorded for cuda-dynres-903 with provenance. Runs even when --arch narrows the run, since it loads its own model. is_degenerate_decode keys on the measured fingerprint (done_reason null or one repeated glyph) and deliberately NOT on short/empty responses, which the num_predict trap produces for unrelated reasons. Validated both ways on live builds: PASS inside a full harness run against maxusai/ollama:pr215-gate-rc0 (gate live; VERDICT PASS), FAIL with the recorded fingerprint against ungated sync-0.32.15. test_verdicts.py: 50 tests OK (7 new). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Preflight now asserts this fix on every future build:
|
Preflight complete: full CUDA matrix PASS on the gated image — deploy-readyImage under test:
Reading of the result: token ladders 5/5 per arch, text baselines, payload proofs, think+format, and pinned budgets all match recorded expectations exactly — the gate provably changes nothing outside qwen25vl — while the poison probe asserts the qwen25vl fix itself, in both directions. That closes the last pre-deploy item from the rehearsal comments. Evidence chain for this PR, end to end: mechanism measured (#214), kill-switch on stock latest, gated-build rehearsal on Blackwell + Turing, synthetic-trigger validation, and now a passing preflight with a permanent regression canary. 🤖 Generated with Claude Code |
…tudy Investigation of the qwen2.5-vl fp16-accumulate class (#214/#216, fixed by #215 and filed upstream as ollama#18070). Squashed to a single commit deliberately: earlier revisions of this branch contained model-generated descriptions of client corpus imagery, and squashing ensures no revision reachable from this branch ever did. Every result file here carries verdicts, token counts and fingerprints only; image content is redacted, and no client image is committed. Contents: - Synthetic triggers: generators (waves 1-5, 199 candidates) and the two shareable artifacts -- trigger_checker56 (clip path, 0.30+) and trigger_stripes56 (spans 0.24.0 and the clip path). Both are generated black/white patterns. - DTYPE-TRACE.md: stage-by-stage dtype validation of 0.7.1's Go engine (preprocessing conforming; [0,1] and CLIP bounds verified empirically), the CPU/GPU split, decoder-divergence and perturbation studies, and the node localisation on the clip path -- ffn_down-31, 3 inf of 15.7M. - Tooling: fp16_audit.py (GGUF-only exposure screen), clip-nan-tracer.patch (non-finite eval callback), screen_activation_peak.py, measure_channel_thresholds.py, simulate_fp16_accum.py, repeat_sweep.sh. - Blast radius: six vision models screened and probed; gemma4 and granite carry no F16 vision matmuls at all; 105 repeat observations under forced fp16 on the models the gate excludes, with a positive control. - Corrections recorded in place: single-product overflow, the 256/255 luma gain as causal, peak activation as a sufficient objective, and the typhoon checkerboard claim -- each falsified by measurement. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tudy Investigation of the qwen2.5-vl fp16-accumulate class (#214/#216, fixed by #215 and filed upstream as ollama#18070). Squashed to a single commit deliberately, and result files carry NO image descriptions. Probes in this study ran against client corpus photos and their derivatives, and the model's answers describe client property; every `head` field of descriptive text has been removed, leaving verdicts, token counts, done_reason and -- where the model produced one -- the degenerate single-glyph fingerprint, which is not image content. No client image is committed; the only images here are two generated black/white patterns. Contents: - Synthetic triggers: generators (waves 1-5, 199 candidates) and the two shareable artifacts -- trigger_checker56 (clip path, 0.30+) and trigger_stripes56 (spans 0.24.0 and the clip path). - DTYPE-TRACE.md: stage-by-stage dtype validation of 0.7.1's Go engine (preprocessing conforming; [0,1] and CLIP bounds verified empirically), the CPU/GPU split, decoder-divergence and perturbation studies, and the node localisation on the clip path -- ffn_down-31, 3 inf of 15.7M. - Tooling: fp16_audit.py (GGUF-only exposure screen), clip-nan-tracer.patch (non-finite eval callback), screen_activation_peak.py, measure_channel_thresholds.py, simulate_fp16_accum.py, repeat_sweep.sh. - Blast radius: six vision models screened and probed; gemma4 and granite carry no F16 vision matmuls at all; 105 repeat observations under forced fp16 on the models the gate excludes, with a positive control. - Corrections recorded in place: single-product overflow, the 256/255 luma gain as causal, peak activation as a sufficient objective, and the typhoon checkerboard claim -- each falsified by measurement. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes the qwen2.5vl poison-image garbage decode from #214 by forcing fp32 accumulation in the model's runner — without patching the llama.cpp payload.
What
When the launcher starts a llama-server runner for arch
qwen25vl,applyArchServerEnvs(llm/llama_server.go) setsGGML_CUDA_CUBLAS_COMPUTE_TYPE=f32in that subprocess's environment. Each runner serves exactly one model, so stock ggml's process-wide cuBLAS compute override becomes model-scoped. An operator-set value always wins (=f16reproduces stock behavior for A/B;=bf16is the perf-friendlier alternative — cuBLAS bf16 GEMMs also accumulate fp32). Every other arch launches bit-identical to today.Why
#214 localized the
'?'×31garbage (quant-independent, CPU-healthy) to the vision tower/merger's f16-weight matmuls running as fp16-accumulate cuBLAS GEMMs on CUDA/HIP (compute_type = src0->typeat pin b10488), while the CPU backend always accumulates fp32. Confirmed causally at runtime (comment): the class is alive in the latest upstream release (HHXHon stock 0.33.0) and the knob heals it (HH), withf32/bf16healing andf16reproducing. No upstream version bump saves us — and our current build sits inside the 0.32.10–0.32.15 sticky-slot window, where one poison request contaminates the resident runner until reload.Why this shape
The per-op form (compat patch 904:
GGML_PREC_F32post-pass over the clip graph, compile-validated) is shelved onfix/clip-mm-prec-f32as the upstreamable fix and the fallback if a future pin drops the env knob;llama/README.md's update checklist now carries that hazard.Scope notes
qwen25vl: 3b/7b/32b) — the class is latent per-graph, each implementation has its own poison set. qwen2vl shares the clip graph builder but has no measured trigger and stays stock. A 3B-only variant (key on embedding width 2048) is a one-line change if 7B baseline continuity is preferred.-fp16text tags pay an fp32-GEMM prefill cost.docs/maxusai/qwen25vl-cublas-f32-env.md; probe shape and decision table:docs/maxusai/clip-mm-prec-f32-validation.md(on the sibling branch).Validation
TestApplyArchServerEnvs: forces f32 / operator override wins / qwen2vl and gemma4 untouched — pass.go build+./llm/...tests green (golang:1.26 container), gofumpt clean.HHH;-e GGML_CUDA_CUBLAS_COMPUTE_TYPE=f16→ garbage must return — then the standard preflight.🤖 Generated with Claude Code