Repository navigation
Add cached_lookup to cache kernel lookups per signature - #1061
Merged
Merged
Conversation
Back-ends look up the compiled kernel on every launch: CUDA.jl's `cufunction` resolves the method instance, builds a compiler job, takes a lock and queries the code cache, about 5500 instructions or a third of a small `@cuda` launch. `cached_lookup` remembers the results of such a lookup per kernel signature and reuses them while the task's world age and a back-end key (e.g. the context and the compiler keyword arguments) are the same, which takes a few tens of instructions. The world has to match exactly, as code that is still valid in a newer world can be the wrong code once a more specific method exists. Results are not stored while a compile hook is active, so that the `@device_code_*` macros see every lookup, or while generating output, as the cache is serialized with its callers.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #1061 +/- ##
==========================================
+ Coverage 92.38% 92.42% +0.03%
==========================================
Files 31 31
Lines 7285 7323 +38
==========================================
+ Hits 6730 6768 +38
Misses 555 555 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This was referenced Oct 26, 2026
Merged
Merged
Merged
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.
Back-ends look up the compiled kernel on every launch. CUDA.jl's
cufunction, for example, resolves the method instance, builds the compiler config and aCompilerJob, takes a lock, queries the code cache throughcached_results, and scans for the context'sCuFunction. On a cache hit that is about 5500 instructions (0.20 µs), a third of the instructions of an asynchronous@cudalaunch. For back-ends with cheaper launches, like the POCL host kernels in KernelAbstractions, the lookup is most of the launch.This adds
GPUCompiler.cached_lookup(lookup, V, F, tt, key)::V, which back-ends wrap around that lookup:It remembers the result per lookup function type, kernel signature, key type and result type, and reuses it while the task's world age and the back-end's key (here the context and the compiler keyword arguments, compared with
isequal) are the same. The cache is a constant that a@generatedfunction embeds in the caller, so a hit is an atomic load and a couple of comparisons, with no lock or hashing. It's an immutable list of the 4 most recent results, so a kernel used on a couple of devices or from tasks in different worlds doesn't keep missing.The world has to match exactly. Checking that the cached code is still valid in the current world isn't enough: after compiling
f(::Number)for anInt, addingf(::Int)leaves that code valid, but the kernel call now dispatches to the new method. In another world the lookup runs again and normally finds the compiled code in GPUCompiler's cache. Only the lookup's result is kept, not the callable, as its captured values have to come from each launch. Nothing is stored while a compile hook is active, so that@device_code_*sees every lookup, or while generating output, as the cache is serialized along with its callers. Back-ends that can recreate sessions should put something in the key that changes when they do; there's no API to clear the cache.I also looked at making the uncached lookup cheaper for back-ends that don't adopt this, but there is little to gain there. Half of a
cufunctionhit isjl_rettype_inferredcomparing the cache owner token field by field (box identity of the immutable token isn't preserved, sojl_egal's pointer check never hits), andmethodinstanceis already cheap.I adopted this locally in two back-ends, measured on an RTX 5080 with Julia 1.12 (minimum time, user-space instructions on the calling thread):
cufunction(hit)@cuda(async)clfunction(hit)Most of what remains in CUDA.jl's
cufunctionis fetching the task-local context. The POCL "before" is JuliaGPU/KernelAbstractions.jl#923, which hand-writes the same cache; its tests pass withcached_lookupinstead, as do CUDA.jl's execution, exception, reflection and KernelAbstractions tests.The tests cover the more specific method, callee redefinition, lookups in an older world, captured values and the callable's lifetime, key changes and eviction, compile hooks, exceptions, concurrent use and precompilation. The test suite passes on Julia 1.10 and 1.12.