Files
d4r/kernels/rr/rrswin_enc0_engine.md
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

164 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# `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 |