Repository navigation
Report device exceptions to the host - #731
Merged
Merged
Conversation
Kernels were always compiled at Julia's own debug level, which decides how much GPUCompiler reports about exceptions. The keyword makes it possible to get more detail for a single kernel without restarting Julia with -g2.
Contributor
|
Documentation preview: https://juliagpu.github.io/oneAPI.jl/previews/PR731/ |
A work-item that threw only printed a message, if anything, and exited, so the host carried on with incomplete results. Kernels now report exceptions to their stream, and synchronizing it, or waiting for memory it owns, throws a `KernelException` that carries the exception's name and reason, and at -g2 the throwing work-item and its backtrace. Each stream owns a report in host memory, checked when synchronizing without any added submission or wait, and a claim word in device memory. The first work-item to throw claims the report by storing its id in the claim word, which makes it the report's only writer, as devices need not support atomics on host memory. Reports are carved out of slabs shared by a device's streams, as every allocation adds to the cost of every launch. Kernels keep no per-work-item state, which would take private memory and slow down every kernel. Strings are reported by the address of the constant holding them, and read by the host once an exception was reported, only for addresses within an allocation, such as a module's constants, and up to its end. Copying them on the device puts a loop in every reporting function, which GPUCompiler calls for every frame of every throw site, and made IGC compile bounds-checked kernels up to 14 times slower. Reporting is kept out of line, which keeps those kernels fast, except at -g2, where IGC compiles all the frames faster inlined. `@gputhrow` now records its name and reason in the report instead of printing them, which also removes the printf from bounds-checked kernels.
`synchronize()` reported the kernel exceptions of tasks that finished without synchronizing only if their stream was registered for adoption, which happened for batched launches only. Without batching, or when a task's last launch was submitted directly, the exception instead surfaced at whatever next waited for the task's memory, e.g. an unrelated launch on an explicit queue. Register every stream that launches a kernel, and only forget it once it is idle with nothing left to report, through a helper shared by `synchronize`, memory waiters and adoption. Adoption checks again after waiting that the stream is still orphaned, so that it doesn't take the exception of a running task that launched on it in the meantime. Tasks are referenced weakly, so that finished ones can be collected.
Drivers without `zeCommandListImmediateAppendCommandListsExp`, like Aurora's, submit every launch directly, which takes different paths through synchronization and exception reporting. Run the execution and array tests that way on branch builds, to keep the load on the shared runner low.
maleadt
force-pushed
the
tb/kernel-exceptions
branch
from
October 23, 2026 15:39
8163d15 to
24ec33b
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #731 +/- ##
==========================================
- Coverage 84.67% 84.54% -0.14%
==========================================
Files 52 52
Lines 4543 4554 +11
==========================================
+ Hits 3847 3850 +3
- Misses 696 704 +8 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
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.
Kernels that throw now raise a
KernelException. It is thrown when the kernel's stream is synchronized, or when the host waits for memory the stream owns. Before, such kernels printed a message (for@gputhrowquirks) or failed silently:At
-g2, the exception also names the work-item and work-group that threw, followed by a device backtrace. At-g0, it only reports that an exception was thrown. The newdebug_levelkeyword to@oneapisets the level for a single kernel.The design follows OpenCL.jl's exception reporting (JuliaGPU/OpenCL.jl#478), but is simpler because oneAPI.jl already has a stream per task:
oneStreamhas a report in host USM and a claim word in device USM.zeMemGetAddressRangeplaces in an allocation, such as a module's constants, and only up to its end, so a corrupted report cannot fault the device. CUDA.jl does the same in Add hostcalls for device-to-host services CUDA.jl#3243. An earlier version copied the strings on the device, and GPUCompiler replicates those copies at every throw site and, at -g2, every frame. IGC then generated up to 20 times more code: building a GPUArraysgetindexkernel took 0.17 s instead of 0.05 s at -g1, and 0.55 s at -g2 (10.5 s with a debug build of IGC, which timed out CI). It now takes 0.07 and 0.13 s (2.7 s with debug IGC).@gputhrow's name and reason in private memory. That added 192 bytes of private memory to every bounds-checked kernel and slowed a vector add by 11%.@gputhrow. It now records its name and reason (e.g."BoundsError","Out-of-bounds array access") in the report. This replaces the device printf, which makes bounds-checked kernels faster than on main. On an A770, a 2^24-element bounds-checked vector add goes from 597 to 498 µs at -g1, the same as at -g0. At -g2, reporting is inlined, which IGC compiles faster, and the vector add is as fast as on main. Neither level uses private memory or spills registers to memory.Exceptions in kernels launched on an explicitly created
ZeCommandQueueare not reported, as there is no stream to hold the report.This supersedes #621. Thanks to @michel2323 for the earlier work there.
Closes #65