Skip to content

Add alloca intrinsic for per-workitem stack scratch#859

Merged
vchuravy merged 9 commits into
mainfrom
vc/alloca_intrinsic
Jul 9, 2026
Merged

Add alloca intrinsic for per-workitem stack scratch#859
vchuravy merged 9 commits into
mainfrom
vc/alloca_intrinsic

Conversation

@vchuravy

Copy link
Copy Markdown
Member

Summary

Adds GPUCompiler.alloca(::Type{T}, ::Val{N})::Ptr{T}, a primitive that hands device code a fixed-size, per-workitem stack scratch buffer for N elements of T. The intended use is to replace abstractions like KernelAbstractions' @private MArray-backed scratchpad with a direct stack allocation (see companion KA PR).

Why an intrinsic instead of llvmcall + alloca

Emitting the alloca directly through llvmcall is unsound/ineffective:

  1. Promotion is lost. A Ptr round-trips through ptrtoint/inttoptr at the llvmcall boundary, so SROA/mem2reg can never promote the slot to registers — which is the whole point of small @private scratch.
  2. Address space isn't known at the front end. On NVPTX/AMDGPU an alloca must live in the datalayout's alloca address space (e.g. addrspace 5) and be addrspacecast to generic; this can't be written portably across the PTX/GCN/Metal/SPIR-V back-ends from Julia.
  3. Lifetime. Per the LangRef, an alloca's storage is freed when its function returns; in an llvmcall wrapper that's the wrapper, and correctness relies entirely on the inliner relocating a static entry-block alloca.

How it works

  • The front end emits a declared-only julia.gpu.alloca.<bytes>.<align> intrinsic (modeled on julia.gpu.debug_level).
  • lower_alloca!, run from irgen before the optimizer, materializes it as a real entry-block alloca in the datalayout's alloca address space (so SROA/mem2reg can promote it), cast back to generic for the returned Ptr. This produces the same IR shape Julia already emits for mutable stack allocations.
  • T must be isbits; a zero-byte request returns a null pointer.

Testing

Adds a native-target testset asserting an entry-block alloca [N x i8], align <align>, full promotion when optimized, no surviving intrinsic, and the zero-byte → null path.

Verified end-to-end (including execution) through the POCL/SPIR-V back-end via the companion KernelAbstractions change: @private Float32 (4,) lowers to alloca [16 x i8], align 4 in addrspace 0 (OpenCL Function/private) and runs correctly.

🤖 Generated with Claude Code

@maleadt

maleadt commented Jun 22, 2026

Copy link
Copy Markdown
Member

Conceptually fine, but I didn't realize we needed this.

  • Promotion is lost. A Ptr round-trips through ptrtoint/inttoptr at the llvmcall boundary, so SROA/mem2reg can never promote the slot to registers — which is the whole point of small @private scratch.

Hmm, isn't this optimized away?

  • Address space isn't known at the front end. On NVPTX/AMDGPU an alloca must live in the datalayout's alloca address space (e.g. addrspace 5) and be addrspacecast to generic; this can't be written portably across the PTX/GCN/Metal/SPIR-V back-ends from Julia.

Yeah, I guess. How do we make sure Julia's alloca's are in the correct AS? Couldn't/Shouldn't we patch those up afterwards?

  • Lifetime. Per the LangRef, an alloca's storage is freed when its function returns; in an llvmcall wrapper that's the wrapper, and correctness relies entirely on the inliner relocating a static entry-block alloca.

llvmcall wrappers are alwaysinline; doesn't that suffice?

@vchuravy

Copy link
Copy Markdown
Member Author

The crux is that llvmcall + alloca is UB and I have seen LLVM delete the alloca and replace it with poison. I can dig up the old PR I had for that.

My overarching goal is to remove the StaticArrays dependency that Kernel abstraction currently forces and which you have complained about before since creates behavior where we have to hope for escape analysis to work.

The rest is Claude obsessing over details. We have fixed alloca Julia to be emitted correctly

Comment thread src/irgen.jl
@codecov

codecov Bot commented Jun 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.11321% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 78.71%. Comparing base (9f2c1bd) to head (9862035).
⚠️ Report is 9 commits behind head on main.

Files with missing lines Patch % Lines
src/irgen.jl 98.11% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #859      +/-   ##
==========================================
- Coverage   79.77%   78.71%   -1.06%     
==========================================
  Files          25       25              
  Lines        4796     4971     +175     
==========================================
+ Hits         3826     3913      +87     
- Misses        970     1058      +88     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@vchuravy vchuravy force-pushed the vc/alloca_intrinsic branch from 116c36b to 09e210a Compare June 23, 2026 07:22
vchuravy and others added 2 commits June 30, 2026 13:15
Introduce `GPUCompiler.alloca(::Type{T}, ::Val{N})::Ptr{T}`, which hands device
code a fixed-size, per-workitem stack scratch buffer for `N` elements of `T`.

This is meant to replace abstractions like KernelAbstractions' `@private`
`MArray`-backed scratchpad with a direct stack allocation. Emitting the `alloca`
through `llvmcall` directly is unsound/ineffective: the `Ptr` round-trip through
`ptrtoint`/`inttoptr` blocks SROA/mem2reg promotion, the target stack address
space (e.g. addrspace 5 on NVPTX/AMDGPU) isn't known at the front end, and the
LangRef lifetime of the `alloca` is tied to the inlined `llvmcall` wrapper.

