Repository navigation
perf(core): hardware-gated MLX_MAX_OPS_PER_BUFFER decode default - #360
Merged
Merged
Conversation
On pre-M5 Apple Silicon (M1 through M4, Metal GPU family 3) the default command-buffer op cap is small enough that high-op-density decode stalls the GPU at buffer boundaries. Raising MLX_MAX_OPS_PER_BUFFER to 1000 closes that dispatch-gap idle and recovers Gemma3n decode throughput (#329/#345). apply_metal_ops_per_buffer_default (hardware.rs) sets the variable to 1000 at process start on M1-M4 when it is unset, wired into the CLI, server, and bench-decode main() before any MLX op. M5+ (Neural Accelerator) and non-Apple (CUDA) are left on MLX's default: the same sweep is flat on M5 Max (#358) and an earlier M5 study found larger buffers slower there. An operator-set MLX_MAX_OPS_PER_BUFFER always wins, and the knob is documented in the CLI help. M1 Ultra re-bench (best-of-3): gemma3n-e2b 82.7 to 92.5 (+11.9%), gemma3n-e4b 63.0 to 68.4 (+8.6%) versus the recorded default. Dense qwen3-8b and MoE qwen3-30b-a3b also run faster at 1000 than at a starved cap, so the generation-wide gate does not regress them; the MoE decode-gap sweep (#268) was flat. GB10 is not benched in this environment and is left on MLX's default. Closes #353.
inureyes
added a commit
that referenced
this pull request
Jun 20, 2026
Audio I/O on the OpenAI /v1/audio/* surface (Whisper speech-to-text #371, Kokoro-82M text-to-speech #374, endpoint plumbing #368), reasoning_content on non-streaming chat (#359), pre-M5 decode +8-12% via a hardware-gated op cap (#360), Turbo4Asym decode rerouted through dequant-SDPA (#369), batched sampling fixes (#339, #340, #344, #356), and the Turbo KV advisor/docs aligned to the measured sweep with ADR 0002 (#343, #376). README: consolidate the v0.3.0 and v0.3.1 highlights under a single v0.3 entry and add v0.3.2.
be-free-snowboy
pushed a commit
to freestylejs/snowbrain-mlxcel
that referenced
this pull request
Oct 27, 2026
…lup#360) On pre-M5 Apple Silicon (M1 through M4, Metal GPU family 3) the default command-buffer op cap is small enough that high-op-density decode stalls the GPU at buffer boundaries. Raising MLX_MAX_OPS_PER_BUFFER to 1000 closes that dispatch-gap idle and recovers Gemma3n decode throughput (lablup#329/lablup#345). apply_metal_ops_per_buffer_default (hardware.rs) sets the variable to 1000 at process start on M1-M4 when it is unset, wired into the CLI, server, and bench-decode main() before any MLX op. M5+ (Neural Accelerator) and non-Apple (CUDA) are left on MLX's default: the same sweep is flat on M5 Max (lablup#358) and an earlier M5 study found larger buffers slower there. An operator-set MLX_MAX_OPS_PER_BUFFER always wins, and the knob is documented in the CLI help. M1 Ultra re-bench (best-of-3): gemma3n-e2b 82.7 to 92.5 (+11.9%), gemma3n-e4b 63.0 to 68.4 (+8.6%) versus the recorded default. Dense qwen3-8b and MoE qwen3-30b-a3b also run faster at 1000 than at a starved cap, so the generation-wide gate does not regress them; the MoE decode-gap sweep (lablup#268) was flat. GB10 is not benched in this environment and is left on MLX's default. Closes lablup#353.
be-free-snowboy
pushed a commit
to freestylejs/snowbrain-mlxcel
that referenced
this pull request
Oct 27, 2026
Audio I/O on the OpenAI /v1/audio/* surface (Whisper speech-to-text lablup#371, Kokoro-82M text-to-speech lablup#374, endpoint plumbing lablup#368), reasoning_content on non-streaming chat (lablup#359), pre-M5 decode +8-12% via a hardware-gated op cap (lablup#360), Turbo4Asym decode rerouted through dequant-SDPA (lablup#369), batched sampling fixes (lablup#339, lablup#340, lablup#344, lablup#356), and the Turbo KV advisor/docs aligned to the measured sweep with ADR 0002 (lablup#343, lablup#376). README: consolidate the v0.3.0 and v0.3.1 highlights under a single v0.3 entry and add v0.3.2.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Follow-up to #329/#345 (the Gemma3n decode profile). That profile found raising
MLX_MAX_OPS_PER_BUFFERfrom its default to ~1000 recovers +11 to 13% decode throughput on M1 Ultra (gemma3n-e2b 82.7 to 93.2, e4b 63.0 to 70.0) by closing command-buffer dispatch-gap idle on the high-op-density decode stack, with a plateau near 1000 and a slight regression at 2000. It deferred the change because the lever is hardware-specific: M5 must stay on its default (the earlier M5 study found larger buffers slower, and #358 later measured the M5 Max sweep as flat), so a single global default is unsafe.Change
apply_metal_ops_per_buffer_default(src/lib/mlxcel-core/src/hardware.rs) setsMLX_MAX_OPS_PER_BUFFER=1000at process start on pre-M5 Apple Silicon (AppleSiliconGenM1 through M4, no Neural Accelerator) when the variable is unset. M5+ (Neural Accelerator) and non-Apple (CUDA, e.g. GB10) are left on MLX's default. An operator-setMLX_MAX_OPS_PER_BUFFER(any value) always wins. The pure gate decision ismetal_ops_per_buffer_default(gen, has_neural_accelerator)and is unit-tested. The applier is called at the top ofmain()in the CLI (src/main.rs), the server (src/bin/mlx_server.rs), and the decode bench (src/bin/bench_decode.rs), before any MLX op runs. The knob is documented in the CLI--helpenvironment-variable list. mlxcel-core is edition 2021, soset_varis sound there given the documented early-call contract.Validation (M1 Ultra, 2026-06-19, best-of-3, raw prompt, 80 tokens after a 20-token warmup, caffeinate)
The gate fires on this M1 Ultra (detected as
M1) and reproduces the section-4 1000-row throughput from the profile:An explicit low override is respected and collapses throughput to 67.7 (e2b) and 51.8 (e4b) at
MLX_MAX_OPS_PER_BUFFER=10, confirming both the lever direction and that operator settings win.Generalization (no regression on other families): dense qwen3-8b-4bit (86.1) and MoE qwen3-30b-a3b-4bit (89.0) also run faster at 1000 than at a starved cap (75.9 and 79.3 at ops=10), so the generation-wide pre-M5 gate does not regress them. The MoE decode-gap sweep (#268) was flat, consistent with the dispatch-gap binding being strongest where per-layer op density is highest.
Acceptance criteria
gemma3n-decode-profile-m5max.md) are recorded in the profile docs. GB10 is not benched in this environment; the gate leaves GB10 (non-Apple, CUDA) on MLX's default, so it is unaffected until a follow-up bench.MLX_MAX_OPS_PER_BUFFERenv var preserved as a manual override (respected when set, documented in CLI help).Validated under the pinned toolchain (rust 1.93.1, matching CI):
cargo clippy --all-targets --features metal,accelerate -- -D warningsclean for the root andmlxcel-core, 25 hardware unit tests pass, fmt clean.Closes #353.