Files
d4r/kernels/rr/rrswin_enc0_engine.md
T
woutermans 50fa65d87b engine/rr: native Ray Reconstruction stages with capture/replay and shim integration
- engine/rr: d4r's own shaders for the denoiser frame (exposure, six encoders,
  five decoders, hkpn output stage) at NGX layouts, plus PTX->GLSL/HIP packing
  scripts, reference implementations and weight extraction
- runtime.h/runtime_rr.inc, game_rr.{h,cpp}: GameDenoiser wired into the engine
  build (d4r-rr replay binary) and the D3D12 shim's feature-13 path
- shim: answer GetFeatureRequirements from d4r's own state only (the old
  cross-binary CUDA handoff crashed OptiScaler's scan); drop the core's
  CUDA GetFeatureRequirements import
- shim/harness: exact guide capture/replay of a frame's RR inputs
- kernels/rr: expanded enc0/engine prototypes; kernels/tools: ptxsim rewrite
  plus rrsim and an engine test; vulkan: ptx2glsl FP8/cooperative-matrix
  support, kvk inout buffers and KVK_DISABLE_OPTIMIZATION
- bridge: report the surface array in cuSurfObjectCreate traces
- packaging: RR settings and the ICU launch overrides in D4R_README
2026-10-12 12:47:53 +02:00

12 KiB
Raw Blame History

cuda_dldn_engine_swin_enc0_kernel on the staged-image engine

This file is the implementation record for enc0's port from the mirror-cut scalar body to the staged-image engine (kernels/rr/rrswin_engine.h). §1–§5 are the design contract the code implements; §6 is the evidence table that accrues as each offset and layout claim is validated (per-phase oracle captures, engine unit tests, output-surface compares).

Status: design fixed; primitives and body port dispatched. Gates and their results are recorded at the bottom as they are measured.

1. What the port changes

The mirror-cut body d4r_swin_enc0 (kernels/rr/rrswin_enc0.hip) keeps

  • its cut ABI (called from NVIDIA's PTX between the prologue and E22; arguments a0..a23 = the staged block's 24 A-fragment words, b0..b3 = phase 1's C seeds, w = arg040 (the 54560-byte prepared weight image), prm = the 1032-byte parameter block, scratch = the cut's input_tensor, 3200 bytes of shared memory),
  • every epilogue (E5/E10 softmax, E6/E12 norms and table adds, E13/15/17/19 activations, E20 arena scatter + lane merge, E21 surface scatter, E22 handover),
  • the score/P·V phases 5, 6, 10, 11 (score_gemm, pv_gemm), phase 21 (band21) and phase 22's GEMM as they are (scalar f32 dots over exact e4m3 decodes, 136 of 536 mma),

and replaces the sixteen plain GEMM phases 1, 2, 3, 4, 7, 8, 9, 12, 13, 14, 15, 16, 17, 18, 19, 20 (400 of 536 mma) with engine GEMMs: operands read from staged e4m3 images and prepared slots, one k32_e4m3 per (m, n) tile, accumulators rounded to f16 after each k32 step — the conservative chain that is bit-identical to the translated kernel's mma.sync.aligned.m16n8k32.row.col.f16.e4m3.e4m3.f16 (see §2 of docs/rr-engine.md and rrswin_engine_k16_test.cpp step 2).

Replaced with this port: the scalar gemm() calls for those sixteen phases, whose per-mma cost is 64 byte loads from the weight image, 64 e4m3 decodes, 128 f32 FMAs and twelve DPP permutes per lane.

2. Orientation and the two identities that make the port mechanical

Measured facts (all previously validated, not re-derived here):

  • D layout (kernels/tools/rrswin_operand_fit.cpp): engine tile D — lane l, VGPR i holds row i + 8h, column l & 15, h = l >> 4. With the engine's operand roles (swin_gemm_tile: weights = A via kop_image, activations = B via swin_operand) the tile's D is [weight column wr][token tk] — NVIDIA's D transposed: D[wr = i + 8h + 16*mt][tk = (l&15) + 16*nt] for tile (mt, nt).
  • Staging D into the next phase's A image is exactly the existing swin_stage_operand(img, stride, 16*nt, mt, acc): it writes img[(16*nt + l&15)*stride + 16*mt + 8h + i] = e4m3(acc[i]), i.e. image row = token, image k = weight column — the A-operand form the next phase's swin_operand reads.
  • The residual identity: the next phase's C operand is a cell-for-cell copy of the producer's D. NVIDIA's mma computes D = A·B + C where C[m][n] sits at (token, weight column); the engine tile's D cell (wr, tk) is C[tk][wr]. So phase P+1's engine tile seed is c[i] = f16(P's own tile (mt, nt) at VGPR i) — the producer's accumulator compacted to f16 pairs, no exchange, no shuffle. This is why phases 12, 16, 18, 20 (residual = previous phase's D) and 7 and 14 (residual + per-column table) need no C plumbing.
  • Per-column biases and tables are column-major f16: every read of the form *(u32*)(w + base + 16*nt + ((laneid<<2)&12)) (E9's seeds, wbias_regs, E6/E13's tables) places column c at byte 2*c of the blob. Evidence: the word for (nt, lane) covers columns 8nt+2t, 8nt+2t+1 at bytes 16nt+4t .. +3 = 2*(8nt+2t) .. +3. The engine tile seed for a per-column C is therefore c[i] = f32(f16 at w + 2*(i + 8h + 16*mt)).

