Skip to content

feat(09): gate register prefetch via REG_STAGES, fit two CTAs per SM - #21

Merged
ArthurinRUC merged 9 commits into
mainfrom
feat/cutedsl-09-no-reg-prefetch
May 30, 2026
Merged

ArthurinRUC merged 9 commits into
mainfrom
feat/cutedsl-09-no-reg-prefetch

Conversation

@ArthurinRUC

Copy link
Copy Markdown
Owner

09-pipelining

  • Add a REG_STAGES constant gating the S2R register pipeline. REG_STAGES >= 2 hoists the smem→rmem load for k_block + 1 ahead of the k_block MMA (double-buffered register pipeline); REG_STAGES == 1 disables register prefetch so each k-tile loads its whole A/B fragment, then runs every MMA, leaving only the GMEM→SMEM→RF pipeline to overlap work.
  • Fold the C addend into the accumulator in the epilogue (acc starts at zero, acc += C after the matmul) instead of preloading it, so sC is never live during the mainloop.
  • Alias sC and sO over the drained A/B pipeline smem, dropping the footprint to A_pipe + B_pipe.
  • Set min_blocks_per_mp=2 so two CTAs co-reside per SM at ≤128 reg/thread.

08-dynamic-mma

  • Reuse the precomputed tCsC partition for the smem clear.

- 07: remove the explicit ``acc_dtype`` parameter from the kernel
  signature; the same value is reachable via ``tCrC.element_type`` after
  the fragment is allocated, matching the convention already used in
  06/08/09.
- 08 / 09: replace the universal S2R atoms with explicit
  ``LdMatrix8x8x16bOp(False, 4)`` for A/B and
  ``LdMatrix8x8x16bOp(False, 2)`` for C when C is 16-bit
  (else ``CopyUniversalOp``). This mirrors 07's choice and the
  SM75_U32x4_LDSM_N / SM75_U32x2_LDSM_N selection in the C++ kernels,
  surfacing the ldmatrix path at the source level instead of relying
  on the DSL backend to lower a universal atom.
- 08 / 09: keep the existing two-tensor predicate structure
  (``tApA`` M-only + ``tApA_first`` per-element M+K) rather than merging
  into one tensor; merging requires an extra refill between the residue
  tile and the mainloop that introduces a WAW dependency on the same
  predicate registers and blocks scheduling.
Replace the speculative comment on ``Copy_G2S_op`` selection with an
evidence-based explanation derived from PTX inspection of the failing
configuration. The previous comment claimed AutoVec lowered to a
specific cp.async variant and blamed alignment; the actual mechanism is
that ``AutoVectorizingCopyWithAssumedAlignment<128>`` is a 128-bit atom
applied to a per-element-strided source, which under ``copy_if`` (with
no recast specialization, unlike ``copy()``) unrolls to 8 wide loads at
2-byte offsets — 7 of which are misaligned and fault.

The comment now points at NVIDIA/cutlass#2354 for the upstream tracker
and references ``cutlass/examples/cute/tutorial/tiled_copy_if.cu`` as
the official workaround pattern.
For blocks that lie strictly inside both M and N tiles AND K is
tile-aligned, the per-element predicate fill + ``copy_if`` on the
residue prologue tile does redundant work — every M-bound and K-bound
check evaluates to true. Add a runtime check ``is_interior`` and branch
to a plain ``copy()`` for the prologue's stage 0 in that case, falling
back to the existing predicated path for boundary blocks and
K-residue shapes.

The fast path is intentionally limited to stage 0: later prologue
stages and the mainloop already carry a per-iter overshoot guard
(``if (ik == NTilesK) clear(tApA_g2s)``) which complicates short-
circuiting and offers little benefit on those code paths.
The single-stage dynamic-mma kernel has no cp.async pipeline to hide
memory latency, so it depends on occupancy for latency hiding. Left
unconstrained, the CuTeDSL register allocator maximises per-thread ILP
and lets the kernel grow to ~150 registers/thread (not a spill, just a
loose allocation), which caps it at one resident block per SM on
H100/H200.

Pass ``min_blocks_per_mp=2`` to the kernel launch (lowered to the NVVM
``minctasm`` launch-bound attribute). This tightens the per-thread
register budget so two blocks fit per SM; the kernel lands at 128
registers/thread with zero spill, and the higher occupancy gives the
warp scheduler enough eligible warps to cover the serial load/compute
latency. 2 is the largest spill-free setting here -- a higher value
would force register spills to local memory.

09-pipelining is intentionally left untouched: its multi-stage cp.async
pipeline already hides latency within each block, and forcing a second
resident block there only adds issue-slot contention.
The mainloop cleared the A/B shared-memory tiles (.fill(0) / clear())
on every K-tile iteration so that predicated-off cp.async slots read
as zero. That clear only needs to run once per block: the M-boundary
rows masked off by the M-only predicate are never written by any
K-tile, and the ik=0 K-residue columns are overwritten by every later
full K-tile. Hoisting it out of the loop drops 63 of 64 redundant
full-tile clears, trimming the per-iteration instruction count in the
hot loop.

Applied to both the CuTeDSL and C++ kernels. 09-pipelining already
clears its smem once in the prologue, so it is unaffected.
The CuTeDSL test harnesses built the C (addend) tensor at the input
dtype -- fp16 or bf16 -- for the fp32-accumulator dtype specs. The C++
bindings these tests mirror fix ComputeTypeC to float for the same
specs, so the CuTeDSL tests were compiling and validating a different
kernel configuration (16-bit C) than the C++ side (fp32 C). Build the
C template and per-shape C tensors as fp32 so the CuTeDSL tests
exercise the same operation as C++.

Also reseed the RNG before each per-spec sweep, matching the C++
drivers' per-block reseed, and update the sweep banners accordingly.
Build the C addend as fp32 for the fp32-accumulator specs of the
warpgroup-MMA and warp-specialization tests, matching the C++
ComputeTypeC=float bindings, as already done for the other examples.

That correction exposed a latent kernel bug: both kernels aliased the
epilogue sC/sD buffers onto the sB staged buffer alone, which only has
room for a 16-bit C tile. An fp32 C tile overflows sB and triggers an
illegal memory access. Re-base the alias from sB to sA -- sA and sB are
contiguous in SharedStorage, so the alias now spans the combined sA+sB
region, which holds the fp32 C tile. No extra shared memory is used.

Also reseed the RNG before each per-spec sweep, matching the C++
drivers' per-block reseed.
The __syncthreads() / cute.arch.sync_threads() that sat between the
A/B smem fill(0)/clear() and the subsequent predicated cp.async is
unnecessary. Each thread's fill and its following cp.async target the
same g2s partition slots, so single-thread program order already
orders the two writes to the same addresses. Cross-thread visibility
for the s2r readers is provided by the existing post-cp_async_wait
__syncthreads(), which also publishes the zero-fill stores from every
other thread.

Affected sites:
- 08-dynamic-mma/cutedsl_dynamic_mma.py (pre-K-loop A/B clear)
- 08-dynamic-mma/dynamic_mma.cu (pre-K-loop A/B clear)
- 09-pipelining/cutedsl_pipelining.py (stage-0 A/B clear)

09-pipelining/pipelining.cu and pipelining_no_reg_prefetch.cu already
omitted this sync.
09-pipelining:
- Add a REG_STAGES constant gating the S2R register pipeline. With
  REG_STAGES >= 2 the smem -> rmem load for k_block + 1 is hoisted ahead
  of the k_block MMA (double-buffered). REG_STAGES == 1 disables register
  prefetch: each k-tile loads its whole A/B fragment, then runs every MMA,
  leaving only the GMEM -> SMEM -> RF pipeline to overlap work.
- Fold the C addend into the accumulator in the epilogue (acc starts at
  zero, acc += C after the matmul) instead of preloading it, so sC is
  never live during the mainloop. The epilogue then aliases sC and sO over
  the drained A/B pipeline smem, dropping the footprint to A_pipe + B_pipe.
- Set min_blocks_per_mp=2 so two CTAs co-reside per SM at <=128 reg/thread.

08-dynamic-mma:
- Reuse the precomputed tCsC partition for the smem clear.
@ArthurinRUC
ArthurinRUC merged commit 57182a9 into main May 30, 2026
5 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.

1 participant