Skip to content

Commit 116c36b

Browse files
vchuravyclaude
andcommitted
Add alloca intrinsic for per-workitem stack scratch
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>
1 parent 0c08fd0 commit 116c36b

3 files changed

Lines changed: 165 additions & 1 deletion

File tree

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name = "GPUCompiler"
22
uuid = "61eb1bfa-7361-4325-ad38-22787b887f55"
3-
version = "1.22.4"
3+
version = "1.23.0"
44
authors = ["Tim Besard <tim.besard@gmail.com>"]
55

66
[workspace]

src/irgen.jl

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,10 @@ function irgen(@nospecialize(job::CompilerJob))
151151
# the job's configured level, so device code can branch on it as a compile-time
152152
# constant that is part of the cache key (unlike reading the `-g` global directly).
153153
lower_debug_level!(job, mod)
154+
155+
# materialize `GPUCompiler.alloca` intrinsics as real entry-block allocas, before the
156+
# optimizer runs so the slots can be promoted (see `lower_alloca!`).
157+
lower_alloca!(job, mod)
154158
end
155159

156160
return mod, compiled, gv_to_value
@@ -1215,6 +1219,125 @@ function lower_debug_level!(@nospecialize(job::CompilerJob), mod::LLVM.Module)
12151219
return true
12161220
end
12171221

1222+
1223+
## stack allocation
1224+
1225+
# device code can request a fixed-size, per-workitem stack scratch buffer via
1226+
# `alloca(T, Val(N))`, returning a `Ptr{T}` to uninitialized storage for `N` elements of
1227+
# `T`. this emits the `julia.gpu.alloca.<bytes>.<align>` intrinsic, which `lower_alloca!`
1228+
# (run from `irgen`, before the optimizer) materializes as a real entry-block `alloca`.
1229+
#
1230+
# this exists because emitting an `alloca` directly through `llvmcall` is unsound/ineffective:
1231+
# the `Ptr` round-trip through `ptrtoint`/`inttoptr` blocks SROA/mem2reg promotion, the target
1232+
# stack address space (e.g. AS 5 on NVPTX/AMDGPU) isn't known at the front end, and the
1233+
# LangRef lifetime of an `alloca` is tied to the (inlined) `llvmcall` wrapper. lowering it
1234+
# ourselves lets us place the slot in the kernel entry block, in the datalayout's alloca
1235+
# address space, early enough for the optimizer to promote it.
1236+
1237+
function alloca_intr(mod::LLVM.Module, bytes::Integer, align::Integer)
1238+
name = "julia.gpu.alloca.$(bytes).$(align)"
1239+
intr = if haskey(functions(mod), name)
1240+
functions(mod)[name]
1241+
else
1242+
# returns an opaque pointer; intentionally *not* readnone/speculatable, as each call
1243+
# must yield a distinct slot and must not be hoisted or CSE'd.
1244+
LLVM.Function(mod, name, LLVM.FunctionType(LLVM.PointerType()))
1245+
end
1246+
return intr
1247+
end
1248+
1249+
# run-time equivalent: emits a call to the alloca intrinsic, returning a `Ptr{T}` to scratch
1250+
# storage for `N` elements of `T` (materialized by `lower_alloca!`).
1251+
function alloca_value(@nospecialize(T), N::Int)
1252+
isbitstype(T) ||
1253+
error("GPUCompiler.alloca only supports `isbits` element types, got $T")
1254+
N >= 0 || throw(ArgumentError("GPUCompiler.alloca count must be non-negative, got $N"))
1255+
1256+
bytes = sizeof(T) * N
1257+
align = Base.datatype_alignment(T)
1258+
1259+
# a zero-byte allocation has no storage to point at; hand back a null pointer rather than
1260+
# emitting a degenerate 0-element alloca.
1261+
if bytes == 0
1262+
return :(reinterpret(Ptr{$T}, C_NULL))
1263+
end
1264+
1265+
@dispose ctx=Context() begin
1266+
T_ptr = LLVM.PointerType()
1267+
1268+
# create function
1269+
llvm_f, _ = create_function(T_ptr)
1270+
mod = LLVM.parent(llvm_f)
1271+
1272+
# get intrinsic
1273+
intr = alloca_intr(mod, bytes, align)
1274+
intr_ft = function_type(intr)
1275+
1276+
# generate IR
1277+
@dispose builder=IRBuilder() begin
1278+
entry = BasicBlock(llvm_f, "entry")
1279+
position!(builder, entry)
1280+
1281+
ptr = call!(builder, intr_ft, intr, Value[], "alloca")
1282+
1283+
ret!(builder, ptr)
1284+
end
1285+
1286+
call_function(llvm_f, Ptr{T})
1287+
end
1288+
end
1289+
1290+
# device-facing accessor: a `Ptr{T}` to per-workitem stack scratch for `N` elements of `T`.
1291+
# the storage is uninitialized and only valid within the calling kernel. `T` must be `isbits`
1292+
# (an `alloca` of GC-tracked references would be unrooted). intended as a building block for
1293+
# higher-level scratch abstractions (e.g. KernelAbstractions' `@private`).
1294+
@inline @generated alloca(::Type{T}, ::Val{N}) where {T,N} = alloca_value(T, N)
1295+
export alloca
1296+
1297+
# replace every `julia.gpu.alloca.*` call with an entry-block alloca in the containing function
1298+
function lower_alloca!(@nospecialize(job::CompilerJob), mod::LLVM.Module)
1299+
changed = false
1300+
prefix = "julia.gpu.alloca."
1301+
1302+
for intr in collect(functions(mod))
1303+
fn = LLVM.name(intr)
1304+
startswith(fn, prefix) || continue
1305+
1306+
bytes, align = parse.(Int, split(fn[length(prefix)+1:end], '.'))
1307+
slot_typ = LLVM.ArrayType(LLVM.Int8Type(), bytes)
1308+
1309+
for use in collect(uses(intr))
1310+
call = user(use)
1311+
@assert call isa LLVM.CallInst
1312+
f = LLVM.parent(LLVM.parent(call))
1313+
1314+
@dispose builder=IRBuilder() begin
1315+
# materialize the slot at the top of the entry block so that it is a static
1316+
# alloca (promotable, and allocated once rather than per loop iteration).
1317+
position!(builder, first(instructions(first(blocks(f)))))
1318+
slot = alloca!(builder, slot_typ, "alloca")
1319+
alignment!(slot, align)
1320+
1321+
# `alloca!` placed the slot in the datalayout's alloca address space; cast back
1322+
# to generic (AS 0) to match the `Ptr` the front end handed out.
1323+
ptr = if LLVM.addrspace(value_type(slot)) == 0
1324+
slot
1325+
else
1326+
addrspacecast!(builder, slot, LLVM.PointerType())
1327+
end
1328+
replace_uses!(call, ptr)
1329+
end
1330+
erase!(call)
1331+
changed = true
1332+
end
1333+
1334+
@assert isempty(uses(intr))
1335+
erase!(intr)
1336+
end
1337+
1338+
return changed
1339+
end
1340+
12181341
# convert kernel state argument from pass-by-value to pass-by-reference
12191342
#
12201343
# the kernel state argument is always passed by value to avoid codegen issues with byval.

