Repository navigation
metal_kernel: build and hash each kernel variant once (gemma4-qat#189) - #5
Merged
Merged
Conversation
…ma4-qat#189) The closure returned by fast::metal_kernel rebuilt the full kernel source (header included) on every call, and CustomKernel::eval_gpu hashed that source again on every dispatch to form the library key. For a kernel with a long header this is several microseconds of host work per call. - The closure now keeps a memo, shared by all copies of the closure, that maps each call variant (input dtypes and pass mode, output dtypes, template argument kind + name + value) to its kernel name, source and source hash. The memo is guarded by a mutex; entries are never changed or removed, so a pointer to one stays valid after the lock is released. - CustomKernel holds its source in a shared_ptr<const std::string> and takes an optional precomputed source hash. The std::string constructor and state() are unchanged, so export/import is unchanged. - The Metal library key keeps its format ("{name}_{hash:x}_{options}"). The memo key includes the template argument kind, so `int` 1 and `bool` true (which print the same template value) are separate variants. New C++ tests (tests/metal_kernel_tests.cpp) cover this, check that memoized name and source equal a fresh build over each input pass mode and dtype, and call copies of one kernel from 8 threads. A Python case is added to test_custom_kernel_caching. The CUDA edit (use the stored hash and dereference the source) is not compiled here.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
…t#189) The memo added in the previous commit kept every variant's generated source for the life of the kernel function. A kernel called with many distinct template values (for example a large header and 10,000 integer values) kept all of those sources, with no limit. - The memo now holds at most metal_kernel_max_cached_variants (64) entries. When it is full, a new variant clears it. - Entries are shared_ptr<const KernelVariant>. A caller copies the shared_ptr under the lock, so no raw pointer to an entry escapes the lock, and clearing is safe while other threads use their entries. - A CustomKernel primitive already owns its source through a shared_ptr, so an array made before its entry was cleared still evaluates. Clear-all is the simplest bounded policy. A kernel with more than 64 live variants rebuilds them after each clear, which costs the same as a call did before the memo. FIFO or LRU would not help a cyclic working set larger than the cap and need more state. CustomKernel::shared_source() exposes the source pointer for tests. New C++ test: 200 distinct template values give correct results, at most 64 sources stay alive, an evicted variant builds a new equal source, and a pending array whose entry was evicted evaluates correctly. The Python test_custom_kernel_many_template_values covers the same through the Python API.
jeethu
added a commit
to numen-tech/mlx-swift
that referenced
this pull request
Sep 29, 2026
… gemma4-qat ml-explore#189) Bumps Source/Cmlx/mlx to numen-tech/mlx#5 head b2c93bd1: the per-kernel variant memo now holds at most 64 shared_ptr-owned entries and is cleared when full (Codex P2). tools/update-mlx.sh (run with LC_ALL=C) changes no generated source; only the Metal.hpp commit stamp moves.
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.
Implements numen-tech/gemma4-qat#189 (epic ml-explore#178): build and hash a custom kernel's source once per variant instead of on every call and every dispatch.
Merge order: this PR first, then the numen-tech/mlx-swift PR that bumps
Source/Cmlx/mlxto this head, then the gemma4-qatperf/189-kernel-hash-memopin. Please merge with a merge commit so the head SHA that mlx-swift pins stays reachable.Change
fast::metal_kernel's closure keeps ashared_ptr<VariantCache>that all copies of the closure share. mlx-c copies the closure. The cache maps a variant key to{kernel name, shared_ptr<const string> source, source hash}. A mutex guards find and insert. The build itself runs outside the lock, and a racing duplicate build is harmless because the insert keeps the first entry.Bounded memo (b2c93bd, Codex P2). Entries are
shared_ptr<const KernelVariant>, and the map holds at mostfast::metal_kernel_max_cached_variants(64) of them per kernel function. When it is full, a new variant clears it. A caller copies theshared_ptrunder the lock, so no raw entry pointer leaves the lock. ACustomKernelowns its source through ashared_ptr, so an array made before its entry was cleared still evaluates. Clear-all is the simplest bounded policy: a kernel with more than 64 live variants rebuilds them after each clear, which costs what every call cost before the memo. FIFO or LRU would not help a cyclic working set larger than the cap and need more state. Retained source is now at most 64 × the source size (about 6 MB for a 95 KB header), not one source per distinct variant.Variant key. The key encodes the raw per-call arguments that the generated name and source depend on:
write_signatureThis equals the issue's "name + template names/kinds" key, and it skips
write_template's ostringstream on a hit. Everything else that feeds the source (names, header, body, shape infos,ensure_row_contiguous, attributes) is fixed per closure.int1 vsbooltrue. Both print1in the template string. The key includesTemplateArg::index(), so they are separate variants. The raw value widths also differ.CustomKernel. It storesshared_ptr<const std::string> source_andsize_t source_hash_, and a new constructor takes the shared source plus an optional precomputed hash. The oldstd::stringconstructor delegates to it, which export deserialization needs, andstate()still returns the source string, so the export format is unchanged. The Metal library key format"{name}_{hash:x}_{options}"is unchanged.CUDA.
custom_kernel.cppuses the stored hash and dereferences the source. This file was not compiled here (no CUDA toolchain).Tests
build/tests/tests) at b2c93bd. The baseline had 283; the difference is the 4 new cases intests/metal_kernel_tests.cpp:weak_ptrs through the newCustomKernel::shared_source()), an evicted variant builds a new equal source, and a pending array whose entry was evicted evaluates correctly.to_string, fails the int/bool test. With the clear removed, the bounded-memo test fails (196 live sources > 64).MLX_METAL_JIT=ONbuild).test_fast+test_compile99 OK (1 skipped), andtest_export_import22 OK. A new int/bool case was added totest_custom_kernel_caching. At b2c93bd, the newtest_custom_kernel_many_template_values(200 template values, a pending array evaluated after eviction) passes, andtest_fast+test_export_importgive 55 OK (1 skipped).Microbench (loaded machine, indicative only)
This is host time per call for a trivial kernel, in µs:
quantized.h)The bounded memo (b2c93bd) measures the same as the unbounded one in the same run: build ~0.2 / ~0.2, eval ~1.9 / ~1.8.
The gemma4-qat side adds a report-only Swift microbench to
ParoQuantMLXTests. Quiet-machine numbers will follow there.Notes
shape_infosand the kernel name are still copied into eachCustomKernel. This behaviour predates the PR, and both are small.