Device runtime: host-visible exceptions, a per-work-item malloc, and a compile-smoke grid - #621
Draft
michel2323 wants to merge 2 commits into
Draft
Device runtime: host-visible exceptions, a per-work-item malloc, and a compile-smoke grid#621michel2323 wants to merge 2 commits into
michel2323 wants to merge 2 commits into
Conversation
…ompile-smoke grid GPUCompiler 2.2.2 (JuliaGPU/GPUCompiler.jl#899, `compilesig_invokes=false`) stopped eliding the `DomainError` allocation inside `Base.Math.exponent`'s local `throw1` closure. `sqrt(::Complex)` reaches it through `ssqs`, and oneAPI.jl provides no device `malloc`, so the gpuarrays/statistics test died with InvalidIRError: unsupported call to an unknown function (call to gpu_malloc) The allocation is the exception object. Base constructs `InexactError` and `DomainError` directly in several places — `exponent`'s closures, `ssqs`'s `Int(::Float)` conversion, `_cpow`, `Int32(::Float32)`, `round(Int, ::Float64)` — none of which goes through a throw helper that `src/device/quirks.jl` could override individually. An exception object only exists to be thrown, so overlay the constructors themselves with `@print_and_throw`; that covers every site at once. The overlays are `@inline` and specialized on purpose: a `@noinline`/`@nospecialize` version has to box its `Float32` argument, which is the same allocation by another route (and the reason `sqrt(::ComplexF32)` never compiled). `test/device/codegen.jl` compiles ~200 (function, eltype) cells through the validating pipeline (`compile_to_obj`; the `code_*` reflection entry points skip validation) without launching anything, so the next "f(::T) stopped compiling" names its cell instead of failing deep inside a GPUArrays test. The cells it still marks broken are SPIR-V code-generator limitations, not allocations: the Khronos translator rejects `_cpow`'s `i63` and `llvm.smul.with.overflow`, the LLVM back-end cannot select `G_SADDO` and mis-legalizes `_cpow`. Verified on an Aurora debug node with GPUCompiler 2.2.2 (LTS stack): device/codegen, execution and gpuarrays/statistics pass (322/6 broken/0).
…p for malloc
Device exceptions used to be silent. `signal_exception` was a no-op and the
SPIR-V target lowers the trap behind it to a plain return, so a kernel that
threw — a bounds error, a domain error, anything — just stopped that
work-item and the host never heard about it. And anything that allocated
a Julia object on the device did not compile at all: GPUCompiler lowers
heap allocations to a back-end `malloc` that oneAPI.jl never provided, so
every throw path whose exception object the optimizer failed to delete
ended in
InvalidIRError: unsupported call to an unknown function (call to gpu_malloc)
which is what GPUCompiler 2.2.2 exposed for `sqrt(::Complex)`.
Both are now served through a kernel state, the hidden first argument
GPUCompiler threads to every device function:
- `exception_flag` points at a 16-byte host USM buffer per (context,
device). `signal_exception` and `report_oom` set its words; the host
reads and atomically clears them whenever it synchronizes a stream —
`synchronize()`, `@sync`, copying back to the host — and raises a
`KernelException` (with a note when the heap ran out). The check is
deliberately absent from `synchronize_all_streams`, which runs from
finalizers. The state is prepended in the generated `call`, not in
`onecall`, which stays a raw launch primitive for foreign SPIR-V.
- `heap` points at a bump arena in the work-item's private memory, and
`malloc` bump-allocates from it without atomics. A global-memory heap
was tried first and does not work: Julia's boxed objects live in
address space 0 once GPUCompiler strips its address spaces, which is
private memory to SPIR-V and to Intel's compiler, and stores through
such a pointer into a global buffer are silently lost (verified on a
Max 1550: allocations happened, the values never arrived). SPIR-V has
no per-invocation module-scope storage either (the translator rejects
address-space-0 globals), so `add_private_heap!` allocas the arena in
the kernel entry after the kernel-state passes and patches its pointer
into the state the entry hands on. Only kernels whose code reaches
`gpu_malloc` carry the arena; the heap is fresh on every launch and
nothing is freed. Exhausting the `PRIVATE_HEAP_SIZE` (1 KiB) is
reported as an out-of-memory `KernelException`, never a silent
failure.
The constructor overlays in quirks.jl stay: they print a reason, which the
heap path does not, and keep allocations off the hot error paths.
Verified on an Aurora debug node (LTS stack, GPUCompiler 2.2.2): boxed
values round-trip through the heap, 4096 concurrent work-items allocate
correctly, exhaustion is reported, and the full test suite passes
(12830 pass, 61 broken; the one failure, broadcast Float16, is the known
host AVX512-FP16 issue and passes with the CI's JIT target).
michel2323
marked this pull request as draft
August 21, 2026 23:02
Member
Author
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.
Fixes the ALCF CI failure on
mainafter #610 and closes the class of bugs behind it.Why CI went red
GPUCompiler 2.2.2 (JuliaGPU/GPUCompiler.jl#899,
compilesig_invokes=false) stopped eliding theDomainErrorallocation insideBase.Math.exponent's localthrow1closure.sqrt(::Complex)reaches it throughssqs, and oneAPI.jl provided no devicemalloc, so thegpuarrays/statisticstest died withEvery throw path that
src/device/quirks.jldoes not cover was a latent instance of this; which ones surfaced depended on what the optimizer happened to delete.Commit 1 — constructor overlays + compile-smoke grid
src/device/quirks.jloverlays theInexactError/DomainErrorconstructors with@print_and_throw. Base builds these directly in several places (exponent,ssqs,_cpow,Int32(::Float32),round(Int, ::Float64)) without a throw helper that could be overridden one by one. The overlays are@inlineand specialized on purpose: a@noinline/@nospecializeversion boxes itsFloat32argument, which is the same allocation by another route (and whysqrt(::ComplexF32)never compiled — the "doesn't work with ComplexF32 in oneAPI" that GPUArrays' statistics test skips around).test/device/codegen.jlcompiles ~200 (function, eltype) cells through the validating pipeline (compile_to_obj; thecode_*reflection entry points skip validation) without launching anything, so the next "f(::T) stopped compiling" names its cell. The cells it marks broken are SPIR-V code-generator limitations: the Khronos translator rejects_cpow'si63andllvm.smul.with.overflow, the LLVM back-end cannot selectG_SADDOand mis-legalizes_cpow.Commit 2 — a device runtime: host-visible exceptions and
mallocDevice exceptions used to be silent (
signal_exceptionwas a no-op and the SPIR-V target lowers the trap behind it to a return), and anything allocating a Julia object on the device did not compile. Both are now served through a kernel state (GPUCompiler.kernel_state_type), the hidden first argument threaded to every device function:signal_exception/report_oomset words in a 16-byte host-USM flag per (context, device); the host reads and atomically clears them whenever it synchronizes a stream (synchronize(),@sync, copies to the host) and raises aKernelException. The check is deliberately absent fromsynchronize_all_streams, which runs from finalizers. The state is prepended in the generatedcall, not inonecall, which stays a raw launch primitive for foreign SPIR-V.malloc: a bump allocator over a per-work-item arena in private memory. A global-memory heap was tried first and does not work on this stack: Julia's boxed objects live in address space 0 once GPUCompiler strips its address spaces, which SPIR-V and IGC treat as private memory, and stores through such a pointer into a global buffer are silently lost (verified on a Max 1550 — the allocations happened, the values never arrived). The translator also rejects address-space-0 module globals, soadd_private_heap!allocas the arena in the kernel entry after the kernel-state passes and patches its pointer into the state. Only kernels whose code reachesgpu_malloccarry the arena; it is fresh on every launch, nothing is freed, and exhaustingPRIVATE_HEAP_SIZE(1 KiB) is reported as an out-of-memoryKernelExceptionrather than failing silently.The constructor overlays stay: they print a reason, which the heap path does not, and keep allocations off the hot error paths.
Verification
Aurora debug node, LTS stack, GPUCompiler 2.2.2:
@sync/Array()surface the exception;broadcast Float16fails only without the CI's-C native,-avx512fp16, the known host AVX512-FP16 issue).Login node, both SPIR-V back-ends, GPUCompiler 2.2.1 and 2.2.2: the compile grid passes with the same broken set. Not run on the rolling stack.