ITADN

Segfault in `lower-jit` (`rewriteKernelCallABI`) lowering a KernelAbstractions launch to `gpu.LaunchFuncOp`

#3026Openxkykai 创建于 2026-07-06
X
xkykaicommented
This is a reduced version of https://github.com/NumericalEarth/NumericalEarth.jl/issues/407 and a temporary solution in Oceananigans is https://github.com/NumericalEarth/NumericalEarth.jl/pull/408. Compiling a KernelAbstractions `launch!` on the **CUDA** backend segfaults inside the `lower-jit` pass, in `rewriteKernelCallABI`, while lowering an `enzymexla.jit_call` to a `gpu.LaunchFuncOp` — but only when the launched kernel has **no writeable device-array output** (the resulting `gpu.LaunchFuncOp` produces no results). The same program compiles fine with `raise=true` (no `jit_call` is produced) and on the CPU backend. ## MWE (pure Oceananigans, ~10 lines) ```julia using Oceananigans, Reactant, CUDA using Oceananigans.Architectures: ReactantState, architecture using Oceananigans.Utils: launch! using KernelAbstractions: @kernel, @index Reactant.set_default_backend("gpu") @kernel function _empty!(grid) # receives the grid, writes nothing i, j = @index(Global, NTuple) end grid = RectilinearGrid(ReactantState(); size = (), topology = (Flat, Flat, Flat)) Reactant.@jit launch!(architecture(grid), grid, (1, 1), _empty!, grid) # ^ segfaults (signal 11) inside lower-jit / rewriteKernelCallABI ``` Run in a **fresh** session, un-raised (the crash is order/state dependent — see Notes). An Oceananigans coupled-model regridder kernel whose output fields happen to be `nothing` (so it writes nothing) is the first launch compiled when building the model, and it crashes here. ## Where it crashes `lower-jit` walks each `enzymexla.jit_call` and, lowering one to a `gpu.LaunchFuncOp`, segfaults. Backtrace (crash frame first, down to `@jit`): ``` signal 11 (Segmentation fault) [libReactantExtra.so] # crash: walk callback lowering a JITCallOp to gpu::LaunchFuncOp inside rewriteKernelCallABI llvm::function_ref<...>::callback_fn<mlir::detail::walk<..., rewriteKernelCallABI(...)::$_2, mlir::gpu::LaunchFuncOp, void>>(long, mlir::Operation*) mlir::detail::walk<mlir::ForwardIterator>(...) rewriteKernelCallABI(mlir::ModuleOp&, mlir::Location, ..., mlir::enzymexla::JITCallOp, ...) CompileCall(..., mlir::enzymexla::JITCallOp, ...) (anonymous namespace)::LowerJITPass::runOnOperation()::{lambda(mlir::enzymexla::JITCallOp)#1} mlir::detail::walk<mlir::ForwardIterator>(...) (anonymous namespace)::LowerJITPass::runOnOperation() mlir::PassManager::run(mlir::Operation*) mlirPassManagerRunOnOp run_pass_pipeline! Reactant/src/compiler/OptimizationPasses.jl:208 (pipeline key = "all") compile_mlir! Reactant/src/compiler/Compiler.jl:238 @jit ``` The pass, as invoked on an sm_75 (T4): ``` lower-jit{backend=cuda compileLaunch=true cubinChip=sm_75 cubinFeatures=+ptx90 cubinFormat=bin cubinTriple=nvptx64-nvidia-cuda indexBitWidth=32 jit=true run_init=true toolkitPath=… …} ``` ## What is / isn't required to trigger it (bisected) Each row is a separately verified run (fresh GPU session, un-raised, first compile). The grid is passed as a kernel argument in every row, so it is not the discriminator: | Kernel body | Launch worksize | Writes a device-array output? | Result | |-------------|-----------------|-------------------------------|--------| | `a[i,j,1] = b[i,j,1]` (two `Field`s, no grid arg) | dynamic `(1,1)` | yes | compiles, runs | | `out[i,j,1] = size(grid, 3)` (`Field` + grid) | static `KernelParameters(1:1,1:1)` | yes | compiles, runs | | empty body (grid only) | static `KernelParameters(1:1,1:1)` | **no** | **segfault** | | empty body (grid only) | dynamic `(1,1)` | **no** | **segfault** | So the discriminator is **whether the kernel writes a real output field**: a launch whose `gpu.LaunchFuncOp` has *no results* crashes `rewriteKernelCallABI`; a launch that writes an output compiles fine. The launch worksize (static `KernelParameters` vs dynamic tuple) does not matter — both output-less variants crash. A hand-written KA kernel with an `OffsetArray`-over-device-array argument (no Oceananigans) fails *earlier*, in GPU codegen (dynamic `getindex`), so it can't reach `lower-jit`; the Oceananigans `Field` / `launch!` machinery is what gets an output-less kernel to the crashing pass. ## Notes toward the cause - **Order/state dependent.** Reproduces only when the **un-raised** compile is the *first* touch of that function in the session. If a `@jit raise=true` compile of the *same* call runs first, the subsequent un-raised `lower-jit` lowering then **succeeds** (emits a `gpu.binary`, no crash). Strong evidence the trigger is in-session state, not the IR text alone. - **A textual round-trip does not reproduce it.** Dumping the module right before `lower-jit` (`Reactant.@code_hlo optimize=:before_jit raise=false …`) and re-running only `lower-jit` on the parsed textual IR does not segfault — corroborating the in-session-state dependence. - **`raise=true` is the current workaround** — it avoids emitting the `jit_call` entirely, so nothing reaches `rewriteKernelCallABI`. - **`DEBUG_KERNEL[] = true` does not make it catchable.** That injects `debug=true cuResultHandlerPtr=<ptr>` into `lower-jit`, but that handler is for *runtime* CUDA results; this fault is at *compile time* inside the rewrite, before any kernel runs, so a Julia `try/catch` never sees it. ## Environment Reactant 0.2.268, Enzyme 0.13.173, Oceananigans 0.110.6, CUDA.jl 6.2.0, Julia 1.12.6, NVIDIA Tesla T4 (sm_75), CUDA runtime 13.1, cuDNN 9.14, XLA/PJRT CUDA. @dkytezab @Pangoraw
1 条评论