@@ -149,6 +149,10 @@ function irgen(@nospecialize(job::CompilerJob))
149149 # the job's configured level, so device code can branch on it as a compile-time
150150 # constant that is part of the cache key (unlike reading the `-g` global directly).
151151 lower_debug_level! (job, mod)
152+
153+ # materialize `GPUCompiler.alloca` intrinsics as real entry-block allocas, before the
154+ # optimizer runs so the slots can be promoted (see `lower_alloca!`).
155+ lower_alloca! (job, mod)
152156 end
153157
154158 return mod, compiled, gv_to_value
@@ -1216,6 +1220,138 @@ function lower_debug_level!(@nospecialize(job::CompilerJob), mod::LLVM.Module)
12161220 return true
12171221end
12181222
1223+
1224+ # # stack allocation
1225+
1226+ # device code can request a fixed-size, per-workitem stack scratch buffer via
1227+ # `alloca(T, Val(N), Val(AS))`, returning an `LLVMPtr{T,AS}` to uninitialized storage for `N`
1228+ # elements of `T` in address space `AS`. this emits a call to the `julia.gpu.alloca` intrinsic
1229+ # with the size and alignment as constant operands, which `lower_alloca!` (run from `irgen`,
1230+ # before the optimizer) materializes as a real entry-block `alloca`.
1231+ #
1232+ # this exists because emitting an `alloca` directly through `llvmcall` is unsound/ineffective:
1233+ # the `Ptr` round-trip through `ptrtoint`/`inttoptr` blocks SROA/mem2reg promotion, the target
1234+ # stack address space (e.g. AS 5 on NVPTX/AMDGPU) isn't known at the front-end, and the
1235+ # LangRef lifetime of an `alloca` is tied to the (inlined) `llvmcall` wrapper. lowering it
1236+ # ourselves lets us place the slot in the kernel entry block, in the datalayout's alloca
1237+ # address space, early enough for the optimizer to promote it.
1238+
1239+ function alloca_intr (mod:: LLVM.Module , T_ptr:: LLVMType )
1240+ name = " julia.gpu.alloca"
1241+ intr = if haskey (functions (mod), name)
1242+ functions (mod)[name]
1243+ else
1244+ # takes the size in bytes and the alignment as constant operands, and returns a
1245+ # pointer in the requested address space; intentionally *not* readnone/speculatable,
1246+ # as each call must yield a distinct slot and must not be hoisted or CSE'd. a module
1247+ # only ever targets a single address space, so one declaration suffices.
1248+ T_i64 = LLVM. Int64Type ()
1249+ LLVM. Function (mod, name, LLVM. FunctionType (T_ptr, [T_i64, T_i64]))
1250+ end
1251+ return intr
1252+ end
1253+
1254+ # run-time equivalent: emits a call to the alloca intrinsic, returning an `LLVMPtr{T,AS}` to
1255+ # scratch storage for `N` elements of `T` in address space `AS` (materialized by
1256+ # `lower_alloca!`).
1257+ function alloca_value (@nospecialize (T), N:: Int , AS:: Int )
1258+ isbitstype (T) ||
1259+ error (" GPUCompiler.alloca only supports `isbits` element types, got $T " )
1260+ N >= 0 || throw (ArgumentError (" GPUCompiler.alloca count must be non-negative, got $N " ))
1261+
1262+ bytes = sizeof (T) * N
1263+ align = Base. datatype_alignment (T)
1264+
1265+ # a zero-byte allocation has no storage to point at; hand back a null pointer rather than
1266+ # emitting a degenerate 0-element alloca.
1267+ if bytes == 0
1268+ return :(reinterpret (Core. LLVMPtr{$ T,$ AS}, C_NULL ))
1269+ end
1270+
1271+ @dispose ctx= Context () begin
1272+ # `LLVMPtr{T,AS}` lowers to an (i8/opaque) pointer in address space `AS`; match that
1273+ # as the intrinsic's return type so the `llvmcall` boundary type-checks.
1274+ T_ptr = convert (LLVMType, Core. LLVMPtr{T,AS})
1275+
1276+ # create function
1277+ llvm_f, _ = create_function (T_ptr)
1278+ mod = LLVM. parent (llvm_f)
1279+
1280+ # get intrinsic
1281+ intr = alloca_intr (mod, T_ptr)
1282+ intr_ft = function_type (intr)
1283+
1284+ # generate IR
1285+ @dispose builder= IRBuilder () begin
1286+ entry = BasicBlock (llvm_f, " entry" )
1287+ position! (builder, entry)
1288+
1289+ args = Value[ConstantInt (LLVM. Int64Type (), bytes),
1290+ ConstantInt (LLVM. Int64Type (), align)]
1291+ ptr = call! (builder, intr_ft, intr, args, " alloca" )
1292+
1293+ ret! (builder, ptr)
1294+ end
1295+
1296+ call_function (llvm_f, Core. LLVMPtr{T,AS})
1297+ end
1298+ end
1299+
1300+ # device-facing accessor: an `LLVMPtr{T,AS}` to per-workitem stack scratch for `N` elements of
1301+ # `T` in address space `AS`. the storage is uninitialized and only valid within the calling
1302+ # kernel. `T` must be `isbits` (an `alloca` of GC-tracked references would be unrooted).
1303+ # intended as a building block for higher-level scratch abstractions (e.g. KernelAbstractions'
1304+ # `@private`).
1305+ @inline @generated alloca (:: Type{T} , :: Val{N} , :: Val{AS} ) where {T,N,AS} = alloca_value (T, N, AS)
1306+
1307+ # pick the element type for a `bytes`-sized, `align`-aligned stack slot. rather than a flat
1308+ # `[bytes x i8]`, emit aligned integer chunks: SROA takes a hint from the element type and
1309+ # will happily shred an i8 array into unaligned scalars (terrible for vectorization), whereas
1310+ # an element size equal to the alignment makes it split into aligned pieces instead. the
1311+ # element size is capped at 64 bits since not all back-ends support wider integers. mirrors
1312+ # Julia's `emit_static_alloca` (src/codegen.cpp).
1313+ function alloca_slot_type (bytes:: Integer , align:: Integer )
1314+ elsize = min (align, 8 )
1315+ padded = cld (bytes, elsize) * elsize
1316+ eltyp = LLVM. IntType (elsize * 8 )
1317+ # a single element covers the whole slot; don't bother wrapping it in a length-1 array.
1318+ return padded == elsize ? eltyp : LLVM. ArrayType (eltyp, padded ÷ elsize)
1319+ end
1320+
1321+ # replace every `julia.gpu.alloca` call with an entry-block alloca in the containing function
1322+ function lower_alloca! (@nospecialize (job:: CompilerJob ), mod:: LLVM.Module )
1323+ haskey (functions (mod), " julia.gpu.alloca" ) || return false
1324+ intr = functions (mod)[" julia.gpu.alloca" ]
1325+
1326+ @dispose builder= IRBuilder () begin
1327+ for use in collect (uses (intr))
1328+ call = user (use)
1329+ @assert call isa LLVM. CallInst
1330+ bytes, align = convert .(Int, operands (call)[1 : 2 ])
1331+ f = LLVM. parent (LLVM. parent (call))
1332+
1333+ # materialize the slot at the top of the entry block so that it is a static
1334+ # alloca (promotable, and allocated once rather than per loop iteration).
1335+ position! (builder, first (instructions (first (blocks (f)))))
1336+ slot = alloca! (builder, alloca_slot_type (bytes, align), " alloca" )
1337+ alignment! (slot, align)
1338+
1339+ # `alloca!` placed the slot in the datalayout's alloca address space; cast it to
1340+ # the intrinsic's return type, i.e. the address space requested by the caller
1341+ # (emitting an addrspacecast when it differs from the alloca address space).
1342+ ptr = pointercast! (builder, slot, value_type (call))
1343+
1344+ replace_uses! (call, ptr)
1345+ erase! (call)
1346+ end
1347+ end
1348+
1349+ @assert isempty (uses (intr))
1350+ erase! (intr)
1351+
1352+ return true
1353+ end
1354+
12191355# convert kernel state argument from pass-by-value to pass-by-reference
12201356#
12211357# the kernel state argument is always passed by value to avoid codegen issues with byval.
0 commit comments