Skip to content

feat(minimax-h3): load the bf16 TEXT ENCODER (14 shards, 63 GB) and measure what Q4_K_M costs the conditioning - #100

Merged
localai-bot merged 2 commits into
mainfrom
row/H3-ENC-BF16-COND-DIFF
Aug 7, 2026
Merged

feat(minimax-h3): load the bf16 TEXT ENCODER (14 shards, 63 GB) and measure what Q4_K_M costs the conditioning#100
localai-bot merged 2 commits into
mainfrom
row/H3-ENC-BF16-COND-DIFF

Conversation

@localai-bot

Copy link
Copy Markdown
Collaborator

Stacked on #99#98. Retarget to main once those merge — the diff below is this row alone (898 non-exempt lines).

What this enables

Every H3 render so far conditioned on a Q4_K_M Qwen3-VL-32B text encoder, and nobody had measured what that quantization does to the conditioning tensor. It was not measurable: --encoder only ever accepted a GGUF, and the unquantized tower ships as 14 safetensors shards, 63 GB.

  • MiniMaxH3EncoderConfigFromShards derives the geometry from the shard index's shapes alone, with the same recovery rules and the same non-shape defaults (rope_theta, mrope_section, rms_norm_eps, selected_layer) as the GGUF loader — so an A/B cannot end up comparing two different RoPEs.
  • StreamMiniMaxH3EncoderShardsToDevice fills the same MiniMaxH3EncoderDeviceWeights::views map the GGUF arm fills, over bf16 instead of ggml blocks. Projections upload directly out of the read-only mmap, and the [q|k|v] / [gate|up] row fusions are done on the device, into offsets of one allocation — so even the one transform this loader performs costs no host copy.
  • MiniMaxH3EncoderEmbedTokensFromShards gathers embedding rows straight out of the mmap'd shard; the table is [151936, 5120] and a prompt touches a few dozen rows.
  • A bf16-weight widen step in MiniMaxH3EncoderTextForwardDevice (scratch reused across layers), because vt::MatmulBT needs one dtype for both operands and these activations are f32. The 50 layers H3 runs are 48.8 GiB bf16 against 97.5 GiB f32 on a 122 GiB unified pool. bf16→f32 is exact, so this is a residency choice, not a precision one — and it is gated as such.
  • --encoder <dir> and --encoder-only in examples/minimax_h3_gen. --encoder-only skips the DiT entirely, dropping peak from ~96 GiB to ~49 GiB — on this box the difference between a run and an OOM reboot.

No new forward. The encoder graph is byte-for-byte the same code for both arms. That is what makes the quantization question measurable rather than arguable.

The measurement

Q4_K_M vs bf16 encoder, same 233-token prompt, same forward, on Thor:

per-token cosine 0.99745 mean, 0.909 min
rel RMS 0.0340 overall, 0.0685 excluding the attention sink
median rotation 3.5°
is it a scale change? No — best rescale only moves 0.0340 → 0.0328
calibration arm a one-word prompt edit moves the same energy, but touches 172/233 tokens; the quantization touches 232/233

So: the Q4_K_M encoder shifts the conditioning about as much as editing one word of the prompt does — but diffusely, spread across nearly every token instead of concentrated where the edit was.

Gates I ran, post-rebase

  • test_minimax_h3 75/75 cases, 55609 assertions, clean Release build of libvllm.a, test_minimax_h3, minimax-h3-gen. Full scripts/agent-preflight.sh: green.
  • Shards resolve, fuse and stream: a synthetic 4-shard encoder at the real name spellings; every fused view memcmp-exact against q ++ k ++ v / gate ++ up for every layer; unfused projections byte-exact; separate names gone; final norm, lm_head and the vision tower deliberately not bound; truncation honoured; embedding gather exact and out-of-range throwing.
  • The loader RAN, and is not the GGUF path: MiniMaxH3EncoderShardStreamStats asserted on shards / layers / views / fused groups / direct-vs-converted uploads, with host_peak_bytes equal to one norm — the peak cannot scale with the model — and the views are kBF16, a dtype the GGUF loader can never produce.
  • The widening is exact: the same checkpoint written BF16 and F32 (bf16-rounded values) streams to kBF16 and kF32 views respectively, and the two full encoder forwards are bit-identical (memcmp == 0). The A/B cannot be confounded by the widening.

What is NOT claimed

  • This does not establish that the render changes. The number is a conditioning-tensor diff. Whether a 0.99745-cosine, diffusely-rotated conditioning produces a visibly different video is exactly the question this does not answer; the render A/B on the saved embeds is owed and is why --save-embeds exists.
  • No bf16 render exists on either branch of this stack. No bf16-vs-quantized DiT quality or speed number is claimed.
  • The conditioning numbers are from one prompt on Thor (sm_110). They are a first measurement, not a distribution.
  • The real 63 GB encoder load and its measured peak RSS on GB10 are not re-verified here.

Rebase note

origin/main grew a vision-conditioning path in the encoder block (--cond-image: merged features masked-scattered into inputs_embeds plus the 3 DeepStack taps) after this branch was written. This branch had refactored that whole block into a text-only EncodeH3Prompt helper, which would have silently dropped it. Resolved by keeping main's inline block for the run path and scoping EncodeH3Prompt to --encoder-only, which is text-only by design. Both capabilities survive.

@mudler
mudler force-pushed the row/H3-ENC-BF16-COND-DIFF branch from 2fb99b3 to 46df7c7 Compare August 7, 2026 07:42
@mudler
mudler force-pushed the row/H3-BF16-SHARDED-STREAM branch from 7c800b1 to ed8680c Compare August 7, 2026 08:03
@mudler
mudler force-pushed the row/H3-ENC-BF16-COND-DIFF branch from 46df7c7 to 71763e9 Compare August 7, 2026 08:03
@mudler
mudler force-pushed the row/H3-BF16-SHARDED-STREAM branch from ed8680c to 2a448ab Compare August 7, 2026 08:45
@mudler
mudler force-pushed the row/H3-ENC-BF16-COND-DIFF branch from 71763e9 to a47bf6a Compare August 7, 2026 08:45
@localai-bot

Copy link
Copy Markdown
Collaborator Author

Force-pushed twice since opening:

  1. Rebased onto the fixed stackfeat(minimax-h3): the ORIGINAL bf16 DiT release is indexable — 13 shards, 66.3 GB #98 gained a UBSan fix (misaligned 16-bit safetensors load) that this branch's test path also reaches.
  2. Repaired stray conflict markers that VK-C bench: coopmat GEMM is 11.1x-32.9x over our scalar kernel on Thor #101 merged onto main. .agents/benchmark-record.md on origin/main literally contains <<<<<<< HEAD / ======= / >>>>>>> origin/main around the Vulkan-coopmat and Kimi-Linear sections (lines ~14539–14596). Both sections are real evidence in an append-only record, so both are kept and only the scaffolding is removed. Nothing was dropped.

@mudler
mudler force-pushed the row/H3-BF16-SHARDED-STREAM branch 2 times, most recently from cda2f52 to 9d27a78 Compare August 7, 2026 09:53
@mudler
mudler force-pushed the row/H3-ENC-BF16-COND-DIFF branch from a47bf6a to 32823d7 Compare August 7, 2026 09:57
@mudler
mudler force-pushed the row/H3-BF16-SHARDED-STREAM branch from 9d27a78 to 48b4b04 Compare August 7, 2026 10:26
@mudler
mudler force-pushed the row/H3-ENC-BF16-COND-DIFF branch from 32823d7 to df0646a Compare August 7, 2026 10:29
@mudler
mudler force-pushed the row/H3-BF16-SHARDED-STREAM branch 2 times, most recently from ed21660 to 2a69bcd Compare August 7, 2026 11:02
@mudler
mudler force-pushed the row/H3-ENC-BF16-COND-DIFF branch from df0646a to f96884d Compare August 7, 2026 11:07
@mudler
mudler force-pushed the row/H3-BF16-SHARDED-STREAM branch from 2a69bcd to 8a5fb66 Compare August 7, 2026 12:12
mudler added 2 commits August 7, 2026 12:57
… run it alone

Branch: row/H3-ENC-BF16-COND-DIFF (helper). Carries §8.6's
MiniMaxH3ShardedCheckpoint CHERRY-PICKED from row/H3-BF16-SHARDED-DIT 1a46ff17
rather than mirrored: the shard-index resolver is exactly the piece this needs,
and a second copy of those 222 lines would drift.

Every H3 render so far conditioned on a Q4_K_M Qwen3-VL-32B encoder
(enc_q4km.gguf, 14.6 GB) and NOBODY had measured the encoder's contribution.
That matters because weak conditioning and quantization-damaged conditioning are
indistinguishable from outside a render — the wuxia prompt asked for
shot/reverse-shot coverage of a sect exchanging intelligence and produced a good
generic portrait — and this family is quantization-sensitive (ComfyUI PR 15298:
the partial split-half RoPE produces channel-wise magnitude outliers that corrupt
even INT8). The blocker was mechanical: --encoder took only a GGUF, while the
unquantized tower ships as 14 safetensors shards.

This is a LOADER, not a second forward. MiniMaxH3EncoderDeviceWeights binds a
plain name->vt::Tensor map; the GGUF arm fills it with ggml blocks, this fills it
with bf16, and MiniMaxH3EncoderTextForwardDevice runs unchanged over either. That
is what makes the two arms comparable at all.

- MiniMaxH3EncoderConfigFromShards derives the geometry from the index's SHAPES
  alone (no payload), with the SAME recovery rules as the GGUF loader AND the same
  defaults for what shapes cannot carry (rope_theta, mrope_section, rms_norm_eps,
  selected_layer) — otherwise an A/B compares two RoPEs, not two quantizations.
- StreamMiniMaxH3EncoderShardsToDevice (new minimax_h3_encoder_sharded.cpp) keeps
  the projections BF16 on the device and uploads them DIRECTLY out of the
  read-only mmap; the [q|k|v] and [gate|up] row fusions are done ON THE DEVICE, by
  uploading each member into its offset of one allocation, so even the one
  transform this loader performs costs no host copy. Only the norms are widened,
  and those are [5120].
- MiniMaxH3EncoderTextForwardDevice WIDENS a bf16 weight to f32 immediately before
  its GEMM, into a scratch reused across all 50 layers. vt::MatmulBT needs one
  dtype for both operands and these activations are f32, while staging the tower
  f32 is 97.5 GiB against 48.8 GiB bf16 on a 122 GiB UNIFIED pool. bf16 -> f32 is
  EXACT, so this is residency, not numerics — and it is gated as such.
- MiniMaxH3EncoderEmbedTokensFromShards gathers a prompt's rows straight out of the
  mmap'd [151936, 5120] table, the safetensors twin of the GGUF per-row dequantize.
- minimax-h3-gen accepts --encoder <dir> wherever it accepted the GGUF, and
  --encoder-only runs the tower alone (no DiT, no VAEs) and writes --save-embeds.
  The DiT was loaded FIRST in the normal path, so conditioning alone used to cost
  ~96 GiB peak instead of ~49 GiB on a pool that OOM-reboots this box. Both encoder
  paths now go through ONE helper.

Gates (CPU, test_minimax_h3 70/70 cases / 49706 assertions, up from 68/68):
(1) a synthetic 4-shard encoder at the REAL name spellings resolves, and every
fused view is memcmp-exact against q ++ k ++ v and gate ++ up for EVERY layer,
unfused projections byte-exact, the separate names GONE, model.language_model.
norm.weight + lm_head.weight + the vision tower NOT bound, truncation honoured,
the embedding gather exact and out-of-range throwing; (2) ★ the loader RAN and is
NOT the GGUF path — MiniMaxH3EncoderShardStreamStats asserted on shards, layers,
views, fused groups and BOTH upload paths, with host_peak_bytes equal to ONE norm
so peak cannot scale with the model, and the views are kBF16, a dtype the GGUF
loader can never produce; (3) ★ the widening is EXACT — the same checkpoint
written BF16 and F32 (bf16-rounded values) streams to kBF16 and kF32 views
respectively and the two full encoder forwards are BIT-IDENTICAL (memcmp == 0),
so the conditioning measurement cannot be confounded by the widening itself.

Also repairs a PRE-EXISTING check-public-doc-tables ratchet red on origin/main
(docs/STATUS.md 284114 chars > 284073) by collapsing the H3 row's superseded
narrative to its binding result.

Honest residuals: the real 63 GB load, its measured peak RSS, and the
Q4_K_M-vs-bf16 conditioning numbers are UNVERIFIED here — the GPU was busy with a
render. They are this row's next step. Pre-existing red, not this row's:
check-fusion-consistency / test_check_fusion_consistency
(minimax_h3_video_vae_device), verified RED on a clean origin/main tree.

Folds in the lifetime fix found while gating this row: the shard streamer's
checkpoint must OUTLIVE its async uploads. It was a defect in code that never
landed, so it belongs in the commit that introduces the streamer rather than
as a follow-up against a tree that never existed on main.

FOLLOWING_AGENTS_PROTOCOL
Assisted-by: Claude Opus 5 (1M context)
… as much as a one-word prompt edit, but DIFFUSELY

Branch: row/H3-ENC-BF16-COND-DIFF (helper). Records only. Measured on Thor
sm_110, CUDA 13.0.1 container, build d1085374 (built and measured as d3861693,
amended only for the row-branch trailer; IDENTICAL tree dd9283cf, so the
measurement binary IS this commit). GPU idle — the LocalAI render finished on
its own and was never touched.

