Find the slow op, fix it, re-measure
When MLX inference is slower than you want, the answer is rarely "the framework is slow." It's almost always "this specific operation in your specific workload is the bottleneck, and you didn't know which one until you measured." This lesson is the practical profiling toolkit.
The two-tier approach
Tier 1: MLX-level profiling. mlx-lm and mlx itself expose enough metadata to tell you where time goes within MLX's view of the world — generation_tps, peak_memory, per-eval timing. Use this to identify which call is slow.
Tier 2: Metal-level profiling. When MLX-level data isn't enough (e.g. you've narrowed down to one matmul but want to know which Metal kernel is slow), Xcode's Metal Debugger gives you GPU-level traces. Heavier setup, but the only path when the bottleneck is below MLX's API surface.
Tier 1 — Built-in mlx-lm metadata
The GenerationResponse objects from stream_generate (covered in lm.lesson2) carry generation_tps and peak_memory. Log them in production and you have throughput + memory traces for free without a separate observability stack.
For non-LLM workloads, wrap the suspect MLX call in time.perf_counter() with mx.eval() at the boundary. Don't measure with print() in the hot loop — terminal I/O contaminates the measurement (covered in core.lesson7).
Tier 2 — Xcode Metal Debugger
Open Xcode → Debug → Capture GPU Frame while your MLX workload runs. The capture shows you each Metal kernel dispatch with timing. The output is dense — you'll see kernels named after the MLX operations that produced them — but identifying the longest-running kernel almost always points at the right hot spot.
This is the right tool when you're trying to understand why a particular MLX operation is slow at the kernel level. For most users this is overkill; learning Tier 1 is enough.
The fix-and-re-measure loop
Profiling without action is debugging tourism. Once you've identified the slow op, the fixes generally fall into:
- Use
mx.compileon the hot function (core.lesson7). Often the cheapest 2-5× speedup available. - Quantize if you haven't (model size affects memory bandwidth, which is often the real bottleneck on Apple Silicon).
- Reshape your computation to match MLX's strengths — fewer, larger ops fuse better than many small ones.
- Replace a CPU fallback if you discover one (rare in MLX, common in PyTorch MPS).
- Upgrade MLX — the framework moves fast and kernel improvements ship every few weeks.
Then re-profile to confirm the fix. Don't skip the re-profile step; intuition about what's faster is unreliable.