Performance Tuning¶
mlxcel chooses conservative launch and attention policies automatically. Tune only after measuring a representative prompt length, batch size, model, cache mode, and device. A value measured on one GPU or context shape is not a portable default.
Kernel Autotuning¶
mlxcel tune profiles supported launch candidates and stores winners under ${MLXCEL_CACHE_DIR:-$HOME/.cache/mlxcel}/autotune. Writing the cache does not change inference until MLXCEL_AUTOTUNE is enabled.
# Print the matrix without profiling
mlxcel tune --dry-run
# Tune all operations supported by this backend
mlxcel tune
# Tune paged decode using a checkpoint's head geometry
mlxcel tune --op paged-decode-splits -m ./models/qwen3-4b-4bit
# Consume only cached winners
MLXCEL_AUTOTUNE=cache mlxcel serve -m ./models/qwen3-4b-4bit
# Consume cached winners and tune unseen shapes on first use
MLXCEL_AUTOTUNE=1 mlxcel serve -m ./models/qwen3-4b-4bit
Supported operations are paged-decode-splits on Metal and CUDA, plus currently unvalidated CUDA-only qmm-tile and qmv-multirow sweeps. The default matrix covers batch sizes 1,2,4,8 and context lengths 1024,4096,16384. Use --model or explicit --head-dim, --q-heads, --kv-heads, and --block-size to match production geometry. The profiler uses warmup, adaptive sampling, median scoring, and a default 2% minimum improvement so noise does not replace a stable default.
Attention Dispatch¶
The server's --decode-storage-backend auto default selects paged decode only for qualified model families and falls back to dense storage elsewhere. Leave it on auto unless a controlled comparison proves a different choice on the exact workload. The current automatic paged set includes Llama, Qwen 3, Qwen 3.5 text, Gemma 3 text, and Llama 4 workers; internal-cache and hybrid families remain dense until they provide model-owned paged dispatch.
Advanced paths are also policy-gated:
| Path | Scope | Default behavior | Important controls |
|---|---|---|---|
| Absorbed MLA decode | MLA families such as DeepSeek | Uses compressed latent KV representation where the family supports it | MLXCEL_MLA_* diagnostics and kill switches are documented by --help/environment reference when exposed |
| Cascade attention | Paged decode with long shared prefixes | Detects useful two-level prefix groups and declines small or unprofitable groups | Keep automatic policy unless profiling a repeated-prefix serving workload |
| Fused sparse paged decode | MiniMax-M3 sparse decode | Dispatches only when selected rows clear the kernel floor and context/selection sparsity is at least 8 | MLXCEL_SPARSE_PAGED_*; dump mode is debugging-only |
| TurboQuant KV | Long-context dense models | fp16 unless a quantized cache mode is selected | See TurboQuant KV Cache |
The primary sparse-paged controls are MLXCEL_SPARSE_PAGED_ATTENTION and MLXCEL_SPARSE_PAGED_MIN_SPARSITY. Sparse paged decode is token-exact for the implemented MiniMax-M3 route. DeepSeek Sparse Attention is not routed through that kernel because its positional score requires an additional K stream that the current v2 kernel does not accept. CUDA implementations of several advanced kernels remain unvalidated; do not infer CUDA qualification from a Metal result.
Host and Command-Buffer Controls¶
MLX_MAX_OPS_PER_BUFFER automatically defaults to 1000 on pre-M5 Apple Silicon (M1-M4) and keeps the MLX default on M5 or non-Apple targets. On the same pre-M5 hosts, MLXCEL_DECODE_MB_PER_BUFFER raises the Metal command-buffer input budget to 1000 for pipelined decode steps only: with the op cap raised, the input budget is the limit that binds, and at MLX's default a decode token commits a buffer every one or two layers. Prefill keeps the device default because the larger budget roughly doubles peak memory on long prompts. Setting MLX_MAX_MB_PER_BUFFER yourself pins one value for both phases and turns the decode-only raise off.
On CUDA, MLX_CUDA_GRAPH_CACHE_SIZE and MLX_CUDA_SDPA_CACHE_SIZE default to 2000 to avoid cache thrashing in long-lived, shape-diverse serving. On compute capability 12.1 (GB10), MLX's graph capture budgets are the tightest in its table, and mlxcel raises them (MLX_MAX_OPS_PER_BUFFER=100, MLX_MAX_MB_PER_BUFFER=1000) only for model families that gained on every measured workload, which is laguna today; other families gained on some workloads and lost on others. mlxcel generate prints the applied budget next to its compute-capability line and mlxcel-server logs it. An explicit environment value always wins.
Model load reads and uploads a generation model's checkpoint tensors before it reports Model loaded, so the first token pays no weight I/O and the reported load time covers resident weights. Tensors of vision and audio towers stay lazy until a request carries that modality. MLXCEL_WEIGHT_MATERIALIZATION=lazy materializes each tensor on first use instead. On CUDA, load stages tensors through four reused 64 MiB pinned host buffers; MLXCEL_CUDA_PINNED_LOAD=0 forces the pageable path. MLXCEL_PROFILE_TTFT adds the upload counters to the [TTFT] line.
On M1 and M2, dense products of 2 to 6 rows run on the GEMV kernel, which raises batched decode at small batch sizes. MLXCEL_DENSE_GEMV_ROWS=0 restores the upstream dense dispatch. On CUDA, MLXCEL_SDPA_DETERMINISTIC=1 selects a reproducible decode SDPA path for comparisons that need identical output between runs.
Decode runs on one batch-native engine: a single sequence is a batch of one, and mlxcel run, mlxcel generate, and the server share the same sampling and finish step. Most model families decode natively as a batch, and the server logs decode path: native batched forward per model at startup.
On M1 Ultra, batched decode still has measured gaps: B=1 decode is 3 to 6% lower than before the batched decode work on a few checkpoints, and Mixtral stays below its B=1 rate at B=8 with a warm prompt cache (#2817). Measure your own checkpoint at the batch size you serve before raising --max-batch-size.
For server throughput, tune --n-parallel, --max-batch-size, --prefill-chunk-size, --prefill-grant-interval, --batch-size, and --ubatch-size together. Raising concurrency increases throughput only while the model, KV cache, and temporary tensors fit. Use Memory Estimation, /metrics, and a steady-state load test instead of relying on load time alone.
Measurement Checklist¶
- Record the exact mlxcel version, checkpoint revision, device, cache mode, prompt length distribution, output length, and concurrency.
- Warm the model and kernel cache before collecting samples.
- Compare prefill tokens/s, decode tokens/s, time to first token, tail latency, memory high-water mark, and request failures.
- Run one change at a time and repeat enough samples to overcome thermal and background-load noise.
- Keep a kill switch or the
autobaseline available. If an advanced path reports a fallback, do not claim its performance from that run.