From c1fce3bbc60bbfd364a1dbfdfc20ea2eeb7bf5e0 Mon Sep 17 00:00:00 2001 From: Valentin Churavy Date: Mon, 27 Jul 2026 11:54:51 +0200 Subject: [PATCH] [pocl] Drop dead deferred-codegen bodies before SPIR-V translation Deferred-codegen entrypoints -- notably the wrappers Enzyme generates -- are held externally live across GPUCompiler's `InternalizePass` so that linking can resolve them. Once linked and `alwaysinline`d they are dead, but external linkage keeps `GlobalDCEPass` from dropping them, so the SPIR-V backend still has to translate a function it cannot express: they take first-class aggregates containing `addrspace(1)` pointers, and extracting one back out miscompiles. The backend types the struct member as pointer-to-uchar but the extract result as pointer-to-double, so `spirv-val` rejects the module: error: Result type (OpTypePointer) does not match the type that results from indexing into the composite (OpTypePointer). %105 = OpCompositeExtract %_ptr_CrossWorkgroup_double %91 0 Reproduces without Enzyme in eight lines of IR, so the real fix belongs upstream. Until then, empty the bodies of unreferenced non-kernel definitions in `finish_ir!` so only declarations reach the backend. We empty rather than erase so the `finish_ir!` loop over the remaining deferred jobs can still look them up by name. With this plus EnzymeAD/Enzyme.jl#3309, forward-mode Enzyme over a POCL kernel compiles, runs and produces correct derivatives. Co-Authored-By: Claude Opus 5 --- src/pocl/compiler/compilation.jl | 37 ++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/pocl/compiler/compilation.jl b/src/pocl/compiler/compilation.jl index 1fbc06dfe..c81815cbe 100644 --- a/src/pocl/compiler/compilation.jl +++ b/src/pocl/compiler/compilation.jl @@ -136,9 +136,46 @@ function GPUCompiler.finish_linked_module!(@nospecialize(job::OpenCLCompilerJob) ) GPUCompiler.add_input_arguments!(job, mod, f, kernel_intrinsics) end + return end +function GPUCompiler.finish_ir!( + @nospecialize(job::OpenCLCompilerJob), + mod::LLVM.Module, entry::LLVM.Function + ) + entry = invoke( + GPUCompiler.finish_ir!, + Tuple{CompilerJob{SPIRVCompilerTarget}, LLVM.Module, LLVM.Function}, + job, mod, entry + ) + + # Deferred-codegen entrypoints -- notably the wrappers Enzyme generates -- are + # held externally live across `InternalizePass` so that linking can resolve + # them. Once linked and `alwaysinline`d they are dead, but external linkage + # keeps `GlobalDCEPass` from dropping them, so the SPIR-V backend still has to + # translate a function it cannot express: they take first-class aggregates + # containing `addrspace(1)` pointers, and extracting one back out miscompiles + # (the backend types the struct member as a pointer-to-uchar but the extract + # result as pointer-to-double, which fails `spirv-val`). + # + # Drop the bodies of unreferenced non-kernel definitions so only declarations + # remain. We empty rather than erase so the `finish_ir!` loop over the + # remaining deferred jobs can still look them up by name. + if job.config.kernel + for f in functions(mod) + f == entry && continue + isdeclaration(f) && continue + LLVM.isintrinsic(f) && continue + LLVM.callconv(f) == LLVM.API.LLVMSPIRKERNELCallConv && continue + isempty(uses(f)) || continue + empty!(f) + end + end + + return entry +end + ## compiler implementation (configure, compile, and link)