Skip to content

[Fix] conv3d fp8: guard padded M rows in the epilogue store - #969

Merged
coderfeli merged 2 commits into
ROCm:mainfrom
jiacao-amd:jiacao/fix-conv3d-fp8-padded-row-store
Aug 6, 2026
Merged

[Fix] conv3d fp8: guard padded M rows in the epilogue store#969
coderfeli merged 2 commits into
ROCm:mainfrom
jiacao-amd:jiacao/fix-conv3d-fp8-padded-row-store

Conversation

@jiacao-amd

Copy link
Copy Markdown
Contributor

Problem

tests/kernels/test_conv3d_implicit_fp8.py::test_conv3d_fp8_vs_fp8cast_reference[1-96-4-8-9-96-1-1] fails intermittently on linux-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:

buffer_ops.buffer_store(out.to(fx.BFloat16), y_rsrc, off_ncdhw, mask=col_valid)

For n == 1 the output address fast path is off = col * dhw + row. The M rows added to pad npq up to a whole TILE_M are not out of bounds under that mapping — when dhw == npq they alias real elements of the next column:

col=0, row=288  ->  off = 288  ==  col=1, row=0

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) with padding=1: npq=288, TILE_M=128grid_m=3, 96 padded rows, and dhw == npq == 288, making 32.99% of the output clobberable. 3.075e-01 is 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:

failures worst rel_err
before 12/200 3.075e-01
after 0/200 1.11e-05

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 == 0 or dhw != 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

  • The BF16 kernel is unaffected: kernels/conv/conv3d_implicit.py already guards rows via _row_chk = npq % TILE_M != 0.
  • The FP8 split-K path already checks row < npq for its atomics ("Atomics ignore hardware OOB suppression; guard explicitly"). Only the non-atomic FP8 store was missing the equivalent check.

jiacao-amd added 2 commits August 5, 2026 17:31
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.
@jiacao-amd

Copy link
Copy Markdown
Contributor Author

Perf impact: none on aligned shapes, negligible on unaligned

The first version added the row compare unconditionally. Pushed a follow-up that gates it on row_chk = npq % TILE_M != 0, matching _row_chk in the BF16 kernel — shapes with no padded rows now generate no extra instruction at all.

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 (npq=768, row_chk=False) — dumped 21_final_isa.s before vs after:

IDENTICAL ISA  (1767 instructions both)

Byte-for-byte identical, so aligned shapes pay exactly nothing.

Unaligned shape (npq=288, row_chk=True) — the guard is active:

metric before after
v_mfma 168 168
buffer_store_short 32 32
ds_read 252 252
v_cmp 94 96
v_cndmask 81 81
s_barrier 21 21
VGPR / SGPR 120 / 60 120 / 60
spills 0 / 0 0 / 0

The cost is 2 v_cmp in the epilogue. The math loop is untouched, occupancy is unchanged (same VGPR/SGPR, no spills), and the remaining textual diff is scheduler reordering.

Correctness re-confirmed after the change: 0/200 failures, worst rel_err 1.11e-05. 33 conv3d tests pass.

@jiacao-amd
jiacao-amd marked this pull request as ready for review August 5, 2026 22:06
@coderfeli
coderfeli merged commit bc33f5e into ROCm:main Aug 6, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants