Skip to content

fix(perf): use MLX fast RMSNorm in GatedDeltaNet decode path - #30

Merged
inureyes merged 1 commit into
mainfrom
perf/gated-delta-fast-rmsnorm
May 18, 2026
Merged

inureyes merged 1 commit into
mainfrom
perf/gated-delta-fast-rmsnorm

Conversation

@inureyes

Copy link
Copy Markdown
Member

Summary

GatedDeltaNet linear-attention layers (Qwen3.5, Qwen3-Next, KimiLinear) were computing Q/K and gated-output RMSNorm by expanding the formula into primitive MLX ops on every decode step (square → mean_axis → add(eps) → sqrt → divide → multiply(scale)). Six graph nodes per call; per-step overhead dominates on Apple Silicon. Reference mlx-lm uses mx.fast.rms_norm for these paths.

This PR replaces those expanded graphs with the fast kernel.

Changes

  • src/models/gated_delta.rs
    • Add scaled_fast_rms_norm_no_weight(x, scale, eps) — wraps mlxcel_core::fast_rms_norm_no_weight and post-multiplies by a scalar (mirrors mx.fast.rms_norm(x, None, eps) * scale).
    • Collapse RMSNormGated::forward to a single fast_rms_norm call before the SwiGLU gate.
  • src/models/qwen3_5.rs, src/models/qwen3_next.rs, src/models/kimi_linear.rs
    • Replace the manual Q/K RMSNorm-and-scale blocks at every call site (prefill, decode, and for Qwen3.5 the speculative-decoding verify pass) with scaled_fast_rms_norm_no_weight.
    • Q uses inv_scale^2, K uses inv_scale, where inv_scale = head_k_dim^-0.5 — same numerics as before, one MLX primitive instead of six.

Performance (M5 Max, 4-bit, decode tok/s)

Model Before After Speedup mlx-lm parity
qwen3.5-0.8b-4bit 425.16 535.43 +25.9% 68% → 96%

Measured 5-run mean on def fibonacci(n):\n (100 tokens, --profile --no-chat-template). Before runs: 427.17, 428.86, 423.32, 423.65, 422.78. After runs: 535.44, 535.26, 535.06, 536.12, 535.25.

Other GatedDeltaNet families (Qwen3-Next, KimiLinear) share the helper and benefit from the same kernel switch; their absolute numbers are tracked separately.

Test plan

  • cargo fmt --all -- --check
  • cargo check --features metal,accelerate
  • Manual (M5 Max): qwen3.5-0.8b-4bit 100-tok decode reproduces ~535 tok/s on def fibonacci(n):\n .
  • Manual: Qwen3.5 speculative-decoding draft/verify path still produces identical tokens to greedy decode (verify-pass RMSNorm dtype agreement preserved).
  • Manual: smoke-run Qwen3-Next and KimiLinear families — outputs unchanged, decode tok/s steady or up.

GatedDeltaNet linear-attention layers (Qwen3.5, Qwen3-Next, KimiLinear) were normalizing Q/K and the gated output by expanding the RMSNorm formula into primitive ops on every decode step: `square` → `mean_axis` → `add(eps)` → `sqrt` → `divide` → `multiply(scale)`. Each of these is a separate MLX graph node, and at decode time the per-call overhead — not the FLOP count — dominates on Apple Silicon. Reference mlx-lm uses `mx.fast.rms_norm` for the same paths.

This change replaces the expanded graphs with the fast kernel:

- `src/models/gated_delta.rs` — add `scaled_fast_rms_norm_no_weight(x, scale, eps)` helper that wraps `mlxcel_core::fast_rms_norm_no_weight` and post-multiplies by a scalar (mirrors `mx.fast.rms_norm(x, None, eps) * scale`). Collapse `RMSNormGated::forward` to a single `fast_rms_norm` call before the SwiGLU gate.
- `src/models/qwen3_5.rs`, `src/models/qwen3_next.rs`, `src/models/kimi_linear.rs` — replace the manual Q/K RMSNorm-and-scale blocks (three call sites per model: prefill, decode, and the speculative-decoding verify pass on Qwen3.5) with `scaled_fast_rms_norm_no_weight`. Q uses `inv_scale^2`, K uses `inv_scale`, where `inv_scale = head_k_dim^-0.5`, preserving the existing dtype-and-scale semantics.

Measured on M5 Max with `qwen3.5-0.8b-4bit`, 100-token decode, `--profile --no-chat-template`, 5-run mean over the prompt `def fibonacci(n):\n    `:

- Before: 425.16 tok/s
- After:  535.43 tok/s  (+25.9%, 96% of mlx-lm's 555.43 tok/s on the same prompt)

The other GatedDeltaNet families share the helper and benefit from the same kernel switch; their absolute numbers are tracked separately. Numerics are unchanged at the kernel level (same RMS formula, just compiled into one MLX primitive instead of six), and the speculative-decoding verify-pass path is updated alongside prefill/decode so draft/verify dtype agreement is preserved.
@inureyes inureyes added status:review Under review type:bug Bug fixes, error corrections, or issue resolutions type:performance Performance improvements priority:high High priority area:models Model architectures, weights, loading, metadata labels May 18, 2026
@inureyes
inureyes merged commit 872110c into main May 18, 2026
4 checks passed
@inureyes
inureyes deleted the perf/gated-delta-fast-rmsnorm branch May 18, 2026 18:17
inureyes added a commit that referenced this pull request May 18, 2026
This release focuses on Apple Silicon decode performance for VLM and linear-attention model families, plus a CLI fix for `mlxcel list`.

Performance:

- Gemma3n bf16 prefill path: gemma3n-e4b-bf16 decode 11.16 to 38.81 tok/s on M5 Max (#28).
- Qwen3-VL text-only decode: QKV fusion + `forward_text_only` fast path. Qwen3-VL-30B-A3B-Instruct-4bit decode 56.00 to 146.29 tok/s (2.61x, 99% of mlx-lm parity); Qwen3-VL-32B-Instruct-4bit decode 18.79 to 27.33 tok/s (1.45x, 94% parity); image-in-prompt and DeepStack paths unchanged (#29).
- GatedDeltaNet fast RMSNorm in Qwen3.5 / Qwen3-Next / KimiLinear: `mlx::fast::rms_norm` replaces the expanded square / mean / sqrt / divide graph for Q/K and gated output. qwen3.5-0.8b-4bit decode 425.16 to 535.43 tok/s on M5 Max (+25.9%, 96% parity); Qwen3.5 speculative-decoding verify pass updated alongside prefill / decode to preserve draft / verify dtype agreement (#30).

Fixed:

- Rewrite `mlxcel list` output to drive from the `ModelType` registry: fixes stale count, missing VLM family, missing ~30 model types, and removes a broken docs link (#27).

Docs:

- Refresh README performance snapshot and `docs/benchmarks.md` to match the latest M5 Max sweep.
@inureyes inureyes added this to the 0.1 milestone Jun 21, 2026
@inureyes inureyes self-assigned this Aug 31, 2026
@inureyes inureyes added status:done Completed and removed status:review Under review type:performance Performance improvements labels Sep 6, 2026
be-free-snowboy pushed a commit to freestylejs/snowbrain-mlxcel that referenced this pull request Oct 27, 2026
GatedDeltaNet linear-attention layers (Qwen3.5, Qwen3-Next, KimiLinear) were normalizing Q/K and the gated output by expanding the RMSNorm formula into primitive ops on every decode step: `square` → `mean_axis` → `add(eps)` → `sqrt` → `divide` → `multiply(scale)`. Each of these is a separate MLX graph node, and at decode time the per-call overhead — not the FLOP count — dominates on Apple Silicon. Reference mlx-lm uses `mx.fast.rms_norm` for the same paths.

This change replaces the expanded graphs with the fast kernel:

- `src/models/gated_delta.rs` — add `scaled_fast_rms_norm_no_weight(x, scale, eps)` helper that wraps `mlxcel_core::fast_rms_norm_no_weight` and post-multiplies by a scalar (mirrors `mx.fast.rms_norm(x, None, eps) * scale`). Collapse `RMSNormGated::forward` to a single `fast_rms_norm` call before the SwiGLU gate.
- `src/models/qwen3_5.rs`, `src/models/qwen3_next.rs`, `src/models/kimi_linear.rs` — replace the manual Q/K RMSNorm-and-scale blocks (three call sites per model: prefill, decode, and the speculative-decoding verify pass on Qwen3.5) with `scaled_fast_rms_norm_no_weight`. Q uses `inv_scale^2`, K uses `inv_scale`, where `inv_scale = head_k_dim^-0.5`, preserving the existing dtype-and-scale semantics.

Measured on M5 Max with `qwen3.5-0.8b-4bit`, 100-token decode, `--profile --no-chat-template`, 5-run mean over the prompt `def fibonacci(n):\n    `:

- Before: 425.16 tok/s
- After:  535.43 tok/s  (+25.9%, 96% of mlx-lm's 555.43 tok/s on the same prompt)

The other GatedDeltaNet families share the helper and benefit from the same kernel switch; their absolute numbers are tracked separately. Numerics are unchanged at the kernel level (same RMS formula, just compiled into one MLX primitive instead of six), and the speculative-decoding verify-pass path is updated alongside prefill/decode so draft/verify dtype agreement is preserved.
be-free-snowboy pushed a commit to freestylejs/snowbrain-mlxcel that referenced this pull request Oct 27, 2026
This release focuses on Apple Silicon decode performance for VLM and linear-attention model families, plus a CLI fix for `mlxcel list`.

Performance:

- Gemma3n bf16 prefill path: gemma3n-e4b-bf16 decode 11.16 to 38.81 tok/s on M5 Max (lablup#28).
- Qwen3-VL text-only decode: QKV fusion + `forward_text_only` fast path. Qwen3-VL-30B-A3B-Instruct-4bit decode 56.00 to 146.29 tok/s (2.61x, 99% of mlx-lm parity); Qwen3-VL-32B-Instruct-4bit decode 18.79 to 27.33 tok/s (1.45x, 94% parity); image-in-prompt and DeepStack paths unchanged (lablup#29).
- GatedDeltaNet fast RMSNorm in Qwen3.5 / Qwen3-Next / KimiLinear: `mlx::fast::rms_norm` replaces the expanded square / mean / sqrt / divide graph for Q/K and gated output. qwen3.5-0.8b-4bit decode 425.16 to 535.43 tok/s on M5 Max (+25.9%, 96% parity); Qwen3.5 speculative-decoding verify pass updated alongside prefill / decode to preserve draft / verify dtype agreement (lablup#30).

Fixed:

- Rewrite `mlxcel list` output to drive from the `ModelType` registry: fixes stale count, missing VLM family, missing ~30 model types, and removes a broken docs link (lablup#27).

Docs:

- Refresh README performance snapshot and `docs/benchmarks.md` to match the latest M5 Max sweep.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:models Model architectures, weights, loading, metadata priority:high High priority status:done Completed type:bug Bug fixes, error corrections, or issue resolutions

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant