PTX: avoid atomics on thread-private memory - #963
Draft
timesselens wants to merge 1 commit into
Draft
timesselens wants to merge 1 commit into
timesselens wants to merge 1 commit into
Conversation
PTX has no atomic instructions for the local state space. An `atom` on a generic address that points to a stack slot faults at run time on a GTX 1080 with CUDA_ERROR_INVALID_ADDRESS_SPACE, a sticky error that leaves the context unusable. LLVM IR allows atomics on any memory, and the NVPTX back-end does not legalize them. Enzyme generates such atomics: the adjoint of a device function that is not inlined accumulates into the shadow of a by-reference argument with `atomicrmw fadd`, and that shadow can be an `alloca` of the kernel. A reverse-mode kernel calling `@noinline f(s::Pair)` crashed this way, and so did WaterLily.jl's reverse-mode kernels, whose item closures are `Active`. Add `ptx_local_atomics!`, run at the end of `optimize_module!` (after inlining has exposed what it can). On a pointer known to be local (an `alloca`, or addrspace 5) it rewrites `atomicrmw` and `cmpxchg` into a plain load, operation and store with the atomic's alignment and volatility, since only this thread can access that memory. On a generic pointer of unknown origin it calls an `alwaysinline` helper that branches on `llvm.nvvm.isspacep.local`, taking the plain path for local memory and the original atomic otherwise (ordering, scope, alignment, volatility and weakness kept); that atomic is marked so the pass does not wrap it again. Atomics on global, shared and constant memory are left alone, and operations the C API does not name (uinc_wrap, udec_wrap) stay atomic. The tests in test/ptx.jl work on IR and need no GPU: known-local, generic, global, cast-from-global and shared pointers; all integer operations, fmax, fadd and cmpxchg; idempotence; the plain form checked against the atomic on the host through llvmcall for every operation; and `PTX.code_llvm` of two kernels, to check that the pass runs in `optimize_module!`. The IR is written in typed-pointer syntax, so it also parses on Julia 1.10. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #963 +/- ##
==========================================
+ Coverage 86.65% 86.93% +0.27%
==========================================
Files 29 29
Lines 5786 5924 +138
==========================================
+ Hits 5014 5150 +136
- Misses 772 774 +2 ☔ 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.
PTX has no atomic instructions for thread-private (local) memory. An
atomicrmworcmpxchgon a generic pointer that points to a stack slot becomes a genericatom, which on Pascal GPUs faults withCUDA_ERROR_INVALID_ADDRESS_SPACE(717), a sticky error that leaves the context unusable. Enzyme's reverse mode produces such atomics (EnzymeAD/Enzyme.jl#428). This PR proposes turning atomics on memory known to be local into a plain load, operation and store at the end of the PTX pipeline, and guarding atomics on generic pointers of unknown origin with a run-timeisspacep.localcheck.Before / after
On a GTX 1080 (Pascal), with CUDA.jl 6.4. That needs GPUCompiler 2, so Enzyme.jl comes from EnzymeAD/Enzyme.jl#3512, with the relocation-slot guards suggested there:
(GTX 1080, CUDA.jl 6.4, Julia 1.12.5; with
pginlined, the gradient was already as expected.) In the IR, an atomic on a generic pointer becomes:What seems to happen
atomicrmw fadd, since the callee can't know whether the pointer is private to the thread. Here the caller's shadow is analloca, so the pointer is generic and points to local memory.atom: "atom with scalar type may be used only with .global and .shared spaces and with generic addressing, where the address points to .global or .shared space." Pascal faults on such an address. Newer GPUs may tolerate it (only Pascal was tested here), but it is outside the ISA either way.NVPTXAtomicLower(D98650) demotes anatomicrmwon local memory only when the pointer is already in the local address space afterNVPTXLowerAllocaandInferAddressSpaces, which are both function-local. A pointer that arrives as an argument stays generic, andcmpxchgisn't handled.Proposed change
ptx_local_atomics!(src/ptx.jl) is a module pass at the end ofoptimize_module!, after inlining has exposed what it can, followed byAlwaysInlinerPass. For everyatomicrmwandcmpxchg:alloca, or in addrspace 5): a plain load, operation and store, keeping the atomic's alignment and volatility;alwaysinlinehelper that branches onllvm.nvvm.isspacep.local. It takes the plain path for local memory and the original atomic otherwise, keeping its ordering, scope, alignment, volatility and weakness. That atomic is marked, so a second run leaves it alone;CuDeviceArrayand shared-memory atomics carry their address space, so they are not touched.The pass handles every operation that LLVM's C API names;
uinc_wrapandudec_wrapstay atomic.Tests
In
test/ptx.jl, inside the "IR" set, so no GPU is needed. The IR is written in typed-pointer syntax, which parses under both pointer regimes.cmpxchgon a stack slot: no atomic left;llvmcall, for every operation and six input pairs.PTX.code_llvmof a kernel with an atomic on its own stack slot has noatomicrmwleft, and one through a pointer argument gets theisspacep.localcheck.Run with
julia --project -e 'using Pkg; Pkg.test(test_args=["ptx"])'. With this test file onmain, the first set gives 31 failures and 3 errors, and both pipeline tests fail.Open questions
alloca). This PR is the PTX side of that; PTX also needs the run-time check for generic pointers, because anatomon local memory faults there. Once Metal: lower LLVM atomics to AIR atomic intrinsics #942 lands, the demotion could share its helper, and I'd be happy to rebase onto it or fold this into your rework.Related
allocapassed to a function that isn't inlined;InferAddressSpacesis function-local) and kept the issue open for a later fix. They judged a fix inside Enzyme (knowing across calls that a pointer is thread-local) a large change; this PR lowers the atomics in the back-end instead, asNVPTXAtomicLowerdoes for the known-local case.NVPTXAtomicLower): the known-local case in the back-end.@cudalaunches, so raw@cudareverse mode now reaches this path directly.Verification
Pkg.test(test_args=["ptx"])on macOS aarch64 with Julia 1.10.12, 1.11.9, 1.12.5 and 1.13.0: the two new sets pass (114/114 and 2/2). "Julia value global names" errors on every version, with or without this PR, because the NVPTX back-end can't be loaded on macOS.[Pair2(1.0, 1.0)]. WaterLily.jl's reverse-mode test set, whose kernels takeActiveclosures, crashed at its first GPU test before; with this change (and fixes to Enzyme's CUDA math functions) it got past it, and the set as it was then passed on CPU + CUDA. Linux x86_64, on this head:ptx188/188 (withptx/precompile). The Before/after block, pasted as is on this head:[Pair2(1.0, 1.0)]; withmain, the error shown, followed at exit bycuModuleUnloadfinalizer errors from the sticky context.