@@ -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
12161220end
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.
0 commit comments