Repository navigation
[0.10.3] perf(metal): cache generated custom-kernel sources per kernel function - #47
Draft
jonathan308 wants to merge 1 commit into
Draft
jonathan308 wants to merge 1 commit into
jonathan308 wants to merge 1 commit into
Conversation
A custom Metal kernel function rebuilt its whole generated source (header, signature, body and template instantiation) on every call, and every dispatch then hashed that source to name its library. A one-token decode step of a large model calls the same few kernels hundreds of times (about 700 calls per Nemotron 3.5 Super token), all of it host time on the engine thread that builds and encodes the step. Each kernel function now keeps its generated sources keyed by the kernel name (template values, input dtypes and address spaces, output dtypes) plus the template parameter names, and passes each source's hash to the primitive, which names the library from it instead of hashing again. The generated sources, compiled libraries and dispatches are unchanged.
jonathan308
added a commit
to Layr-Labs/mlx-swift-lm
that referenced
this pull request
Oct 24, 2026
The cached custom-kernel sources (Layr-Labs/mlx#47 via mlx-swift#65) did not meet the 0.10.2 bar (>= 5% Super c=1 decode on the M5 with bit-identical outputs) and move to 0.10.3. This PR keeps only the shared host-scalar kernel arguments, on main's mlx-swift pin.
This branch has not been deployed
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.
Summary
A custom Metal kernel function rebuilt its whole generated source on every call, and every dispatch hashed that
source again to name its library. Nemotron 3.5 Super makes about 700 fused-kernel calls per decoded token, all on
the engine thread that builds and encodes the step.
Same generated sources, same library names, same pipelines and dispatches. The cache is per kernel function,
mutex-guarded, bounded at 256 specializations (a function sees a handful).
Rebased onto ff4bb35 (#43). No overlap with #43's compile-cache erase or #40's floor divide.
Measurements (M3 Ultra)
Host graph construction of one Nemotron 3.5 Super decode token (88 layers at the real dimensions, lazy synthetic
weights, no GPU work;
DecodeBench buildonly, median of 200 tokens, 3 alternating runs, quiet CPU):Per call: the Mamba scan custom kernel 11.5 µs → 4.6 µs.
M3 Ultra result (shipping 0.10.2 stack: provider cc7a611f3, mlx-swift eedca444, MLX ff4bb35)
Super, plain owner (target only), c=1, sampled T=1.0/top_p 0.95, 3 x 512 tokens per build, same session order
(
base219:15,fix219:20 UTC).fix2= all three changes. The independent consolidation run of the samestack at 19:06 measured 40.76 / 40.77 / 40.80.
Why there is no gain: a Metal System Trace of
base2in steady decode shows the GPU 99.95% busy(0.013 ms idle per 25.1 ms token, gaps between command buffers p50 1.1 µs, none over 100 µs) with 12.2 command
buffers per token, so the 100-op decode scope is in effect. The engine thread is busy 55% of the time (13.8 ms of
CPU per token) and is never on the critical path. Host-side savings cannot raise c=1 decode on the M3. On the M5,
GPU time per token is ~17 ms at 57.9 tok/s, while the engine thread should take ~10–11 ms, so it is GPU-bound too
(an inference; the M5 run was skipped as not borderline). The remaining half of the roofline is GPU kernel
efficiency (0.10.3).
11ff5cf3→ Layr-Labs/mlx-swift#65738de4e0bb40f3c7(pin reverted to main's eedca444)c6b4d32edEvidence
python/tests/test_fast.py::test_custom_kernel_reuses_sources_per_specialization: one kernel function calledthree times over 2 template values x 3 dtypes, interleaved; every call returns its own specialization's result.
The existing custom-kernel tests (
test_custom_kernel_caching,..._same_name_different_source_one_eval,..._mixed_dtypes,..._negative_template_int) cover distinct functions, distinct sources and dtypes.scripts/check_forkdiff.pypasses with the newfork.yamlsection.Merge Danger
Door: two-way. Revert the commit; nothing persists.
Blast Radius: host-only. Every custom Metal kernel call goes through the cache; a wrong key would run another
specialization's source, which the key (kernel name already encodes template values, input dtypes and address
spaces, output dtypes; plus template names) rules out.
Refactor pass: pending (parent runs it).