Add KernelAbstractions.@spawn and record_event/wait_event - #750
Conversation
Benchmark ResultsShow table
Benchmark PlotsA plot of the benchmark results have been uploaded as an artifact to the workflow run for this PR. |
|
@christiangnrd we should have this for KI 0.2 as well |
Sure! Is the idea that things you write your code assuming events are supported, and on backends where it isn't it just silently falls back to sequential operation? |
|
Yeah the sequential ordering is the correct thing, and the events are an optimization. |
There was a problem hiding this comment.
Claude seems to find it very important that someone reading any part of the documentation know that this defaults to synchronize and that if record_event is implemented, wait_event must also be implemented.
I think the docs should focus more on desired behaviour, and any implementation recommendations/guidelines should be tucked away and clearly marked as such since realistically it'll be one of us adding backend support and users probably won't care about implementation unless things break
|
It'll probably end up in KI 0.2.0, but if I understand correctly, since the backend implementations are just an optimization (and optional), this can release whenever and backends just set compat to whatever version this is released in once they add support |
|
I've added support for the device selection interface in KA in my Opencl branches (feel free to review JuliaGPU/OpenCL.jl@fe59d68) so once #754 is in I'll rebase this and the OpenCL CUDA tests should pass! |
54c0185 to
8db2ced
Compare
42e70b7 to
c46c49b
Compare
|
@vchuravy I just pushed a few commits that address my review comments and also some stuff that Claude found. They're all documentation changes, except for 586d110, which you should review particularly more closely. It also brought up another issue that I didn't fix since I'm not sure what the best approach would be. Bot explanation related to
|
We could do but then what do we do if sync throws as well. I tend to agree the right thing is to say: In case of an exceptional exit the task is not synced. |
Done |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #750 +/- ##
==========================================
+ Coverage 66.18% 66.81% +0.63%
==========================================
Files 23 24 +1
Lines 1990 2025 +35
==========================================
+ Hits 1317 1353 +36
+ Misses 673 672 -1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
fbe5f5d to
ecdc1cf
Compare
`KernelAbstractions.@Spawn backend expr` runs `expr` on a new Julia task while keeping the work queued on `backend` ordered between the two tasks: 1. the spawning task calls `record_event(backend)`, 2. the new task selects the same device and calls `wait_event`, 3. after `expr` returns the task calls `synchronize(backend)`, so that `wait(task)`/`fetch(task)` imply all of its device work has completed. `record_event` defaults to a full `synchronize` returning `nothing`, and `wait_event(::Backend, ::Nothing)` is a no-op, so every backend gets the "synchronize before, synchronize after" discipline for free. Backends with task-local streams may opt in by recording an event instead and implementing `wait_event` for it as a stream wait. Also document that `synchronize` should be cooperative, since a blocking implementation would serialize otherwise independent spawned tasks. Assisted-by: Claude Code (Fable 5.1)
`Threads.@Spawn :interactive` runs the task in the `:default` pool when the interactive pool has no threads, so asserting `Threads.threadpool() === :interactive` fails on Julia 1.10 and 1.11, which do not start an interactive thread by default. Expect whichever pool Julia will actually use. Assisted-by: Claude Code (Opus 5)
The macro expanded Threads.@Spawn inside its own hygienic scope, so the sync variable Threads.@Spawn escapes resolved to a global in KernelAbstractions rather than the caller's @sync block: tasks created by KernelAbstractions.@Spawn were never awaited by @sync, and their errors were dropped. The same scoping broke $x interpolation in the body. Escape the whole expansion and gensym our temporaries instead. Add tests for @sync and $x. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The implementer notes already say synchronize is required to be cooperative; the docstring said backends "should" make it so. Say "must" in both places, and link to the implementer notes for the rationale instead of repeating it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…uirement Apply the review suggestions: backends should give each task its own queue, and drop the paragraph about single-queue backends. Note that a new task does not necessarily inherit its spawner's device, so multi-device backends must implement the device interface for @Spawn to pin the task correctly. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The @Spawn docstring and the quickstart walked through the record_event/wait_event/synchronize protocol. Users only need the guarantees: same device, ordered after the spawner's queued work, and complete once wait/fetch returns. Keep the protocol on the implementer page and point backend authors there. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Keep the pairing requirement in both, cross-linked, and point to the implementer notes for the protocol instead of restating it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`wait_event` is queue-ordered, not task-ordered: it makes the queue of the device that is active when it is called wait on the event. Say so, and spell out what follows: - `device!` is not a synchronization point; work queued before a switch is unordered with respect to work queued after it. - The way to order across a switch is to record, switch, then wait -- which is the order `@spawn` already uses, now with a comment saying why. - A multi-device backend has to accept an event recorded on another device, either as a driver-level dependency (CUDA's `cuStreamWaitEvent`) or by waiting cooperatively. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KfecYSmYnSAbm5Uu476fQf
`@spawn [threadpool] backend [device=id] expr` now accepts a device to run on, defaulting to the spawning task's as before. The event is still recorded on the spawning task's device, so the ordering guarantee holds across the switch -- which is what the cross-device `wait_event` requirement is for. Document the reason `@spawn` selects a device at all: backends hold it in task-local state, which Julia does not copy into a child task, so a plain `Threads.@spawn` would run on the backend's default device instead of the caller's. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KfecYSmYnSAbm5Uu476fQf
ecdc1cf to
ccccf07
Compare
|
@vchuravy I think the order of operations should be merge this, implement in the backend branches, test functionality, if it's all good we release the KernelInterface part as v0.2.1 (or 0.3 if we end up requiring breaking changes) |
Co-authored-by: Valentin Churavy <v.churavy@gmail.com>
|
I think this is ready from my side. I adjusted the language on some of the implementation notes. |
Summary
Adds
KernelAbstractions.@spawn backend expr, which runsexpron a new Julia task while keeping the work queued onbackendordered between the two tasks. It encodes the discipline users otherwise have to write by hand:The macro's protocol:
record_event(backend).device!, then callswait_event(backend, event).exprreturns, the task callssynchronize(backend), sowait(task)/fetch(task)imply that all of the task's device work has completed.An optional first argument is forwarded to
Threads.@spawnas the threadpool (@spawn :interactive backend expr). The macro is not exported, to avoid clashing withThreads.@spawn.Backend opt-in
Two new optional functions in KernelInterface:
record_event(backend)defaults to a fullsynchronizereturningnothing, which is always correct.wait_event(::Backend, ::Nothing)is a no-op.A backend with task-local streams (CUDA.jl, AMDGPU.jl, ...) can override
record_eventto record an event on the current stream without blocking, and implementwait_eventfor that event type as a stream wait, turning the protocol intoBackends with a single global queue (POCL) need no changes.
Docs
synchronizedocstring now recommends a cooperative, non-blocking implementation, since a blocking one serializes otherwise independent spawned tasks.@spawn" section; the quickstart's task-programming section now recommends@spawn.Test plan
Spawntestsuite entry (test/spawn.jl): ordering after the parent's queued work, visibility afterwait, device pinning, threadpool forms, single evaluation of the backend expression, error propagation, many concurrent tasks. Passes on the CPU backend.record_event/wait_eventfallbacks.Privateerror caused by my local GPUCompiler dev checkout lackingalloca(unrelated to this change).🤖 Generated with Claude Code
https://claude.ai/code/session_01YQWT6DHjjRoh1YaAsFEUN5