Conversation
On M1 through M4 mlxcel raises `MLX_MAX_OPS_PER_BUFFER` to 1000 (#353) but leaves MLX's second commit trigger, `MLX_MAX_MB_PER_BUFFER`, at 40-50. That budget counts input elements (`data_size() >> 20`), not bytes, and decode reads the whole weight set every token, so a 4-bit 7B model commits about 23 command buffers per token and the GPU idles about 40 us at each boundary (xctrace: 89.5% busy). A budget of 1000 lifts decode on M1 Ultra by up to 21% (command-r7b +7%, Qwen3-30B-A3B +20%, Mixtral +21%, Llama 3.1 8B bf16 +17%, Gemma 3 4B flat), but applied globally it also keeps prefill activations alive per buffer and roughly doubles peak memory on a 2048-token prompt (Qwen3-30B-A3B 19.8 to 36.2 GB). It also hurts a synchronous decode step, which encodes one large buffer before the GPU can start. `Device` latches the budget once, so a `device.cpp` overlay adds a runtime override that `needs_commit` reads, and `DecodeCommandBufferBudget` raises it only around pipelined decode: the generate loops and the server's lookahead tick and prime. `MLXCEL_DECODE_MB_PER_BUFFER` tunes or disables it; an explicit `MLX_MAX_MB_PER_BUFFER` still pins both phases. command-r7b 4-bit on M1 Ultra, same binary with the switch on and off (`MLXCEL_DECODE_MB_PER_BUFFER=0`): CLI harness decode 104.2-104.4 to 110.1-111.2 tok/s, server lookahead decode median 110.0 to 118.5 (ten requests, two starts), synchronous server decode unchanged, prefill and pp2048 peak memory unchanged. Speculative loops are not wired yet. Measurements are in docs/benchmark_results/metal-mb-per-buffer-m1ultra-2026-09-21.md.
|
The |
|
The re-run failed the same gate again, just over the 2% target: I could not find a path by which this PR affects that measurement on the GB10 runner. Off Apple Silicon |
Summary
On M1-M4 mlxcel raises
MLX_MAX_OPS_PER_BUFFERto 1000 (#353) but leaves MLX's other commit trigger, the input budgetMLX_MAX_MB_PER_BUFFER, at 40-50. That budget counts input elements (data_size() >> 20), not bytes, and decode reads the whole weight set every token, so a 4-bit 7B model splits each token into about 23 command buffers and the GPU idles about 40 us at every boundary (Metal System Trace: 89.5% busy).Why decode only
Raising the budget globally to 1000 roughly doubles prefill peak memory on long prompts (2048 tokens: Qwen2.5 7B 6.0 to 12.8 GB, Qwen3-30B-A3B 19.8 to 36.2 GB), because a buffer's activations stay alive until it completes. It also hurts a synchronous decode step, which then cannot start on the GPU until the whole step is encoded: server decode for
ignore_eosrequests went from a steady 98-102 tok/s to 68-102.Devicelatches the budget once, so:mlx/backend/metal/device.cppoverlay adds a process-wide override thatneeds_commitreads (three marked hunks to re-apply on pin bumps);DecodeCommandBufferBudgetraises it around pipelined decode only: the four generate loops (not underMLXCEL_FORCE_SYNC) and the server's lookahead tick and prime. Prefill and synchronous decode steps stay on the device default;MLXCEL_DECODE_MB_PER_BUFFERtunes or disables it, and an explicitMLX_MAX_MB_PER_BUFFERstill pins both phases. The hardware gate matches the op cap (M1-M4).Measurements (M1 Ultra, 500-token prompt, 128 tokens)
With the switch as shipped, command-r7b, same binary on and off: CLI harness 104.2-104.4 to 110.1-111.2 tok/s; server lookahead median 110.0 to 118.5 (ten requests, two starts); server synchronous decode unchanged (100.6-101.1 off, 100.5-102.1 on); pp2048 peak memory unchanged (Qwen2.5 6.01 vs 6.07 GB, Qwen3-30B-A3B 19.75 vs 19.75 GB). Full record:
docs/benchmark_results/metal-mb-per-buffer-m1ultra-2026-09-21.md.Validation
Unit tests cover the overlay round trip, guard nesting and restore, and the resolution precedence. The full workspace gate (
make verify-test, 11,411 passed), clippy and fmt pass on the branch this was developed on; on this branch, clippy, fmt and the new tests. Command-buffer boundaries do not change arithmetic, and greedy output for three prompts at 200 tokens is identical to main.Not measured: M2-M4 other than Ultra, M5 (kept on MLX defaults), speculative decode loops (not wired yet), decode beyond 2048 tokens of context.