Conversation
Benchmark ResultsShow table
Benchmark PlotsA plot of the benchmark results have been uploaded as an artifact to the workflow run for this PR. |
| Backends are free to give each Julia task its own queue (stream), so that kernels | ||
| launched from different tasks can execute concurrently. The price is that work queued | ||
| from two tasks is not ordered with respect to each other, and that a task waiting on | ||
| another task with `wait` learns nothing about the state of that task's queue. |
There was a problem hiding this comment.
| Backends are free to give each Julia task its own queue (stream), so that kernels | |
| launched from different tasks can execute concurrently. The price is that work queued | |
| from two tasks is not ordered with respect to each other, and that a task waiting on | |
| another task with `wait` learns nothing about the state of that task's queue. | |
| Backends should give each Julia task its own queue/stream, so that kernels | |
| launched from different tasks can execute concurrently. This implies that work queued | |
| from two tasks is not ordered with respect to each other. |
| - The new task first selects the spawning task's device with [`device!`](@ref KernelAbstractions.device!), | ||
| then calls [`wait_event`](@ref KernelAbstractions.wait_event) with the recorded handle. | ||
| A backend that overrides `record_event` **must** implement `wait_event` for its event | ||
| type, typically by making the current task's queue wait on the event. |
There was a problem hiding this comment.
Hm... do we need the device!?
There was a problem hiding this comment.
I think wrong device being selected/synchronized is why the OpenCL CUDA tests fail
There was a problem hiding this comment.
Not sure how to best handle this since we never really dealt with #631 (comment)
There was a problem hiding this comment.
The lazy solution is to add in the docs that the device interface should be supported to use @spawn with a non-default device and disable those tests on the OpenCL CUDA runs
| A backend with a single, global queue needs no changes: the defaults are exactly the | ||
| "synchronize before, synchronize after" discipline users would otherwise write by hand. |
There was a problem hiding this comment.
| A backend with a single, global queue needs no changes: the defaults are exactly the | |
| "synchronize before, synchronize after" discipline users would otherwise write by hand. |
| Some backends give each Julia task its own queue, so kernels launched from two tasks are not | ||
| ordered with respect to each other, and `wait(task)` on its own says nothing about whether | ||
| the kernels that task launched have finished. Use [`KernelAbstractions.@spawn`](@ref) instead | ||
| of `Threads.@spawn` to launch kernels from a task. It orders the new task's work after the | ||
| work the spawning task has already queued, runs it on the same device, and synchronizes the | ||
| backend before the task finishes, so that `wait(task)` and `fetch(task)` guarantee its | ||
| results are ready: |
|
@christiangnrd we should have this for KI 0.2 as well |
`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)
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. |
| !!! note "Cooperative synchronization" | ||
| Backend implementations **should** make `synchronize` cooperative rather than blocking. | ||
| That is, instead of blocking inside a driver call, it should poll or wait on a | ||
| completion signal while calling `yield` so that other Julia tasks can run in the | ||
| meantime. A blocking implementation stalls every task scheduled on the same thread, | ||
| which defeats overlapping kernels with host work or communication, and makes | ||
| [`KernelAbstractions.@spawn`](@ref)'s trailing `synchronize` serialize otherwise | ||
| independent tasks. |
There was a problem hiding this comment.
Doesn't the quickstart manual say they have to be cooperative?
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 |
Julia 1.10 ignores `[sources]`, so it resolves KernelInterface from the registry rather than from lib/KernelInterface. The "Dev KernelInterface" step compensates, but it was gated on `runner.os != 'Windows'`, copied from the neighboring runtest step; only the test run needs the de-escalated shell, not `Pkg.develop`. As a result the 1.10 Windows job picked up the registered KernelInterface v0.1.0, which lacks `record_event`, and KernelAbstractions failed to precompile with `UndefVarError: record_event not defined`. Run the step everywhere, under bash so the quoting works on Windows. Assisted-by: Claude Code (Opus 5)
`[sources]` is only supported from Julia 1.11 on. On 1.10 the entry pointing KernelInterface at lib/KernelInterface is silently ignored and Pkg resolves it from the registry, so the dev step is what keeps 1.10 testing this repo's copy. Record that where the step is, and cross-reference it from the OpenCL job, which dev's KernelInterface for the same reason. Assisted-by: Claude Code (Opus 5)
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