Files
d4r/docs/rr-engine.md
woutermans 417abc64b5 zluda: renumber the RR patches to 0009-0013
0008 was already taken by 0008-native-post-phase-kernels.patch, so the five
patches that follow it in the build order shift up by one: ray-reconstruction
PTX support, native-FP8 conversion, packed FP8 operands, shadow-store
coalescing and signed FP8 conversion. The patches themselves are unchanged;
only their numbers and the references in README.md, docs/building.md,
docs/native-kernels.md, docs/rr-engine.md, packaging/SOURCES.txt and
scripts/build_release_glibc241.sh move.
2026-10-12 12:47:53 +02:00

189 lines
11 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.
# The swin-layer engine: design
Why this exists: a per-phase port of NVIDIA's `cuda_dldn_engine_swin_*` keeps the cost that
makes those kernels slow. Disassembled, the translated `swin_enc0` kernel is 83,373
instructions for 536 FP8 WMMAs. The top of the mix is operand assembly, not arithmetic:
| instruction | count |
| --- | --- |
| `v_cndmask_b32_e64` | 9,124 |
| `ds_bpermute_b32` | 7,976 |
| `v_lshrrev_b32_e32` / `v_pk_lshrrev_b16` | 9,695 |
| `v_and_b32` / `v_perm_b32` / `v_pk_add_u16` / `v_pk_min_u16` | 13,750 |
| `v_cvt_f16_f32` / `v_cvt_f32_f16` (all forms) | 3,568 |
| `v_wmma_f32_16x16x16_fp8_fp8` | 536 |
Those bit operations and cross-lane permutes exist only to reassemble NVIDIA's register
fragments into WMMA operands. A kernel that keeps its activations in a staged e4m3 image and
reads its operands directly from it does not pay them.
## The primitives, already in the tree and already bit-identical to the translated path
`kernels/tex/tex_common.h` (with `kernels/common/wmma_layout.h`), used today by the working
natives `kernels/tex/enc0_tail.hip` and `kernels/tex/dec0_head.hip`:
- `kop_from16(u4v d)` — this half's WMMA operand (8 e4m3 bytes) from 16 e4m3 bytes, in
NVIDIA's k order. It is a half selection, not a shuffle.
- `kop_image(const kslot* img, int idx)` — the same from a prepared weight image, where a
`kslot` is the 16 e4m3 bytes of one k16 operand row.
- `kslot_from16(u4v d)` — the weight image's own conversion, done once in a `_prep` kernel,
so the main kernel never touches the weight layout.
- `k32_e4m3(f8v c, kop a0, kop b0, kop a1, kop b1)` — one m16n8k32 e4m3 step as two gfx12
FP8 WMMAs. With `D4R_TEX_FP8` it is the native FP8 instruction with no scaling; the
comment records that its result is bit-identical to ZLUDA's gfx12 lowering of NVIDIA's
mma, which is what our reference runs through.
The one structural fact that makes this cheap: on gfx12 the accumulator layout *is* the
next GEMM's operand layout (`wmma_layout.h`: "D VGPR i is row i + 8h, which is the gfx12
B-operand layout of the next GEMM (no exchange)"). So an epilogue requantising its
accumulator to e4m3 and staging it as a row-major image in shared memory gives the next
phase its operands with no cross-lane movement at all.
## Shape of a layer
1. `_prep` kernel (once per launch, like `enc0_tail`'s): read the layer's weight image at
`arg040` and write each phase's B operands as `kslot`s into a global buffer.
2. Main kernel, per token patch:
- load the layer's input (texture/surface args) and compute the first operand image;
- for each phase in the descriptor: build A (`kop_from16` from a staged image, or from
the previous phase's requantised accumulator), run `k32_e4m3` per (m, n) tile with the
prepared B, apply the epilogue (e4m3 requantise into the next image, clamped cubic
activation, f16 column-table add, RMS/LayerNorm, softmax, patch expand, plane scatter,
lane merge, surface scatter);
- write the layer's output.
## Descriptor
Each layer is a `kernels/rr/descriptor_<layer>.py` with `phases()` (a list of dicts, one per
phase: shapes, A source, B offset, C source, K tiling, epilogue kind and its operands) and
`layer()` (launch geometry, argument offsets, token/tile geometry). Every offset must be a
number with evidence behind it; anything unverified must say so.
## Validation
By output surface against the translated reference: run the layer's native kernel alone
(`D4R_RRSWIN_ONLY=<layer>` in its own native dir) and compare the layer's own output
surfaces with `kernels/tools/compare_launch_dump.py`. Per-phase oracle comparison is a
debugging aid for a phase that is already failing, not the acceptance gate. Note two traps
that cost the agents hours: the debug payload is written into a *window* of a larger block
(the containing argument and the window offset differ per layer, so read the argument the
arena actually lives in, at `arena_offset + base`), and the harness reads its dump after a
context synchronise, so a payload can be masked by later kernels in the frame.
### Current gfx1201 evidence
The checked shared-memory primitive probe (`rrswin_engine_k16_test.cpp`) passes its
GEMM, f16 accumulation rounding, requantization, weight preparation and phase-chain
comparisons. These checks do not establish a complete RR implementation.
The phase-8 pilot now compares all 196,608 f16 results in 64 captured blocks
without a mismatch. Its previous comparison was invalid: a six-token-tile phase
overflowed a four-tile accumulator array, device output had no block stride, and
the host block stride omitted the 32 lanes. Weight preparation now uses per-block
shared memory rather than concurrent writes into one global buffer.
The host phase-8 model also needed all 32 lanes to assemble the activation image,
an 80-register stride for the combined operand/result payload, and f16 output
rounding. With those corrections, the documented fragment and weight maps match
the captured oracle; the previous apparent operand-layout contradiction was not
evidence of a wrong map.
An isolated native phase-8 splice preserves the other NVIDIA phases and produces
a byte-identical final 3840×2160 RGBA16F frame with matching denormal settings.
The initial splice is slower: enc0 measures 14.90 ms versus a 6.59 ms translated
baseline. It is a correctness experiment, not a shipping performance improvement.
The first implementation of patch `0010`'s native FP8 requantization measured
26.65 ms of GPU kernel time at 720p→4K after six warm-up frames. Its same-runtime
software-codec control measured 33.90 ms. Compact integer saturation plus
`0011`'s packed FP8 operand gathers measure 25.33 ms in that 20-frame run;
the complete RGBA16F output remains byte-identical. The matching 80-frame
control measures 24.764 ms after excluding the first 20 frames. Patch `0012`'s
complete-pair shadow-store coalescing and eight-wave register budgeting together
measure 19.146 ms, with all 80 saved frames byte-identical to that control.
Patch `0013`'s signed native conversion reduces the same configuration to
16.234 ms with a byte-identical final frame; its plain and ReLU conversions
match software for every half encoding in both packed positions. Removing
the harness's 200 ms inter-frame idle gap measures 15.144 ms in steady state,
also with the identical final image.
The older installed-runtime FTZ output differs and is not a valid arithmetic
control. Cyberpunk with `0010` + `0011` rendered a loaded 4K save with
720p RR input, coherent moving views and approximately 26.0 ms overlay upscaler
time. With `0012` + `0013` the harness measures 16.234 ms (200 ms idle gap
between frames) and 15.144 ms continuous, with byte-identical final frames.
## Next direct-converter win: B operands from transposed accumulators
Validated mapping (read-only IR investigation, not yet implemented) for gfx12
native-FP8 MMAs whose A operand is a requantization of an earlier accumulator
pair — the encoder's relu and plain chains in
`/tmp/d4r-packed-enc0-analysis/linked.ll`:
* Producer: an MMA pair produces four `<2 x i32>` transposed accumulators
`(q=0/1, p=1/2)`; today each is converted with `d4r_wmma_t_to_nv` (4 bpermutes),
stored to registers, requantized by `d4r_cvt_rn_satfinite[_relu]_e4m3x2_f16x2`,
repacked into four words (`%11374..%11377` for the relu chain), loaded again by
every consumer and converted by `d4r_wmma12f8_b_from_a` (8 bpermutes per
consumer). ≥60 consumer sites in enc0, ≥3 fragment groups.
* The composition `t_to_nv → cvt → pack → b_from_a` collapses algebraically: with
`h = lane/16`, `r = lane&7`, `t = 2h`, the WMMA B word `j` of a lane is
`pack(cvt(f16x2{lo16(gather(A0, T_j)), lo16(gather(A1, T_j))}), ...)`, where
`A0/A1 = (r, r+8)` and register `.x` for lanes < 16, `(r+8, l)` and `.y`
otherwise. One helper per producer fragment —
`d4r_wmma12f8_b_from_t(uint2 d00, uint2 d01, uint2 d10, uint2 d11)` — replaces
32 producer bpermutes + the alloca round trip + 8 bpermutes per consumer with
8 bpermutes total, all consumers CSE to one call.
* Slot order is chain-specific (out0/out1 ← q=0; out2/out3 ← q=1; low/high 16-bit
half ← p=1/p=2); a matcher must read it off the actual insertelement/bitcast
structure, and reject fragments with mixed cvt variants or partial updates
(F32-shadow path needs its own derivation and is not covered).
* The same investigation found no sites where packed-f16 arithmetic could
commute across `toNv` (the inserted values come from global loads, not shadow
conversions), so that rewrite is not worth building.
Full derivation with anchors is in the session transcript
(`agent://Fp8ShadowOperandFusion`); the probe extension to gate it (producer →
requantize → consumer chain in `rrswin_fp8_unpaired.ptx`) is described there.
## How a layer feeds the engine (design finding, not yet implemented)
The engine's phases read their activations from a *staged image* — rows of consecutive e4m3 k —
not from NVIDIA's mma fragments. Two ways to get that image exist, and the cheap one is not the
obvious one:
1. Rewrite the layer's prologue natively (load the input planes, the norm, the patch geometry)
and have it stage the image directly. Correct, but it means re-deriving the layer's whole
input path, which is where most of the per-layer reverse engineering sits.
2. Cut NVIDIA's PTX *after* its own loading and norming, exactly as the working bodies do today,
and convert the fragments it hands over into the image **once per phase boundary** instead of
per mma. Each lane's fragment bytes are documented (row and k per byte, in the body's header);
writing them to shared memory at their (row, k) positions is a shared-memory scatter, which is
cheap, whereas ZLUDA's per-mma reassembly is what the 7,976 `ds_bpermute_b32` and 9,124
`v_cndmask_b32_e64` in the translated kernel are paying for.
Route 2 reuses the cut machinery and the ABI the existing bodies already have, and it is
the reason the engine does not need a prologue rewrite to be useful. What it does need, per layer,
is: the fragment-to-image staging at the cut boundary, the descriptor (offset/layout per phase),
the epilogue vocabulary (the engine has the clamped cubic activation; norm, softmax, column-table
add, patch expand, plane scatter, lane merge and surface scatter are still to come), and the
image-to-fragment conversion back at the exit, where the surviving PTX expects specific registers.
## Rejected direct-converter route
The removed `swin_gemm_zluda` experiment passed widened NVIDIA A/B fragments
straight into ZLUDA's native MMA helpers. Those types matched the helper ABI,
but the data did not: ZLUDA first forms the WMMA A operand from NVIDIA B through
`t_a_from_b`, and the WMMA B operand from NVIDIA A through `t_b_from_a`.
Skipping those cross-lane gathers is not bit-exact by construction.
The staged-image route uses independently checked fragment maps and the existing
`kop_image` / `swin_operand` / `k32_e4m3` primitives instead. The isolated phase-8
recipe in `swin_enc0_recipe.py` preserves the surrounding NVIDIA code. It stages
one token tile and the already-prefetched B fragments in shared memory, computes
the two weight tiles, and hands the rounded result back through lane-major shared
slots. Enc0's 3200-byte `input_tensor` is dead after prologue line 3938 and can be
reused; the final surface epilogue still needs the separate `input_noise` array.
The generic helper model and phase-8 oracle comparison establish these mappings,
not the unvalidated full-layer descriptors or the scalar full-body experiments.