Conversation
A warm launch of a two-argument kernel on the CPU backend allocated about
50 times (2.2 KiB). Most of it came from Julia not specializing on
`args...` that a method only splats onward, which boxed every kernel
argument on the way from `Kernel` to `clSetKernelArg`; the rest from
per-launch vectors for the work sizes and SVM pointers, a keyword splat
through the generated kernel call, and a device property query.
- Annotate the splatted arguments as `Vararg{Any, N}` in the `Kernel`
call, the generated `AbstractKernel` call, `call` and `set_args!`, and
unroll `set_args!` by recursion instead of `enumerate`.
- Pass each kernel argument to `clSetKernelArg` as a `Ref{T}` `ccall`
argument, which copies it to the stack.
- Pass the global and local work sizes as three-element tuples through a
`ccall` taking `Ref{NTuple{3, Csize_t}}` when there is no offset or
device RNG state, skipping the `max_work_item_dims` query.
- The generated kernel call takes `global_size` and `local_size` as
explicit keywords.
- `clconvert` and `call` take `nothing` for the SVM pointers instead of
allocating an empty vector; no adaptor rule collects such pointers.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VHciC8x39gm97sABrSvBkt
Every launch went through `clfunction`, which built a compiler config,
looked up the method instance, allocated a `CompilerJob`, queried
GPUCompiler's cache and hashed its way into `_kernel_instances` before
returning the `HostKernel` it had returned last time. Keep the resolved
`HostKernel` per `HostKernel{F, tt}` type together with the world age and
context it was resolved in, and return it directly while no method has
been defined since and the context is unchanged. Reflection (a set
`compile_hook`) and explicit compiler keywords bypass the fast path.
Reading the `compile_hook` scoped value allocates, so it is only read
when a dynamic scope is active. The `Ref` used to find the kernel for the
current context and the `@something` in `compile_or_lookup` are replaced
with plain control flow, which also removes a boxed variable captured by
the `get!` closure.
A warm launch now allocates once, for the event handle.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VHciC8x39gm97sABrSvBkt
| # Reading a `ScopedValue` allocates; outside of any dynamic scope it holds its default. | ||
| @static if VERSION >= v"1.11" | ||
| @inline compile_hook_set() = Core.current_scope() !== nothing && GPUCompiler.compile_hook[] !== nothing | ||
| else | ||
| @inline compile_hook_set() = GPUCompiler.compile_hook[] !== nothing | ||
| end |
There was a problem hiding this comment.
Uhm, I think this was resolved in some version of Julia, right? Don't remember which one offhand though, would need to check
There was a problem hiding this comment.
Probably only v1.14: JuliaLang/julia#61053
Reading a `ScopedValue` no longer allocates on Julia 1.14, and checking `Core.current_scope()` first costs as much as the read itself there, so the guard is limited to the versions that need it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VHciC8x39gm97sABrSvBkt
|
Can we split this up? In particular the cache fast path needs a closer look |
They're separate commits already. Could open separate PRs (although I can't do stacks, as I can't push here) |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #766 +/- ##
==========================================
+ Coverage 63.72% 64.04% +0.31%
==========================================
Files 23 23
Lines 1935 1955 +20
==========================================
+ Hits 1233 1252 +19
- Misses 702 703 +1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
You should now be able to |
|
Closing in favour of #768 |
Cutting down a warm CPU kernel launch from about 50 allocations to 1.