Skip to content

perf(cuda/gemma4): decode is bound by 8-bit MLP weight bytes plus a near-saturated dual CPU/GPU pipeline; qmm kernels are healthy #683

Description

@inureyes

Summary

Follow-up investigation to #680 (closed by PR #682 with corrected attribution). A per-shape quantized-matmul GEMV microbench on GB10 (new tool: examples/qmm_gemv_microbench.rs, results CSV benchmarks/cuda_gb10_qmm_gemv_micro_2026-07-07.csv, both in an accompanying PR) shows the qmm GEMV kernels are HEALTHY, sustaining 201 to 214 GB/s (74 to 78% of the 273 GB/s peak) on every gemma/llama layer shape at both 4-bit and 8-bit, with no shape cliff (smaller attention shapes reach 170 to 190 GB/s from fixed launch latency, same on llama). The earlier "gemma reaches only ~52% effective bandwidth" figure was decode wall time diluted by non-qmm work, not kernel inefficiency. Kernel-side optimization of qmm GEMV is NOT a lever; do not pursue it.

Measured decode decomposition (GB10, greedy, 2048-token context)

Using measured per-shape qmm times (microbench) and CUDA GRAPH_TRACE gap analysis of the #680 nsys captures:

model wall/step qmm streaming small-kernel GPU GPU gaps GPU busy
gemma-4-12b-it-4bit (mixed: MLP 8-bit) ~71 ms (14.3 tok/s) ~52 ms ~14 ms ~4.6 ms 93.6%
llama-3.1-8b-4bit ~21.2 ms (47 tok/s) ~20.7 ms <1 ms ~0.2 ms 99.2%
gemma-4-31b-it-4bit (uniform 4-bit) 114.6 ms (8.7 tok/s) ~82 ms ~33 ms combined non-qmm

The model closes on all three (predicted vs measured within a few percent). gemma-4-12b's per-step GPU gaps are ~1.6 large gaps (>0.5 ms) totaling ~3.7 ms at step boundaries (sampling plus next-step graph construction before the first launch), which llama does not have at all; the CPU lazy-graph pipeline (~2,400 primitives/step, see #680) therefore runs within ~10% of the GPU and becomes the binding constraint as soon as GPU time is reduced.

Why gemma-4-12b is slow: the checkpoint streams 2.3x llama's bytes

The "4bit" gemma-4-12b-it checkpoint is mixed precision: 144 modules (48 layers x gate/up/down) are 8-bit, which is ~9 GB of the ~10.3 GB streamed per token (MLP dominates parameter count). llama-3.1-8b-4bit streams ~4.4 GB. At healthy kernel bandwidth the byte ratio alone explains most of the 14.3 vs 47 tok/s gap; the rest is the ~19 ms/step non-qmm overhead (small-kernel GPU time + gaps) tied to gemma-4's ~50 primitives/layer (#680).

Levers, sized with measured numbers (gemma-4-12b @2048)

  • Requantize the MLP to uniform 4-bit (checkpoint-level, quality tradeoff to be validated; QAT variants exist for other sizes): qmm 52 -> ~33 ms. Alone: ~52 ms/step (~19 tok/s, 1.35x), at which point the CPU pipeline becomes binding.
  • Cut small-kernel GPU time and CPU primitive count together via block-scale fusion (compiled residual+norm+scale chains, attention glue; extends PR perf(gemma4): cut decode-path primitive count on CUDA (#680) #682's decode-gated compiled MLP): the two must land together with requantization to realize it. Combined realistic projection ~42-48 ms/step (~21-24 tok/s, 1.45-1.66x).
  • 1.8x (25.8 tok/s) is a stretch requiring all levers: uniform 4-bit MLP, roughly halved small-kernel GPU time, and CPU graph-build under ~38 ms/step.
  • On the as-shipped mixed checkpoint, code-only work is capped at ~1.16x (GPU busy is 66 ms of which 52 ms is irreducible weight streaming).

Suggested plan

  1. Land the microbench tool + CSV (accompanying PR).
  2. Validate a uniform 4-bit (or QAT 4-bit) gemma-4-12b checkpoint end to end: decode tok/s and output quality vs the mixed checkpoint (note: the local gemma-4-12b-it-assistant-4bit is uniform 4-bit but model_type gemma4_unified_assistant is unsupported by mlxcel, so a requantized standard checkpoint is needed).
  3. Only after (2) confirms the ~19 tok/s intermediate, invest in block-scale fusion to chase 21-24 tok/s.

Related

Activity

  1. inureyes commented on Jul 7, 2026

    @inureyes
    MemberAuthor

    Step 2 result: uniform 4-bit requantization measured at 1.59-1.62x decode, confirming the byte-count attribution

    Built a uniform 4-bit gemma-4-12b checkpoint by dequantizing the 144 8-bit MLP modules (group 64, affine, u32 low-first layout) and requantizing them to 4-bit min-max affine with scales/biases rounded to bf16 before computing q (self-consistent triples; max per-group reconstruction error 0.053). Checkpoint size 10.99 -> 6.74 GB. The requantization script is a standalone numpy tool (raw safetensors parse/write, no GPU); it can be PR'd into scripts/ if wanted.

    Measured on GB10 (mlxcel-bench-decode, greedy 64 tokens, chunked prefill defaults), uniform vs the shipped mixed checkpoint:

    context mixed decode tok/s uniform decode tok/s speedup peak GB (mixed -> uniform)
    512 14.63 23.68 1.62x 11.60 -> 7.45
    2048 14.39 23.11 1.61x 13.00 -> 8.98
    8192 14.28 22.86 1.60x 14.51 -> 10.92

    This lands ABOVE the 1.35x single-lever projection: at ~43 ms/step the non-qmm overhead visible in the step budget shrank from ~19 ms to ~10 ms, i.e. part of the overhead previously serialized against the longer 8-bit qmm stream now overlaps. Prefill throughput is unchanged within noise (950-1045 tok/s). The remaining gap to the original 1.8x bar (25.8 tok/s = 38.8 ms/step) is ~4.5 ms/step, which is the block-scale fusion territory of step 3.

    Quality: greedy smoke prompts behave identically between mixed and uniform (both exhibit this checkpoint's pre-existing temp-0 pad-collapse noted in #682's validation; leading tokens are equally sensible, no relative regression). A real quality gate (perplexity or task eval) should be run before adopting uniform 4-bit as the default serving quantization for gemma-4-12b; the 8-bit MLP override in the shipped checkpoint presumably exists for quality headroom.

    Conclusion: the checkpoint byte count is confirmed as the dominant lever exactly as sized by the microbench (PR #684). Step 3 (fusion for the last ~4.5 ms) remains optional on top.

  2. inureyes commented on Jul 7, 2026

    @inureyes
    MemberAuthor

    Quality gate outcome: BLOCKED by #686 (gemma-4 forward is broken); speed numbers stand

    Steps 2-3 status update. Tools landed: PR #684 (qmm microbench) and PR #685 (scripts/requantize_mlp.py generalized with --match and --target-group-size, plus examples/perplexity.rs).

    Built the planned quality/throughput frontier variants: B uniform 4-bit gs64 (6.74 GB), C down_proj-kept-8-bit (8.16 GB), D MLP 4-bit group_size 32 (7.27 GB). Two loader limitations surfaced: mixed bits WITHIN one MLP (variant C) fails to load because the gemma-4 MLP loader infers bits at the global group_size for the whole projection set, and per-module group_size 32 overrides (variant D) are misread as bits=2. Exact errors are recorded in #686; supporting these is a separate loader improvement if the frontier is to include C/D.

    The perplexity gate then invalidated itself in the most useful way possible: gemma-4-12b scores ppl ~12,000-29,000 on a corpus where gemma-3-4b scores 15.3 and llama-8b 24.8, flat-bad from 32-token contexts, meaning the gemma-4 dense forward is catastrophically degraded in mlxcel (filed as #686, priority critical, likely the same root as the temp-0 pad-collapse). Every quality conclusion about quantization variants is therefore VOID until #686 is fixed: comparing 4-bit vs 8-bit MLP quality on a broken forward measures noise.

    What remains valid: all throughput measurements (uniform 4-bit B = 23.1 tok/s @2048 = 1.61x, peak -3.6 GB) and the step-1 microbench attribution. The answer to "can we keep the 8-bit-MLP quality condition AND regain speed" is deferred: once #686 is fixed, re-run the frontier with the landed tools (B is built; C/D need the loader improvement or can be approximated by requantizing at the whole-MLP granularity).

    Recommended order: fix #686 first (it may also change the perf baseline), then re-run this quality gate, then decide between uniform 4-bit, selective 8-bit, or gs32 on real quality data, and only then invest in step-3 fusion.

  3. self-assigned this
    on Aug 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions