Repository navigation
perf(models): generalize fused QKV+RMSNorm+RoPE to standard-RMSNorm QK-norm models #326
Copy link
Copy link
Closed
Labels
area:coremlxcel-core: MLX FFI, primitives, KV cache, layersmlxcel-core: MLX FFI, primitives, KV cache, layersarea:modelsModel architectures, weights, loading, metadataModel architectures, weights, loading, metadatapriority:highHigh priorityHigh prioritystatus:doneCompletedCompletedtype:performancePerformance improvementsPerformance improvements
Milestone
Description
Summary
Generalize the existing fused QKV-projection + split + Q/K-norm + RoPE kernel so models using a standard RMSNorm (not only GemmaRMSNorm) can use it, then wire it into Qwen3 and Qwen3-MoE first, with a staged rollout to the other QK-norm families.
Source: idea.md "2. Generic Fused QKV + RMSNorm + RoPE For QK-Norm Models" (P0, expected payoff medium, risk medium-high).
Current state
FusedQKVLinear::forward_split_norm_rope_quantized()exists but is specialized toGemmaRMSNormand is documented as "Used by: Gemma3 dense attention path" (src/lib/mlxcel-core/src/layers.rs:1282).- Qwen3 attention uses a fused QKV projection, then separate reshape,
q_norm,k_norm, transpose, and RoPE ops (src/models/qwen3.rs, attention forward around line 115). - Many models still apply Q/K
RMSNormas separate ops: Qwen3-MoE, Qwen3-VL, Qwen3-VL-MoE, OLMoE, OLMo2/3, Apertus, Exaone4, Exaone-MoE, and MiniMax. - The decode-gap investigation reports dense Llama/Granite near parity, while the gap grows with extra small ops such as QK-norm and xIELU (
docs/benchmark_results/moe-decode-gap-investigation.md).
Proposal
- Generalize the fused projection/split/norm/rope primitive so standard
RMSNormweights can use it, not onlyGemmaRMSNorm(for example, accept a norm trait/enum or a generic norm parameter). - Wire it first into Qwen3 and Qwen3-MoE, which already have a fused QKV layout and good benchmark coverage.
- Then port compatible QK-norm families in small batches: Qwen3-VL text path, OLMoE/OLMo2/OLMo3, Apertus, Exaone4/Exaone-MoE, MiniMax. These follow-up family rollouts can be split into sub-issues once the primitive is proven.
Scope
- Generalize
forward_split_norm_rope_quantizedand update its// Used by:comment per the shared-function convention indocs/code-guidelines.md. - Wire Qwen3 and Qwen3-MoE in this issue.
Validation
- Correctness: RMS < 5e-3 against the existing graph path, plus greedy parity for fixed prompts where deterministic parity is expected.
- Performance:
mlxcel-bench-decodeonqwen3-0.6b-4bit,qwen3-8b-4bit,qwen3-30b-a3b-4bit, andApertus-8B-2509-4bit. - Regression guard: include a non-QK-norm dense model (for example Llama) to prove common attention behavior did not change.
- Suggested first experiment (idea.md): prototype generic RMSNorm support on Qwen3 only first, to isolate the QK-norm hypothesis with minimal model spread.
Acceptance criteria
- The fused primitive accepts standard
RMSNormQ/K norms, not justGemmaRMSNorm. - Qwen3 and Qwen3-MoE attention dispatch the generalized fused path on decode; the non-fused path remains as fallback.
- RMS < 5e-3 vs the graph reference; greedy temp-0 parity (or documented jitter) on the listed checkpoints.
-
mlxcel-bench-decodenumbers recorded; the non-QK-norm dense regression guard shows no change. -
// Used by:comment updated; INTEGRATION into the real Qwen3/Qwen3-MoE forward path verified.
Risk and priority
idea.md priority P0; risk medium-high. Directly targets the "extra small ops per layer" decode gap and yields one shared primitive reusable by many recently added QK-norm families.
References
- idea.md "2. Generic Fused QKV + RMSNorm + RoPE For QK-Norm Models".
- Code:
src/lib/mlxcel-core/src/layers.rs(forward_split_norm_rope_quantized),src/models/qwen3.rs. docs/benchmark_results/moe-decode-gap-investigation.md,docs/code-guidelines.md(shared-function rule).
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
area:coremlxcel-core: MLX FFI, primitives, KV cache, layersmlxcel-core: MLX FFI, primitives, KV cache, layersarea:modelsModel architectures, weights, loading, metadataModel architectures, weights, loading, metadatapriority:highHigh priorityHigh prioritystatus:doneCompletedCompletedtype:performancePerformance improvementsPerformance improvements
Activity
feat(layers): add MLXCEL_FUSED_QK_NORM kill-switch and eps assertion
perf(models): generalize fused QKV+RMSNorm+RoPE to standard RMSNorm (#…
test(server): assert #332 cohort parity against all-cold-batched refe…
perf(server): split batched prefill into compatible cohorts (#346)
inureyes commentedon Jun 18, 2026
Deferred validation completed on GB10 / CUDA (PR #357)
The two open acceptance items were the validation/benchmark pass. Done now on the one backend the fused QK-norm primitive was never measured on (the doc comment had it gated off "pending a per-backend win (e.g. CUDA)").
Decode throughput (GB10 SM 12.1, same binary,
MLXCEL_FUSED_QK_NORM=1vs=0, best-of-3,--no-chat-template+ 256 tok):CUDA is not a per-backend win: the fused path is slower on every wired model, worst on the 48-layer MoE. The primitive builds the same MLX ops as the graph path in one C++ call, so the GPU work is identical; it only cuts FFI crossings and norms after the head transpose. Decode is GPU/bandwidth-bound, so that buys nothing. Same direction as M1 Ultra (1-3.4% slower) and consistent with M5 Max preferring the split decode path. The flag stays default-OFF; the graph/split path is the decode default on every measured backend.
Regression guard: apertus (QK-norm but not wired) and llama (no QK-norm) show the flag inert, so the common attention path is untouched.
Correctness:
fused_qkv_split_norm_rope_standard_rmsnorm_matches_graphunit test.Full method, table, and determinism analysis:
docs/benchmark_results/fused-qk-norm-decode-gb10.md(PR #357). No code behavior change.docs(benchmarks): record GB10/CUDA fused QK-norm decode result (#326) (…
docs(install): warn that a default Linux build is CPU-only (#326)
feat(runtime): warn when a CPU-only build runs on an NVIDIA host (#326)…
docs: finalize MLXCEL_FUSED_QK_NORM per-backend decision (#355) (#444)
fix(core): land the #905 fusions unwired after measurement, and repai…
fix(core): land the #905 fusions unwired after measurement, and repai…
fix(speculative): name the rules correctly and make the optimal rule …
feat(speculative): opt-in acceptance-optimal rejection sampling, and …
fix(server): add a kill switch and record the boundary split's output…
fix(server): add a kill switch and record the boundary split's output…
feat(server): donate a history-boundary snapshot during prefill (#1150)