[Fix] conv3d fp8: guard padded M rows in the epilogue store - #969
Conversation
The FP8 conv3d epilogue masked stores on the column bound only. With the
n==1 fast-path output addressing `off = col * dhw + row`, the M rows added
to pad npq up to a whole TILE_M are not out of bounds -- when dhw == npq
they alias real elements of the next column:
col=0, row=288 -> off = 288 == col=1, row=0
Those padded rows carry a zero accumulator (A is zeroed by the im2col
mask), so they race legitimate stores from another block and silently
zero part of the output. Whichever block writes last wins, which makes it
intermittent.
Shape (1,96,4,8,9,96) padding=1 has npq=288, TILE_M=128 -> 96 padded rows
and dhw == npq, so 32.99% of the output is clobberable. Measured 12/200
failures locally on gfx950, matching the intermittent CI failures of
tests/kernels/test_conv3d_implicit_fp8.py on linux-flydsl-mi355-1 (both
observed failures reported rel_err 3.075e-01, the deterministic value for
a fully-clobbered result).
The set of zeroed offsets was verified to be exactly the predicted
padded-row alias set. After the fix: 0/200 failures, worst rel_err back to
1.11e-05. tests/kernels/test_conv3d_implicit_fp8.py and
tests/kernels/test_conv3d_implicit.py pass (33 tests).
The BF16 kernel already guards this via `_row_chk` in
kernels/conv/conv3d_implicit.py; the split-K path of the FP8 kernel also
already checks `row < npq` for its atomics. Only the non-atomic FP8 store
was missing the check.
Gate the row check on `row_chk = npq % TILE_M != 0`, mirroring `_row_chk` in the BF16 kernel, so tile-aligned shapes (which have no padded rows) emit no extra compare. Verified by dumping final ISA for an aligned shape (npq=768): the generated assembly is byte-identical to the pre-fix kernel, so aligned shapes pay nothing. The unaligned shape (npq=288) gains 2 v_cmp for the guard with unchanged VGPR/SGPR (120/60), zero spills, and identical mfma, ds_read, buffer_store_short and s_barrier counts.
Perf impact: none on aligned shapes, negligible on unalignedThe first version added the row compare unconditionally. Pushed a follow-up that gates it on Verified at the ISA level rather than by wall-clock, because this box has all 8 GPUs pegged at 100% by other tenants and run-to-run variance on an identical binary (82–290 us on the same shape) swamped any real signal. Instruction-level comparison is the trustworthy measurement here. Aligned shape ( Byte-for-byte identical, so aligned shapes pay exactly nothing. Unaligned shape (
The cost is 2 Correctness re-confirmed after the change: 0/200 failures, worst rel_err 1.11e-05. 33 conv3d tests pass. |
Problem
tests/kernels/test_conv3d_implicit_fp8.py::test_conv3d_fp8_vs_fp8cast_reference[1-96-4-8-9-96-1-1]fails intermittently onlinux-flydsl-mi355-1, roughly once every few days. Two observed instances:Both reported the identical
rel_err 3.075e-01(0.3075297176837921), which ruled out a plain data race on the accumulators and pointed at a deterministic set of clobbered output elements.Root cause
The FP8 epilogue masks stores on the column bound only:
For
n == 1the output address fast path isoff = col * dhw + row. The M rows added to padnpqup to a wholeTILE_Mare not out of bounds under that mapping — whendhw == npqthey alias real elements of the next column:So they are perfectly legal addresses, not something the hardware OOB check suppresses. Those padded rows carry a zero accumulator (A is zeroed by the im2col mask), so they race legitimate stores issued by another block and silently zero part of the output. Whichever block writes last wins — hence the intermittency.
For shape
(1,96,4,8,9,96)withpadding=1:npq=288,TILE_M=128→grid_m=3, 96 padded rows, anddhw == npq == 288, making 32.99% of the output clobberable.3.075e-01is the deterministic error when every clobberable element loses the race.Fix
Add the row bound to the store mask, mirroring what the BF16 kernel and the FP8 split-K path already do.
Verification
On gfx950 (MI355X), same shape, 200 iterations:
The set of zeroed offsets was checked against the predicted padded-row alias set and matched exactly (9120 offsets,
all such offsets in predicted set: True).tests/kernels/test_conv3d_implicit_fp8.py+tests/kernels/test_conv3d_implicit.py: 33 passed.I swept all 7 parametrized shapes in the test file — only this one triggers the bug. The others either have
padrows == 0ordhw != npq, so the padded rows do not alias into the live region. The exposure is narrow, but when it hits it silently corrupts output rather than failing loudly.Notes
kernels/conv/conv3d_implicit.pyalready guards rows via_row_chk = npq % TILE_M != 0.row < npqfor its atomics ("Atomics ignore hardware OOB suppression; guard explicitly"). Only the non-atomic FP8 store was missing the equivalent check.