3. The dcell exchange (the one cross-lane primitive the port adds)

Epilogues that consume the NVIDIA d[mi][rh][nt] view (norm_as for phases 2 and 12; the scalar score_gemm/pv_gemm for phases 3, 4, 8, 9; E20's r20 build for phase 20) get it from the engine tiles through one shared-memory exchange.

  • Writer (per tile (mt, nt), lane l, its own four f16 pairs pairs[p] = f16(D[2p]) | f16(D[2p+1]) << 16, p = 0..3): lds[(mt*n_nt + nt)*128 + lane*4 + p] = pairs[(mt*n_nt + nt)*4 + p] — all of it in u32 units: 128 words (= 512 bytes) per tile, 32 lanes × 4 words.
  • Reader: d[mi][rh][nt] = lds[((nt>>1)*n_nt + mi)*128 + (8rh + g + 16*(nt&1))*4 + t] with g = laneid>>2, t = laneid&3 — same units.

Derivation (checkable against the two measured facts above): the wanted cell is D[wr = 8nt + 2t + cb][tk = 16mi + 8rh + g]; its engine location is tile (mt_e = nt>>1, nt_e = mi), lane (tk%16) + 16*((wr%16)>>3) = 8rh + g + 16*(nt&1), VGPR wr%8 = 2t + cb — i.e. the reader's own pair index t, both halves. A full twelve-tile exchange is 12 × 512 B = 6144 B and an eight-tile one 4096 B — both exceed the scratch at exchange time, so every exchange runs in 2048-byte chunks (four tiles at a time) overlaying the just-dead image region, with the chunk loop's barriers owned by the phase code (§5).

4. New primitives (kernels/rr/rrswin_engine.h)

All are thin, checked by kernels/tools/rrswin_enc0_engine_test.cpp against independent host models before the body relies on them.

primitive signature semantics
swin_slots_from_wimg (uint8_t* slots, const uint8_t* w, int base, int n_rows) One phase's B fragments (8 u32 per lane at w + base + 512*t + 16*lane + 4*word, t = 0..n_rows/16-1, word = 0..3) scattered into slot layout: slot (n_rows*(k>>4) + n)*16 + (k&15), with (n, k) from the verified map n = 16t + (lane>>2) + 8*(word>>1), k = 2t' + (j&1) + 16*(j>>1) + 8*(word&1). Matches swin_gemm_tile's read with slot_base = 16*mt, slot_step = n_rows.
swin_dcell_exchange writer (lds_u32*, const uint32_t* pairs, int n_mt, int n_nt) / reader (const lds_u32*, uint32_t* d, int n_mi, int n_nt4, ...) §3. Writer and reader are separate calls; the wave barrier between them belongs to the caller.
swin_stage_operand_p (uint8_t* img, int stride, int row_base, int chunk, const uint32_t pairs[4], int mode) swin_stage_operand from a tile's four f16 pairs with a per-pair transform before e4m3x2: mode 0 plain, 1 relu_pair (ZLUDA's relu_keep_nan_halves), 2 act_pair (the clamped cubic). Byte i of the image row holds e4m3(transform(pair[i/2]) half i%2).

swin_stage_fragment needs no row-offset variant: the second half of a split 96-row GEMM restages its fragment tile group at local rows 0–47 (swin_stage_fragment(img, 32, frag + 12, 3)) and runs its GEMM tiles against local row_base = 0, emitting results to the tiles' global indices (nt 3–5). The D output of every split phase is registers (pairs), so no image write needs the global row position.

The relu and act transforms are per-element, so e4m3x2(transform(pair)) equals the translated kernel's transform-then-cvt.rn.satfinite.e4m3x2.f16x2 element for element.

