Skip to content

[Kernel] conv3d: parameterize tile size + add autotuner#820

Open
jiacao-amd wants to merge 13 commits into
ROCm:mainfrom
jiacao-amd:jiacao/conv3d-multi-tile-autotune-v2
Open

[Kernel] conv3d: parameterize tile size + add autotuner#820
jiacao-amd wants to merge 13 commits into
ROCm:mainfrom
jiacao-amd:jiacao/conv3d-multi-tile-autotune-v2

Conversation

@jiacao-amd

@jiacao-amd jiacao-amd commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Generalizes and hardens the two implicit-GEMM conv3d kernels (from #794):

  1. Parameterized tile + autotuner — the hardcoded 128x128 / 2x4-wave tile becomes a compile-time (TILE_M, TILE_N, WAVE_M, WAVE_N), with a per-shape tile autotuner. TILE_K and the filter (kt, kh, kw) stay compile-time constants (their im2col div/mod constant-folding is the perf source).
  2. 3x1x1 temporal fast path + vectorized epilogue — a bounds-check-free im2col decode for stride-1 same-shape Kt x 1 x 1 convs, and a vec4 store that packs a lane's 4 accumulators into one aligned store instead of 4 scalar stores.
  3. Async global->LDS copy + 4-stage pipeline — the main loop now uses async (buffer->LDS direct) loads with a deeper software pipeline, and the async path is extended to large (>2^31-element) tensors.
  4. >2^31-element support — rebased 64-bit addressing for both inputs (BIG_IN) and outputs (BIG_OUT) so large tensors no longer overflow the i32 buffer offset; split-K disabled when its fp32 output would overflow.
  5. Unified 1D / 2D / 3D entry pointconv3d_implicit (and the FP8 variant) is now a single public entry that dispatches by filter rank: true 3D runs directly, 2D/1D reshape to the degenerate 3D case and reuse the same kernel. The rank-specific bodies live in private _conv{1,2,3}d_impl helpers; the two split-K helpers are merged into one _resolve_splitk.

The kernels are renamed to drop the _8wave suffix: conv3d_implicit_8wave.py -> conv3d_implicit.py, conv3d_implicit_8wave_fp8.py -> conv3d_implicit_fp8.py, conv3d_autotune.py -> conv3d_implicit_autotune.py. All of the above is applied to both the BF16 and FP8 kernels.

Changes

File Description
conv3d_implicit.py (was _8wave.py) BF16: parameterized tile; async global->LDS copy + 4-stage pipeline; 3x1x1 temporal fast path; vec4 epilogue; BIG_IN/BIG_OUT 64-bit addressing; unified rank-dispatch entry; merged _resolve_splitk
conv3d_implicit_fp8.py (was _8wave_fp8.py) FP8: same generalizations + all features ported
conv3d_implicit_autotune.py (was conv3d_autotune.py) Manual per-shape tile autotuner (do_bench over a candidate list, cached in-memory + JSON)
tests/kernels/test_conv3d_implicit.py / _fp8.py Tile-sweep + autotune-cache tests; 2D/1D correctness tests

Usage

Default path unchanged -- (128, 128, 2, 4), zero behavior change. Opt in per call with autotune=True (or tile=(...)), or globally via FLYDSL_CONV3D_AUTOTUNE=1.

Results (MI355X gfx950, BF16)

Shape (N, C, K, D x H x W, filter) Act PR TFLOPS main TFLOPS MIOpen TFLOPS PR vs main PR vs MIOpen
2, 256, 256, 64x64x64, 3x3x3 0.3 GB 1015 661 725 1.54x 1.40x
1, 512, 512, 64x128x128, 3x3x3 1.1 GB 1224 697 781 1.76x 1.57x
1, 512, 512, 48x256x256, 1x3x3 3.2 GB 959 603 677 1.59x 1.42x
1, 512, 512, 48x256x256, 3x1x1 3.2 GB 617 428 469 1.44x 1.32x
1, 512, 512, 128x256x256, 1x3x3 8.6 GB 941 605 MIOpen err 1.56x --
1, 512, 512, 128x256x256, 3x1x1 8.6 GB 605 421 MIOpen err 1.44x --

Test plan

  • pytest tests/kernels/test_conv3d_implicit.py -v -- CDNA4 GPU
  • pytest tests/kernels/test_conv3d_implicit_fp8.py -v -- gfx95x only

jiacao-amd and others added 5 commits July 8, 2026 17:59
Generalize the BF16 and FP8 implicit-GEMM conv3d kernels from a single
hardcoded 128x128/2x4-wave shape to a compile-time-parameterized tile
(TILE_M, TILE_N, WAVE_M, WAVE_N) and add a manual per-shape autotuner.

- Replace the hand-scheduled 2x2-half BF16 pipeline (phase_*_prefetch +
  peeled prologue/main/epilogue, welded to QM_STEPS==2/QN_STEPS==1) with a
  generic double-buffered 2-stage loop over a flat acc[MI_M*MI_N] MFMA grid.
- Generalize gather/commit/read/store to range_constexpr over MI_M/MI_N and
  per-thread LDG counts; add tile legality asserts (MFMA divisibility, LDG
  vectorization, LDS budget).
- FP8: same flat-acc generalization, g2s halves -> row-block loop,
  Mfma16x16x128(MI_M, MI_N), flat_work_group_size from BLOCK_THREADS.
- New kernels/conv/conv3d_autotune.py: benchmark a small candidate list with
  do_bench, cache the best tile per shape (in-memory + JSON disk), reusing
  flydsl.autotune. Enabled via autotune=True or FLYDSL_CONV3D_AUTOTUNE=1;
  default path stays (128,128,2,4) for zero behavior change.
- TILE_K and filter size (kt/kh/kw) stay compile-time constants.
- Add tile-sweep + autotune-cache tests and scripts/bench_conv3d_tiles.py.
Buffer load offsets are 32-bit, so a single buffer resource can address
at most ~2 GB. The existing BIG_IN / BIG path rebased per image (nbase =
m_offset // dhw), which is a no-op for n=1 and fails entirely when a
single image exceeds 2^31 elements.

Fix: per-block time-plane rebase for bf16 and fp8:

* conv3d_implicit_8wave (bf16):
  - compile_transpose_ncdhw_ndhwc: BIG = n*c*s > 2^31 path adds per-block
    (nb, c0, s0) rebase so read/write residuals are tile-local (<900M <2^31).
  - compile_conv3d_implicit_8wave: BIG_IN path computes base_t (block's
    first time-plane, shifted down by pt) and rebases x_rsrc so gather_a
    residual (di*d + in_t - base_t)*h*w*c stays within int32 (<200M).

* conv3d_implicit_8wave_fp8:
  - compile_pack_activation_ncdhw_bf16_to_ndhwc_fp8: same BIG per-block
    (nb, c0, s0) rebase for read (BF16 input) and write (FP8 byte output).
  - compile_conv3d_implicit_8wave_fp8: BIG_IN path adds
    _make_fp8_buffer_tensor_from_addr helper and computes a rebased x_div
    so g2s_a_block's g_elem residual stays within int32.

Verified on gfx950 (MI355X):
  - 1x1024x240x160x90 conv (1,3,3) and (3,1,1): PASS (was crash/wrong)
  - 1x1024x120x160x90, 1x2048x60x80x45: no regression
  - All 14 bf16 unit tests pass; 6/7 fp8 tests pass (1 is pre-existing flaky)
For n==1 (no split-K), pack a lane's 4 contiguous, 8-byte-aligned output
values into one vec4 bf16 buffer_store instead of 4 scalar stores. The
epilogue VMEM-store was ~12% of kernel stalls on the short-reduction 3x1x1
path; this brings the 3x1x1 core (256x256x4x4 tile) from 3.18ms to 2.85ms
(854->951 TFLOPS), beating MIOpen (2.92ms). Gated on n==1, not split-K, and
dhw % 4 == 0; scalar fallback otherwise.

Add conv2d_implicit / conv1d_implicit as thin wrappers that reshape to the
degenerate 3D form (D=T=1, and H=R=1 for 1D) and reuse the same kernel and
fast paths -- zero kernel duplication.

Guard temporal_only_fast off when BIG_IN (>2^31 elems): the large-tensor
rebase uses the generic n/t/h/w decode, which the temporal fast decode does
not carry, so >2^31 3x1x1 inputs fall through to the correct generic path.

Tests: add conv2d/conv1d vs torch coverage (26 total, all pass).
Previously the temporal-only fast decode was disabled under BIG_IN (>2^31
elements), falling back to the generic path. Instead, rebase the temporal
fast-path address against the same x_base_elem origin the BIG_IN x_rsrc is
built from, so the residual element offset stays within int32. This keeps the
faster temporal decode for large 3x1x1 inputs while staying correct.

Verified vs torch.nn.functional.conv3d on a >2^31-element input
(1x640x240x160x90, 3x1x1): allclose, maxdiff 1.0 (matches the generic path).
Adds a large_shape regression test.
…upport

Port the BF16 conv3d optimizations to the FP8 kernel: vectorized (vec4) epilogue
store for n==1, the temporal-only 3x1x1 fast decode path, 64-bit output
addressing for >2^31-element outputs (BIG_OUT), and a split-K overflow guard.
Add conv2d_implicit_fp8 / conv1d_implicit_fp8 thin wrappers mirroring the BF16
API, plus 2D/1D FP8 correctness tests. Tidy the BF16 kernel comments.
@jiacao-amd
jiacao-amd force-pushed the jiacao/conv3d-multi-tile-autotune-v2 branch from 31e5866 to 1a740c3 Compare July 12, 2026 08:01
jiacao-amd and others added 4 commits July 12, 2026 03:03
Make conv3d_implicit_8wave(_fp8) the single public entry that dispatches
1D/2D/3D by filter rank; move the rank-specific bodies to private
_conv{1,2,3}d_impl(_fp8) helpers so the 2D/1D reshape wrappers no longer
recurse through the public name. Merge _choose_splitk into _resolve_splitk
(behavior verified bit-identical over the full input space). Also drop
stale explanatory comments and trailing whitespace.

Co-Authored-By: Claude <noreply@anthropic.com>
Port hgemm_splitk's async-copy deep-pipeline recipe into the bf16 conv3d
kernel. Replace the sync global->VGPR->LDS hop (gather_a/commit_a) with
raw_ptr_buffer_load_lds direct global->LDS DMA, freeing the A-tile VGPRs
and letting the software pipeline go from 2 to 4 stages.

Padding is masked by OOB-routing: the buffer resources are rebuilt with the
real num_records and invalid im2col taps are routed past the bounds so the
hardware bounds check writes 0 to LDS (the DMA path has no VGPR step for a
select mask). Gated by USE_ASYNC = not BIG_IN and X/W byte sizes <= 2^31;
BIG_IN and >2^31 tensors keep the proven sync path.

Measured on MI355X (gfx950), tile 256x256x4x4, controlled back-to-back A/B:
  c2048 1x3x3 M216k   842 -> 924 TFLOPS  (+9.7%)
  c2048 3x3x3 M216k   848 -> 967 TFLOPS  (+13.9%)
26/26 conv tests pass.
The buffer_load_lds voffset is unsigned 32-bit (verified), so no 64-bit
soffset split is needed to reach past the old 2^31-byte limit. Extend the
async path to two more regimes:

- not-BIG_IN tensors up to ~4.29GB: raw resource with real num_records,
  padding taps routed to an OOB sentinel just under 2^32.
- BIG_IN with n==1: reuse the existing per-block rebase (relative offsets
  <~0.3GB) and give the rebased resource a fixed 2GB num_records, between
  the max legal tap and the OOB sentinel, so padding still zeroes.

n>1 BIG_IN and >~4.29GB weights keep the sync fallback (di can jump a whole
batch past 2^32). All real video-VAE shapes now take the async path.

Measured on MI355X (gfx950), tile 256x256x4x4:
  512  [240,320,180] 1x3x3   728 -> 784 TFLOPS  (+7.7%,  14GB)
  1024 [240,160,90]  1x3x3   799 -> 887 TFLOPS  (+11.0%, 7GB)
  1024 [120,160,90]  1x3x3   804 -> 902 TFLOPS  (+12.2%, 3.5GB)
  2048 [60,80,45]    3x3x3   847 -> 977 TFLOPS  (+15.4%)
26/26 conv tests pass; large shapes verified correct (allclose).
@jiacao-amd
jiacao-amd force-pushed the jiacao/conv3d-multi-tile-autotune-v2 branch 3 times, most recently from e05dd6c to 5ad796b Compare July 15, 2026 00:29
Rename the conv modules so the 8-wave implementation detail is not in the name:
  conv3d_implicit_8wave      -> conv3d_implicit
  conv3d_implicit_8wave_fp8  -> conv3d_implicit_fp8
  conv3d_autotune            -> conv3d_implicit_autotune
  (+ their tests, internal symbols, and imports)

The bf16 conv3d kernel + test are synced to the jiacao/conv3d-gemm-port versions
(async-only pipeline, sync dead code removed, async naming dropped, PIPE_STAGES=4).
The fp8 kernel + test keep this PR's base content unchanged (file + symbol renamed
only, no logic diff); the fp8 rewrite lands in a separate PR.
@jiacao-amd
jiacao-amd force-pushed the jiacao/conv3d-multi-tile-autotune-v2 branch from 5ad796b to 5311d0f Compare July 15, 2026 00:34
@jiacao-amd
jiacao-amd marked this pull request as ready for review July 15, 2026 06:46
jiacao-amd and others added 3 commits July 16, 2026 00:29
…ernel

- wgm L2-swizzle: grouped-M block remap keeps the weight tile (B matrix) hot
  in L2 across wgm consecutive m-tiles, reducing TCC MISS by ~40% on large
  spatial shapes (measured: L2 hit rate 71.8% -> 83.5% with wgm=4)
- XOR bank-swizzle on LDS a_lds_off/b_lds_off: spreads MFMA ds_read across
  banks using row//2 as the block selector (~0% bank conflict)
- autotune extended to sweep (tile, wgm) pairs: WGM_VALUES=[1,4,8] combined
  with BF16_CANDIDATES; cache schema bumped to v3 (nested [tile, wgm] format)
- autotune dry-run before do_bench to exclude compile latency from timing

Performance (gfx950, BF16, vs image(3) reference FLY column):
  spatial 3x3: 1053-1148 TF/s (+45-67% vs reference, 2.0-2.1x MIOpen)
  temporal 1x3: 856-1154 TF/s (+43-84% vs reference, 2.3-3.5x MIOpen)

Co-Authored-By: Claude <noreply@anthropic.com>
Previously BIG_IN required n==1 because the x_rsrc rebase only handled a
single-sample base address. This lifts that restriction:

- BIG_IN + n==1: unchanged -- rebase x_rsrc to block's temporal origin,
  all per-lane g_off values fit int32 within BIG_IN_NR (2 GB).
- BIG_IN + n>1: x_rsrc stays at the tensor base; each DMA load in _load_a
  computes its row's sample index (n_idx) and rebases via
  create_buffer_resource_from_addr(x_base_addr + n_idx * X_SAMPLE_BYTES)
  so g_off stays int32-safe within a single sample (< 2 GB).

Also reverts the XOR bank-swizzle on a_lds_off/b_lds_off: async DMA
(raw_ptr_buffer_load_lds) writes LDS linearly so applying a read-side
swizzle produces wrong results. The wgm L2-swizzle is kept.

Fixes: shapes like (240,1024,162,92,1024) and (240,512,182,322,512)
that previously triggered "BIG_IN requires n==1" assertion.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
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.

1 participant