Metal command-buffer limits (50 ops / 50 MB) cost 5–8% decode throughput on launch-heavy models (M3 Ultra measurements)
Nobody has claimed this yet.
- Dominant language
- C++
- Stars
- 28.5k
- Forks
- 2.3k
- Avg merge
- 3d 8h
- Merged PRs (30d)
- 62
Description
Device commits a Metal command buffer after max_ops_per_buffer dispatches or when the distinct input buffers referenced exceed max_mb_per_buffer (50 / 50 on Ultra-class devices, backend/metal/device.cpp). For a model whose weight matrices are tens of MB each, the byte rule trips roughly once per layer, so a single-token decode step spans 45–90 command buffers. Each boundary costs about 20 µs of GPU time plus a completion handler on the CPU.
Measured on an M3 Ultra (mlx 0.32.2), GLM-5.3-Flash mixed 4/6-bit (45 layers, MoE + hybrid attention, ~1,600 kernels per token), single-stream decode, same prompts, output hashes compared across arms:
| arm | 20k-token context | short context |
|---|---|---|
| defaults | 35.68 ms/token | 30.91 ms/token |
MLX_MAX_OPS_PER_BUFFER=1000, MLX_MAX_MB_PER_BUFFER=400 |
33.94 (+5%) | 28.71 (+7.7%) |
| defaults again (control) | 35.29 | 30.91 |
Outputs bit-identical, prefill time unchanged, peak memory unchanged during a cold 20k-token prefill. A synthetic chain of 3,200 tiny dependent kernels goes from 8.8 ms to 7.7 ms per step at 200 ops per buffer.
The same variables on a dense Qwen3.8-27B 8-bit model (about 1,000 large matvecs per step) change nothing (52.8 → 53.0 ms/token short, 71.3 → 72.8 at 20k), so the effect is specific to launch-heavy graphs.
Suggestion: scale the byte limit with the size of the largest input buffers, or raise the Ultra defaults; alternatively document the two variables, which are currently undocumented.
A write-up with the per-segment decode profile behind these numbers follows on my blog; happy to share the profiling driver (flag-gated, repeats a segment on sibling layers via mx.depends) if useful.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in backend/metal/device.cpp, where max_ops_per_buffer and max_mb_per_buffer govern command-buffer commits. Reproduce the reported launch-heavy decode measurements and compare them with the dense-model control. Done means selecting and implementing one limit or documentation direction, while confirming output hashes, prefill time, and peak memory remain unchanged.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp
- Domain
- backend, performance
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100