5. Shared-memory map (3200 bytes, the cut's input_tensor)

input_noise (800 B) is a separate .shared variable whose offset relative to input_tensor the linked body cannot know, so the budget is the 3200-byte scratch only.

region size live during
image buffer 1536 (48 rows × 32 B) or 2048 (64 rows × 32 B) one phase's A image, split into halves for the 96-row phases
slots 1024 (64 slots × 16 B; 512 for phase 22, unused — phase 22 stays scalar) one phase's GEMM
dcell exchange 128 × (tile count) between a phase's GEMM and its dcell-consuming epilogue

The 96-row phases (1, 2, 3, 4, 8, 9) cannot hold a 3072-byte image and 1024 bytes of slots at once, so their GEMM runs in two token-tile halves (nt 0–2, nt 3–5): stage rows 0–47, run, restage rows 48–95, run. The halves' staging sources are registers (the cut's as1, the normed tile as3), so restaging is cheap. Phases 7 and 12–20 have 64-row images (2048 B) and fit without splitting. Every exchange overlays the just-dead image or slot region of the same phase.

6. Per-phase implementation table

phase mma A (engine B operand) B (engine A operand) C seed D consumed as
1 24 as1 fragments → image halves slots from w+4096 per-column, w+5120 relu-staged into phase 2's image (mode 1), no dcell
2 24 relu-staged image of D1 slots from w+5184 per-column, w+6208 dcell exchange → norm_as (gain w+6272, eps 2⁻¹³·2⁻⁴? see body) → as3; pairs kept for phase 7's C
3 24 as3 halves slots from w+6336 — dcell exchange → d3
4 24 as3 halves slots from w+7360 — dcell exchange → d4
5–6 48+48 scalar score_gemm/pv_gemm unchanged (d3, d4, bias w+8384)
7 16 as7 (from d6) → image slots from w+20672 d2 pairs + table w+37056 pairs kept (phase 12's C)
8 24 as3 halves slots from w+21696 — dcell exchange → d8
9 24 as3 halves slots from w+22720 — dcell exchange → d9
10–11 48+48 scalar score_gemm/pv_gemm unchanged (d8, d9, bias w+23744)
12 16 as12 (from d11) → image slots from w+36032 d7 pairs pairs kept (phase 14's C); dcell exchange → norm_as (gain w+37120, eps 2⁻¹³) → asN
13 16 asN → image slots from w+37184 per-column, w+38208 act-staged into phase 14's image (mode 2)
14 16 act-staged image of D13 slots from w+38272 d12 pairs + table w+39296 pairs kept (phase 16's C)
15 16 asN → image slots from w+39360 per-column, w+40384 act-staged into phase 16's image
16 16 act-staged image of D15 slots from w+40448 d14 pairs pairs kept (phase 18's C)
17 16 asN → image slots from w+41472 per-column, w+42496 act-staged into phase 18's image
18 16 act-staged image of D17 slots from w+42560 d16 pairs pairs kept (phase 20's C)
19 16 asN → image slots from w+43584 per-column, w+44608 act-staged into phase 20's image
20 16 act-staged image of D19 slots from w+44672 d18 pairs dcell exchange → E20's r20 build (unchanged)
21 32 scalar band21 unchanged (merged m21, weights w+45696, bias w+53888)
22 8 scalar gemm(..., nnt=2) unchanged (r20, w+54016, bias w+54528)

Weight-image byte ranges are the body's own verified table (rrswin_enc0.hip's "Weight image layout of the 22 phases" comment); waddr's formula is verified on all 1024 (n, k) pairs of a tile.

7. Validation gates and results

(filled as measured)

gate requirement result
translated reference set 22 oracle stages p1..p22 captured (/home/wouter/rr/gate-enc0/oracle-p<stage>/dumps), 6 launches each captured
harness determinism compare_launch_dump.py oracle-p1 vs oracle-p3: 108 args, 6 differ — only arg048, first differing byte exactly at 46,000,000 (the debug payload); all surfaces/textures/weights byte-identical proven
performance anchor (translated) D4R_HARNESS_FRAME_WAIT_MS=0 (continuous, matching the 16.2 ms config), 30 frames, frames 13–30: enc0 = 3.879 ms/frame, total 16.79 — the documented 3.79 ms baseline reproduced within ~2 % measured
full-frame reference control-80: 80-frame run of the installed runtime with D4R_HARNESS_SAVE_FRAMES, 80 saved frames (/home/wouter/rr/gate-enc0/control-80/out.raw.frameN) captured
primitives rrswin_enc0_engine_test.cpp steps vs independent host models pending
per-phase oracle engine D4R_RRSWIN_DEBUG_STAGE=<n> vs the translated stage, payload-window compare pending
layer surfaces compare_launch_dump.py translated vs engine enc0 output surfaces pending
full frame 80-frame engine run, frames 21–80 byte-identical to control-80 pending
performance enc0 < 3.879 ms/frame in the anchored continuous configuration pending