Instead, the front end emits a `julia.gpu.alloca.<bytes>.<align>` intrinsic that
`lower_alloca!` (run from `irgen`, before the optimizer) materializes as a real
entry-block `alloca` in the datalayout's alloca address space, cast back to
generic. Running before optimization lets the slot be promoted just like the
mutable stack allocations Julia already emits. `T` must be `isbits`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vchuravy vchuravy force-pushed the vc/alloca_intrinsic branch from 09e210a to e13a8c5 Compare June 30, 2026 11:15
vchuravy added 2 commits June 30, 2026 13:21
Test that  lowers correctly for all GPU targets
1. On Julia 1.10/1.11, LLVM runs in typed-pointers mode. PointerType() creates an opaque pointer in LLVM 15, causing assembly parsing errors on llvmcall since opaque pointers are disabled by default. Fix this by using PointerType(Int8Type()) when supports_typed_pointers() is true.
2. In lower_alloca!, if the pointer types or address spaces differ (which is common under typed pointers where the slot is [32 x i8]* but the call returns i8*), dynamically handle casting using bitcast! or addrspacecast! as appropriate.
3. On Julia 1.10/1.11, under optimize=false, the compiler compiles the llvmcall into a non-inlined helper function. Fix test checking by passing dump_module=true to code_llvm in the unoptimized checks to allow matching inside helper functions.
4. Dev the package in test/Project.toml so that Pkg loads the local repository package under Julia 1.10.
@gbaraldi

gbaraldi commented Jun 30, 2026

Copy link
Copy Markdown
Member

https://github.com/JuliaLang/julia/blob/3d710776cb31ce05775ee10290f709df1f1e939c/src/codegen.cpp#L2308 This is how base does it for the alloca address-ace

- Encode size and alignment as constant call operands of a single
  `julia.gpu.alloca` declaration (matching `deferred_codegen`), instead
  of mangling them into the intrinsic name; `lower_alloca!` now guards
  with an O(1) name lookup like `lower_debug_level!`.
- Replace the hand-rolled bitcast/addrspacecast selection with
  `pointercast!`, and hoist the IRBuilder out of the per-call loop.
- Test the lowering on native (generic path) and GCN (nonzero alloca
  address space) only, dropping the identical PTX/SPIRV/Metal copies.
- Don't export `alloca`; downstream users call it qualified.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread test/Project.toml Outdated
Comment thread test/Project.toml Outdated
@gbaraldi

gbaraldi commented Jul 6, 2026

Copy link
Copy Markdown
Member

https://github.com/JuliaLang/julia/blob/3d710776cb31ce05775ee10290f709df1f1e939c/src/codegen.cpp#L2316-L2328 for the size of the alloys (we may also just want to do always power of 2)

Materialize `julia.gpu.alloca` slots with an element type equal to the
alignment (capped at 64 bits) rather than a flat `[bytes x i8]`. SROA
takes a hint from the element type and will shred an i8 array into
unaligned scalars; aligned integer chunks let it split into aligned
pieces instead, which is much better for vectorization. Mirrors Julia's
`emit_static_alloca`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vchuravy vchuravy requested a review from gbaraldi July 6, 2026 13:45
@gbaraldi

gbaraldi commented Jul 6, 2026

Copy link
Copy Markdown
Member

The one thing I'm a little worried is using Ptr{} as a representation. Those addrspacecasts aren't free on amdgpu. addrspace(0) has an addressing cost when translating to private memory so it might be worth to check if the codegen of it is as expected

@vchuravy

vchuravy commented Jul 6, 2026

Copy link
Copy Markdown
Member Author

I sadly can't get around it. We don't know the target during the generated function stage, but we must provide a stable return type so I can't use LLVMPtr

@gbaraldi

gbaraldi commented Jul 6, 2026

Copy link
Copy Markdown
Member

Can't we make a type for this? Something like AllocaArray or something? Though I guess it needs more intrinsics then.

Another option is to just remove the addrspacecast in a pass?

Edit: In theory InferAddressSpaces is capable of removing these, but it deserves a double check (I'm not sure if our 64 bit indexing mucks that up)

@vchuravy

vchuravy commented Jul 6, 2026

Copy link
Copy Markdown
Member Author

Can't we make a type for this?

Open to all ideas, but I am essentially working around the fact here that Base Julia has no true support for alloca, and using StaticArrays is just "make a type"

We can put the AS into the alloca intrinsic as a type parameter and then force each backend to use a device overlay

The alloca intrinsic now takes a target address space and returns an
LLVMPtr{T,AS} instead of a Ptr{T}. lower_alloca! emits an addrspacecast
only when the requested AS differs from the datalayout's alloca AS. The
two-argument form defaults to AS 0 (generic).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vchuravy

vchuravy commented Jul 9, 2026

Copy link
Copy Markdown
Member Author

@gbaraldi I think I am happy with this. Since each backend will have to define Scratchspace already we can add the AS target knowledge into the intrinsic.

Drop the two-argument convenience method so callers must always specify
the target address space.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vchuravy vchuravy merged commit 78eaff3 into main Jul 9, 2026
36 of 37 checks passed
@vchuravy vchuravy deleted the vc/alloca_intrinsic branch July 9, 2026 19:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants