Skip to content

Commit 431a48c

Browse files
LostBeardclaude
andcommitted
4.16.0: device-local new T[N>32] dynamic-index correct on all 6 backends (Wasm fix) + CUDA graph API rollup
LowerArrays: a per-thread device-local `var acc = new T[N]` (N>32) written+read by a runtime index read back 0 on Wasm (correct on the other 5 backends). The rewriter positions new values at value.Index+1, so the alloca/view/defaultElement created while lowering NewArray landed AFTER `value`; SplitBlock(value) then pushed that setup into the loop EXIT block, leaving the array view DEFINED in the exit while the zero-init loop body USES it - an SSA dominance violation. GPU backends rematerialize the static alloca address at each use and tolerated it; the Wasm state machine ran the zero-init loop with an unset (0) view local and clobbered low linear memory. Fix: create the setup BEFORE `value` (builder.SetupInsertPosition(value)) so it stays in the dominating current block while SplitBlock(value) exports only the user code. Result-neutral for the 5 GPU backends; unroll path (N<=32) byte-identical. (A first attempt that split at defaultElement orphaned that constant - SplitBlock removes its split point - and regressed the PTX/OpenCL fixpoint analysis; caught by the full PMT sweep.) Un-skips BackendTestBase.LocalArray_DynamicIndex_MatchesCpuOracle on Wasm. Full PMT sweep: 4006 passed / 0 failed / 260 skipped. Stable cut 4.16.0 / forks 2.1.0 (minor - new public CUDA graph API lives in the fork): rolls up CUDA graph capture API + Accelerator.WithDefaultStream + ShaderDebugService.AppLoadTimestamp. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 7caef7f commit 431a48c

8 files changed

Lines changed: 69 additions & 32 deletions

File tree

CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,25 @@
22

33
This file tracks notable changes per release. The README's "Recent Highlights" section links here for the full version history.
44

5+
## 4.16.0 (2026-06-23) - CUDA graph capture API + device-local dynamic-index array fix complete on all 6 backends
6+
7+
Stable cut over 4.15.1. Forks bump to **2.1.0** (minor, not patch: this release adds genuinely new public API in the fork - see the fork-version note at the bottom of this entry). Rolls up the 4.15.2-local and 4.15.3-local builds plus the final Wasm fix. Two headline items:
8+
9+
**1. CUDA graph capture/replay API** (new public surface in `ILGPU.Runtime.Cuda`). Capture a fixed-shape kernel sequence once and replay it with a single `cuGraphLaunch`, collapsing per-kernel host dispatch overhead (built for LLM decode in SpawnDev.ILGPU.ML).
10+
- `CudaStream.BeginCapture(CudaStreamCaptureMode = ThreadLocal)` / `.EndCapture()` -> `CudaGraph` / `.CaptureStatus` / `CudaStream.SupportsGraphCapture`. `BeginCapture` throws on the default (NULL) stream - use a dedicated `accelerator.CreateStream()`.
11+
- `CudaGraph.Instantiate()` -> `CudaGraphExec.Launch(stream)` / `.Upload(stream)`. Both are context-bound `AcceleratorObject`s.
12+
- `Accelerator.WithDefaultStream(stream)` (all backends) - a scoped swap (restores on dispose) that reroutes every implicitly-grouped `*StreamKernel` launch onto one capturable stream with no per-call-site change.
13+
- Native graph entry points bound by hand against the loaded driver (same approach as `NvvmAPI`), cross-platform. Verified on RTX 4070 (`DemoConsole -- cuda-graph-capture`): capture inert, replay bit-exact vs CPU reference, ~6.6-7x dispatch microbench.
14+
15+
**2. Device-local dynamically-indexed `new T[]` arrays now correct on ALL 6 backends.** A per-thread device-LOCAL array `var acc = new float[N];` (compile-time size, **N > 32**) that is WRITTEN and READ by a **runtime index** is now correct on CUDA, OpenCL, CPU, WebGPU, WebGL, and Wasm. Two stacked `ILGPU/IR/Transformations/LowerArrays.cs` fixes:
16+
- **(a) Register-allocation crash (N > 32 init loop), from 4.15.3-local.1:** for N > 32 the array zero-init is an IR loop; its counter phi took its initial `0` from the loop **header** instead of the **predecessor**, breaking SSA dominance ("No register allocated for PrimitiveValue 0" at JIT). Only surfaced with dynamic indexing (static indices scalar-replace the array) and N > 32 (N <= 32 unrolls). Fix: create the init `0` in the predecessor.
17+
- **(b) Wasm "reads 0", new in this release:** fix (a) unmasked a Wasm-only miscompile. The rewriter positions new values at `value.Index + 1`, so the alloca/view/defaultElement created while lowering `NewArray` landed AFTER `value`; `SplitBlock(value)` then pushed that setup into the loop EXIT block, leaving the array view DEFINED in the exit while the zero-init loop body USES it - an SSA dominance violation. GPU backends rematerialize the static alloca address at each use and tolerated it; the Wasm state machine executes the literal block order, so the zero-init loop ran with an unset (0) view local and clobbered low linear memory (read back 0). Fix: create the setup BEFORE `value` (`builder.SetupInsertPosition(value)`) so it stays in the dominating current block while `SplitBlock(value)` exports only the user code. Result-neutral for the 5 GPU backends; the unroll path (N <= 32) is byte-identical.
18+
- Gate: `BackendTestBase.LocalArray_DynamicIndex_MatchesCpuOracle` (N=64, runtime index in a loop, CPU oracle) - passes on all 6 backends; the previous Wasm named-skip is removed. Full PMT sweep **4006 passed / 0 failed / 260 skipped (4266 total)**.
19+
20+
Also includes `ShaderDebugService.AppLoadTimestamp` (4.15.2-local.2).
21+
22+
**Fork version note:** the `SpawnDev.ILGPU.Fork` / `.Algorithms.Fork` `2.x` line is normally a lockstep sync counter bumped by patch. This release moves it to a **minor** (`2.0.x` -> `2.1.0`) because the CUDA graph API is genuinely new public surface that lives in the fork (`ILGPU/`), not the wrapper. Going forward: minor-bump the fork only when `ILGPU/` gains real public API, patch for bugfix/sync-only bundles.
23+
524
## 4.15.3-local.1 (2026-06-22) - Device-local dynamically-indexed array codegen fix
625