CONTROLLED THE WAY IT HAS TO BE: same prompt (wuxia.txt, 233 tokens), same
tokenizer, same 50-layer truncation, same MiniMaxH3EncoderTextForwardDevice,
same f32 activations — only the weight bytes differ. Both arms self-report
IDENTICAL geometry (50 / 5120 / 64 / 8 / 128 / 25600), which is what proves they
are the same model rather than two checkpoints sharing a name.

A CALIBRATION ARM, because a cosine means nothing without a yardstick: the bf16
encoder also encoded a ONE-WORD edit of the same prompt ("at night" -> "at
dawn", also 233 tokens). That is the scale the quantization number is read on.

                         relRMS  relRMS(excl sink)  cos mean  cos med   ang med
  Q4_K_M vs bf16         0.03403      0.06849        0.99745  0.99810   3.535 deg
  bf16, one-word edit    0.01897      0.06666        0.99769  0.99963   1.565 deg

  Q4_K_M vs bf16: max|diff| 154.0, RMS 0.5045, cosine min 0.90916 (token 69),
  max rotation 24.61 deg, 232 of 233 tokens below cosine 0.999.

THE READ.
1. NOT a scale change. Q4 conditioning is uniformly ~1% smaller (norm ratio
   0.99010) but the best single global rescale removes almost none of the
   difference (0.03403 -> 0.03280). It is DIRECTIONAL — the kind that matters.
2. Its total energy is ON PAR with a one-word prompt edit (6.85% vs 6.67%
   excluding the attention-sink token). Quantizing the encoder moves the
   conditioning about as much as rewriting a word of the prompt.
3. The SHAPE is opposite, and that is the interesting part. The edit is SPARSE:
   172 of 233 tokens stay above cosine 0.999 (median rotation 0.16 deg), the
   change concentrating on ~6 tokens, the largest at 32 deg. Quantization is
   DIFFUSE: 232 of 233 tokens fall below 0.999, EVERY token rotates a few
   degrees, one by 24.6 deg. A smear applied everywhere, not a different prompt.
4. max|diff| 154 is the ATTENTION SINK, not corruption. Token 0 has norm 15,522
   against a 366 mean (42x) and carries 68% of the total squared error while its
   DIRECTION is intact (cosine 0.99962) — ComfyUI PR 15298's channel-wise
   magnitude outliers showing up concretely, and the reason the sink-excluded
   column is the honest aggregate.

VERDICT: Q4_K_M does real, measurable, directional damage to the conditioning,
comparable in magnitude to editing the prompt, but diffusely. A uniform
few-degree rotation of every token is the signature that blunts fine-grained
compositional instruction (coverage, blocking, staging) toward a prompt's
average semantics — which is exactly the "competent but generic" symptom. The
bf16 encoder is worth a render A/B.

EXPLICITLY NOT ESTABLISHED: that the RENDER changes. Nothing here measures the
DiT's sensitivity to a 3.5-degree median rotation. The owed follow-up is a
byte-identical-everything-else render A/B — same DiT, seed and steps, with
--prompt-embeds cond_q4km.bin vs cond_bf16.bin (both saved on the box) — which
is exactly what the save/replay seam makes controllable.

REPRODUCED: both arms re-run from scratch produced BYTE-IDENTICAL files
(cond_q4km.bin md5 a331232096ef1da2628f885950b2fc55, cond_bf16.bin md5
9c096b63b9bd07f604daebb2fc090f46 on both runs), so these are deterministic
numbers, not samples.

Cost: Q4_K_M arm 40 s / 18.0 GiB peak; bf16 arm 40 s (35 s streaming) / 45.41
GiB uploaded / host conversion peak 0.0195 MiB (ONE norm — the projections never
touch a host buffer) / 51.95 GiB total peak of the 122 GiB unified pool. The
real-checkpoint streamer counters (layers=50 tensors=400 direct=350
converted=200 fused=100) confirm the shard path RAN and that every projection
took the no-host-copy upload. benchmark_binding=false; no throughput claimed.

FOLLOWING_AGENTS_PROTOCOL
Assisted-by: Claude Opus 5 (1M context)
@mudler
mudler force-pushed the row/H3-ENC-BF16-COND-DIFF branch from f96884d to 8d392a8 Compare August 7, 2026 12:59
@localai-bot
localai-bot changed the base branch from row/H3-BF16-SHARDED-STREAM to main August 7, 2026 12:59
@localai-bot
localai-bot merged commit 8eeac9e into main Aug 7, 2026
@mudler
mudler deleted the row/H3-ENC-BF16-COND-DIFF branch August 7, 2026 13:57
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