test/native.jl

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -739,3 +739,44 @@ end
739739
@test !occursin("deferred_codegen", ir)
740740
@test occursin("call void @julia_kernel", ir)
741741
end
742+
743+
@testset "stack allocation intrinsic" begin
744+
mod = @eval module $(gensym())
745+
import ..GPUCompiler
746+
747+
function scratch(x)
748+
p = GPUCompiler.alloca(Float32, Val(8))
749+
@inbounds unsafe_store!(p, x, 1)
750+
@inbounds unsafe_store!(p, x, 8)
751+
return @inbounds unsafe_load(p, 1) + unsafe_load(p, 8)
752+
end
753+
754+
# zero-element scratch yields a (null) pointer without emitting an alloca
755+
empty_scratch() = GPUCompiler.alloca(Float32, Val(0)) === reinterpret(Ptr{Float32}, C_NULL)
756+
end
757+
758+
# the intrinsic is materialized as a single entry-block `alloca [8 x f32 = 32 x i8]`,
759+
# and no `julia.gpu.alloca` call/declaration survives lowering.
760+
@test @filecheck begin
761+
@check_label "define float @{{(julia|j)_scratch_[0-9]+}}"
762+
@check "alloca [32 x i8], align 4"
763+
@check_not "julia.gpu.alloca"
764+
Native.code_llvm(mod.scratch, Tuple{Float32}; optimize=false)
765+
end
766+
767+
# once optimized the slot is promoted away entirely (result is x + x).
768+
@test @filecheck begin
769+
@check_label "define float @{{(julia|j)_scratch_[0-9]+}}"
770+
@check_not "alloca"
771+
@check_not "julia.gpu.alloca"
772+
Native.code_llvm(mod.scratch, Tuple{Float32})
773+
end
774+
775+
# a zero-byte allocation lowers to a null pointer rather than a degenerate alloca.
776+
@test @filecheck begin
777+
@check_label "define {{.*}}@{{(julia|j)_empty_scratch_[0-9]+}}"
778+
@check_not "alloca"
779+
@check_not "julia.gpu.alloca"
780+
Native.code_llvm(mod.empty_scratch, Tuple{})
781+
end
782+
end

0 commit comments

Comments
 (0)