726
Forks bump to `2.0.44-local.1`. A per-thread device-LOCAL array `var acc = new float[N];` (compile-time size, **N > 32**) that is WRITTEN and READ by a **runtime index inside a loop** threw `InvalidCodeGenerationException` ("No register allocated for PrimitiveValue 0") at kernel JIT - blocking the register/local-memory-accumulator universal (flash-class) per-query attention kernel (SpawnDev.ILGPU.ML, Tuvok).

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ Detailed constraints live in each directory's own `CLAUDE.md`. Read the relevant
4545

4646
Tests in `SpawnDev.ILGPU.Demo.Shared/UnitTests/BackendTestBase*.cs` (~211 tests, Tests1-10). Backend-specific classes inherit and override unsupported tests. See `PlaywrightMultiTest/CLAUDE.md` for running tests.
4747

48-
**Current version: see [CHANGELOG.md](CHANGELOG.md) — it is the SINGLE SOURCE OF TRUTH for versions/features (this header is NOT kept in lockstep; do not trust a version number written below).** As of 2026-06-22: nuget.org stable = **4.15.1** (forks `2.0.42`); local feed (`D:\users\SpawnDevPackages`) latest = **4.15.3-local.1** (forks `2.0.44-local.1`) = CUDA graph capture API (`CudaStream.BeginCapture/EndCapture` + `CudaGraph`/`CudaGraphExec` + `Accelerator.WithDefaultStream`) + the device-local dynamically-indexed `new T[]` codegen fix (works on CUDA/OpenCL/CPU/WebGPU/WebGL; ⚠ Wasm N>32 dynamic-index still reads 0 — use `LocalMemory.Allocate<T>(N)`, see `Wasm/CLAUDE.md`). The rest of this paragraph is HISTORICAL context (rc.7–rc.10 era):
48+
**Current version: see [CHANGELOG.md](CHANGELOG.md) — it is the SINGLE SOURCE OF TRUTH for versions/features (this header is NOT kept in lockstep; do not trust a version number written below).** As of 2026-06-23: nuget.org stable target = **4.16.0** (forks `2.1.0`) = CUDA graph capture API (`CudaStream.BeginCapture/EndCapture` + `CudaGraph`/`CudaGraphExec` + `Accelerator.WithDefaultStream`) + the device-local dynamically-indexed `new T[N>32]` codegen fix now correct on **all 6 backends incl Wasm** (`LowerArrays` view-def-block placement; full PMT 4006/0/260). Previous stable = 4.15.1 (forks 2.0.42). The rest of this paragraph is HISTORICAL context (rc.7–rc.10 era):
4949

5050
**rc.10 headline**: LocalMemory<T>(N>=32) WGSL codegen 5-layer fix (unblocks Tuvok's Vp9Idct8x8Kernel + 16x16/32x32/iADST/iHT kernel family on WebGPU bit-exact), `AcceleratorRequirements` capability-gating API + extension methods, `UnsupportedKernelFeatureException` typed exception wired at WebGL GenericAtomic + AtomicCAS codegen sites. Regression test `LocalMemoryRepro_Int64_ShortByteViews` locks down both the WebGPU fix and the WebGL architectural varying-count ceiling. rc.10 docs landed in README "What's New in 4.9.2." **Current P2P sibling: 4.9.2-rc.22** (WebTorrent 3.1.4, full 6-gap audit closed, binary wire framing for BufferSend/BufferData via `P2PBinaryFrame` + ConfigureHighThroughputSctp helper - 10MB WebRTC dispatch passes in 25s). **Pending at HEAD (historical context, shipped in rc.7+): f16 emulation Phases 1 + 2 + 3** - `Capabilities.Float16` always true across WebGPU / WebGL / Wasm / OpenCL; `Capabilities.Float16Native` distinguishes native-vs-emulated on backends where both paths exist (WebGPU, OpenCL); `WebGPUBackend.ForceEmulatedF16` test flag. Emulation paths: WebGPU WGSL `_f16_to_f32` / `_f32_to_f16` helpers + packed u16 storage; WebGL GLSL helpers + Transform Feedback uint output; OpenCL `vload_half` / `vstore_half` built-ins (no extension required) + f32 arithmetic. Full `hardwareConcurrency` multi-worker barrier dispatch with pure-spin generation barriers (wait/notify races on V8 — see Wasm/CLAUDE.md "Barriers are PURE SPIN") and in-Wasm phase dispatcher (no JS-Wasm boundary crossings between phases). All large sort tests (260K-4M) passing including SpawnSceneSimulation (1.4M elements, multi-frame). rc.7 key fixes: WGSL spinlock-key refactor (tuple-keyed `array<atomic<u32>>` for f64 Min/Max/Exchange), Wasm cascade-safe Dispose (per-worker TCS fault on dispose), wait/notify barrier with wakeup loop (replaced pure spin after diagnosing spurious wakeup bug - not a V8 bug), shared memory alloca overlap (same-size dedup), IR address space aliasing (InferAddressSpaces guards), struct/scratch overlap, multi-pass scan, Float16, unsigned ops, 256 threads, memory.grow(), ViewSourceSequencer, subViewByteOffset, atomic RMW opcode table, CopyFromBuffer, onesComplementMask .tt template, per-worker scratch, atomic.fence at 3 sync points, float atomic stores, broadcast atomic store/load, barrier counter zeroing between groups.
5151

ILGPU.Algorithms/ILGPU.Algorithms.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
SpawnDev.ILGPU.Fork* PackageReference Versions inside SpawnDev.ILGPU.csproj.
1313
Run `_check-fork-version-sync.bat` at repo root. See the banner comment in
1414
SpawnDev.ILGPU.csproj for the full procedure. -->
15-
<Version>2.0.44-local.1</Version>
15+
<Version>2.1.0</Version>
1616
<IsPackable>true</IsPackable>
1717
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
1818
</PropertyGroup>

ILGPU/ILGPU.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
check on push. Skipping (b) means consumers ship rc.N still pulling old Fork
1717
transitively and the fix is invisible. See the banner comment in
1818
SpawnDev.ILGPU.csproj for the full procedure. -->
19-
<Version>2.0.44-local.1</Version>
19+
<Version>2.1.0</Version>
2020
<IsPackable>true</IsPackable>
2121
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
2222
</PropertyGroup>

ILGPU/IR/Transformations/LowerArrays.cs

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,22 @@ private static void Lower(
126126
.AsNotNull()
127127
.TargetAddressSpace;
128128

129+
// Insert all of the values we are about to create (the length math, the
130+
// alloca, the view, and the defaultElement) BEFORE `value` instead of
131+
// after it. The rewriter positions us at value.Index + 1, so by default
132+
// our setup lands AFTER the NewArray. That matters for the N > 32 path:
133+
// SplitBlock(value) below moves everything after the split point into the
134+
// loop's exit block AND removes the split point itself - so with the
135+
// default position our own alloca/view/defaultElement would be pushed into
136+
// the exit block, leaving the array view defined in the loop EXIT while the
137+
// zero-init loop body uses it (an SSA dominance violation). GPU codegen
138+
// tolerated it by rematerializing the static alloca address at each use,
139+
// but the Wasm state machine ran the zero-init loop with an unset (0) view
140+
// and clobbered low memory (read back 0). Creating the setup before `value`
141+
// keeps it in the dominating current block; SplitBlock(value) then only
142+
// exports the user code that followed the NewArray.
143+
builder.SetupInsertPosition(value);
144+
129145
// Compute array length
130146
Value totalLength = builder.CreatePrimitiveValue(location, 1);
131147
foreach (Value length in value)
@@ -213,10 +229,13 @@ private static void Lower(
213229
var methodBuilder = builder.MethodBuilder;
214230
var int32Type = builder.CreateType(typeof(int));
215231

216-
// Split the current block at the NewArray value. This moves
217-
// all values that come after it (including the terminator) to
218-
// a new "exit" block, leaving the current block open for us
219-
// to redirect into the loop.
232+
// Split the current block at the NewArray value. This removes
233+
// `value` (it is replaced by the array structure below) and moves
234+
// all values that came after it - the user code following the
235+
// NewArray - into a new "exit" block, leaving the current block
236+
// (which now holds our alloca/view/defaultElement, created before
237+
// `value` above) open for us to redirect into the loop. Because the
238+
// setup precedes the split point it stays in this dominating block.
220239
var exitBlock = builder.SplitBlock(value);
221240

222241
// Create the loop header and body blocks

SpawnDev.ILGPU.Demo.Shared/UnitTests/BackendTestBase.LocalArrayDynamicIndex.cs

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,18 @@ namespace SpawnDev.ILGPU.Demo.Shared.UnitTests
2828
// This kernel hits BOTH conditions: N = 64 (> 32 -> IR init loop) and a runtime index
2929
// dd in a loop (write + read). One output per thread, no shared memory, no barrier ->
3030
// valid on all 6 backends incl WebGL (Transform Feedback one-record-per-vertex).
31+
//
32+
// SECOND ROOT CAUSE (fixed 2026-06-23, also in LowerArrays.cs): the N>32 init-loop fix
33+
// above unmasked a Wasm-only "reads 0" bug. The rewriter positions new values at
34+
// value.Index + 1, so the alloca/view/defaultElement created while lowering NewArray sit
35+
// AFTER `value` in the block; SplitBlock(value) then pushed that setup into the loop EXIT
36+
// block, leaving the array view DEFINED in the exit while the zero-init loop body (and the
37+
// struct) USE it - an SSA dominance violation. GPU backends rematerialize the static alloca
38+
// address at each use and tolerated it; the Wasm state machine executes the literal block
39+
// order, so the zero-init loop ran with an unset (0) view local and clobbered low linear
40+
// memory instead of the array. Fixed by inserting the setup BEFORE `value`
41+
// (builder.SetupInsertPosition(value)) so it stays in the dominating current block while
42+
// SplitBlock(value) exports only the user code. Now correct on ALL 6 backends.
3143
public abstract partial class BackendTestBase
3244
{
3345
private const int LADI_N = 64; // > MaxUnrolledArrayInitSize(32) -> emits the IR init loop
@@ -56,24 +68,9 @@ private static void LocalArrayDynamicIndexKernel(
5668
[TestMethod]
5769
public async Task LocalArray_DynamicIndex_MatchesCpuOracle() => await RunTest(async accelerator =>
5870
{
59-
// KNOWN ISSUE (Wasm only, tracked) - the IR-level fix above makes this kernel
60-
// COMPILE + run correct on CUDA / OpenCL / CPU / WebGPU / WebGPU-no-subgroups /
61-
// WebGL. It then exposed a previously-unreachable Wasm codegen bug: a device-LOCAL
62-
// alloca that is NOT the first scratch consumer (here scratchOffset=16, because the
63-
// array-impl struct takes scratch[0..16) first) reads back 0 - the large (N>32,
64-
// loop-init) dynamically-indexed array's scratch addressing is wrong when
65-
// baseOff != 0 (WasmKernelFunctionGenerator local-alloca path, scratchBase+baseOff).
66-
// N<=32 (companion LocalArrayAcrossBarrier, Tile=8) has baseOff=0 and passes. Under
67-
// active investigation in MY lane - see DevComms
68-
// geordi-localarray-dynindex-fixed-wasm-scratchoffset-open-2026-06-22. Tracked, not
69-
// hidden (Rule 2a): the other 6 backends - including every browser GPU backend Tuvok
70-
// needs for the universal attention - are verified correct.
71-
if (accelerator.AcceleratorType == AcceleratorType.Wasm)
72-
throw new UnsupportedTestException(
73-
"KNOWN OPEN BUG (Geordi, tracked): Wasm scratch addressing for a large (N>32) " +
74-
"dynamically-indexed device-local alloca with scratchOffset!=0 reads 0. " +
75-
"Fixed on the other 6 backends. See geordi-localarray-dynindex-fixed-wasm-scratchoffset-open-2026-06-22.");
76-
71+
// Verified correct on ALL 6 backends (CUDA / OpenCL / CPU / WebGPU /
72+
// WebGPU-no-subgroups / WebGL / Wasm) after both LowerArrays fixes - see the
73+
// class-level comment for the two root causes.
7774
const int G = 64;
7875
const int D = LADI_N; // exercise the full array length
7976
const int REPS = 3;

SpawnDev.ILGPU/SpawnDev.ILGPU.csproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<TargetFramework>net10.0</TargetFramework>
55
<ImplicitUsings>enable</ImplicitUsings>
66
<Nullable>enable</Nullable>
7-
<Version>4.15.3-local.1</Version>
7+
<Version>4.16.0</Version>
88
<!-- Brief current-version highlights only. Full per-version history with code samples lives in CHANGELOG.md (linked from the README). -->
99
<PackageReleaseNotes>4.15.3-local.1: Device-LOCAL dynamically-indexed array codegen fix (Fork 2.0.44-local.1). A per-thread `new T[N]` (N>32) written/read by a RUNTIME index in a loop threw InvalidCodeGenerationException ("No register allocated for PrimitiveValue 0") at kernel JIT. Root cause: the LowerArrays N>32 zero-init loop created the counter-phi's initial 0 constant in the loop HEADER instead of the predecessor block (invalid SSA dominance). Fixed (result-neutral). Verified compile+correct on CUDA/OpenCL/CPU/WebGPU/WebGPU-no-subgroups/WebGL; one tracked Wasm-only follow-up (large dynamically-indexed local alloca with scratchOffset!=0). Unblocks register/local-memory accumulator kernels (flash-class per-query attention) on the GPU backends. Also includes 4.15.2-local CUDA graph capture API + Accelerator.WithDefaultStream + ShaderDebugService.AppLoadTimestamp.
1010

@@ -67,8 +67,8 @@
6767
<ProjectReference Include="..\ILGPU.Algorithms\ILGPU.Algorithms.csproj" />
6868
</ItemGroup>
6969
<ItemGroup Condition="!Exists('$(MSBuildThisFileDirectory)..\ILGPU\ILGPU.csproj')">
70-
<PackageReference Include="SpawnDev.ILGPU.Fork" Version="2.0.44-local.1" />
71-
<PackageReference Include="SpawnDev.ILGPU.Algorithms.Fork" Version="2.0.44-local.1" />
70+
<PackageReference Include="SpawnDev.ILGPU.Fork" Version="2.1.0" />
71+
<PackageReference Include="SpawnDev.ILGPU.Algorithms.Fork" Version="2.1.0" />
7272
</ItemGroup>
7373

7474
<ItemGroup>

0 commit comments

Comments
 (0)