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.
11 KiB
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 akslotis the 16 e4m3 bytes of one k16 operand row.kslot_from16(u4v d)— the weight image's own conversion, done once in a_prepkernel, 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. WithD4R_TEX_FP8it 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
_prepkernel (once per launch, likeenc0_tail's): read the layer's weight image atarg040and write each phase's B operands askslots into a global buffer.- 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_from16from a staged image, or from the previous phase's requantised accumulator), runk32_e4m3per (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 withd4r_wmma_t_to_nv(4 bpermutes), stored to registers, requantized byd4r_cvt_rn_satfinite[_relu]_e4m3x2_f16x2, repacked into four words (%11374..%11377for the relu chain), loaded again by every consumer and converted byd4r_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_acollapses algebraically: withh = lane/16,r = lane&7,t = 2h, the WMMA B wordjof a lane ispack(cvt(f16x2{lo16(gather(A0, T_j)), lo16(gather(A1, T_j))}), ...), whereA0/A1 = (r, r+8)and register.xfor lanes < 16,(r+8, l)and.yotherwise. 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:
- 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.
- 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_b32and 9,124v_cndmask_b32_e64in 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.