[BUG] SM90 cp.async + GMMA collectives: no fence.proxy.async between cp.async operand writes and wgmma reads

Open
#3,721 1 comment 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 4 days

@yunweili3 is already working on this.

Since Oct 8, 2026.

  • #3723 by @yunweili3 — open
  • #3724 by @yunweili3 — open
  • #3799 by @builtin-pb — open

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
42/100
Issue type
Bug
Clarity
Mostly clear
Activity status
Active
Tech stack
cpp
Domain
performance

Research direction

Start with include/cutlass/gemm/collective/sm90_mma_multistage_gmma_ss_warpspecialized.hpp, tracing the producer’s cp.async and clear() writes through barrier arrival to the consumer’s wgmma reads. Check the PTX ordering requirements cited in the report and compare the fence placement in sm90_wgmma_transpose.hpp and sm90_epilogue_tma_warpspecialized.hpp. Done means the operand writes are correctly ordered before wgmma reads, including residue tiles; the report notes the behavior has not been tested on hardware.

Written by the indexing model from the issue text.

Description

CUTLASS C++
Which component has the problem?

CUTLASS C++

Bug Report

Describe the bug

Summary. The SM90 cp.async + GMMA warp-specialized collective writes its shared-memory operands through the generic proxy and has wgmma.mma_async read them through the async proxy, with no fence.proxy.async on the path. Without the fence, PTX allows the wgmma to read operand bytes not yet visible to the async proxy, giving a silently wrong GEMM result; we have not run this on hardware, and current hardware may well make completed cp.async writes visible to the async proxy, though PTX does not state that. The fence can go on the producer side or the consumer side; see Options.

Cause

The collective include/cutlass/gemm/collective/sm90_mma_multistage_gmma_ss_warpspecialized.hpp (main 0b55a2f; unchanged since v4.4.0):

  • fills the shared-memory operands with non-bulk cp.async, and with clear() for residue K-tiles (lines 274 and 284);
  • publishes them with producer_commit(..., cutlass::arch::cpasync_barrier_arrive) (291, 310);
  • then the consumer does consumer_wait (411, 436, 460), warpgroup_arrive(), and wgmma.mma_async, which reads those operands.

PTX ISA 9.4, §9.7.17.7.1 (the wgmma.fence page): "An async proxy fence must be used to establish an ordering between prior writes to shared memory matrices and subsequent reads of the same matrices in a wgmma.mma_async instruction." Several facts mean nothing else on the path supplies that fence:

  • Non-bulk cp.async writes are generic-proxy operations (§9.7.10.28.3.1).
  • wgmma reads its shared operands through the async proxy (§9.7.17.4).
  • warpgroup_arrive() (wgmma.fence) orders register accesses.
  • The implicit completion fence of §9.7.10.28.2 covers cp{.reduce}.async.bulk only.
  • The cp.async.mbarrier.arrive arrive-on is ordered only with the thread's prior cp.async (§8.9.1.1), so it does not cover the clear() stores either.
Options
  • Producer side: each producer thread waits for its own cp.async to complete, executes cutlass::arch::fence_view_async_shared(), then arrives on the barrier. The fence is executed by the writing thread, as in CUTLASS's own sm90_wgmma_transpose.hpp (lines 233–243) and the TMA epilogue (sm90_epilogue_tma_warpspecialized.hpp:736–737). It gives up the cp.async.mbarrier.arrive overlap; we have not measured the cost.
  • Consumer side: one fence_view_async_shared() after each consumer_wait, before warpgroup_arrive(). This is cheaper, but it relies on a fence by a thread that only observed the writes. PTX says bi-directional proxy fences "take effect within a single thread" and that their effect "composes with other forms of synchronization according to the rules of the Memory Consistency Model" (§9.7.15.4); it does not state that this covers other threads' writes.
Scope
  • Not observed on hardware: we have not run this on hardware, and current hardware may well make completed cp.async writes visible to the async proxy. PTX does not state that, though.
  • For comparison, the Triton 3.5.0 and 3.7.0 PTX we compiled for an ordinary tl.dot matmul also had no fence between cp.async and wgmma; Triton 3.8.0 adds one.
Why there is no PR

Which placement is right is your call, given the producer side's cost and the consumer side's reliance on unstated composition. Tell us which you want and we'll write the PR.

Steps/Code to reproduce bug

We found this by checking compiled PTX against the PTX memory model with our own PTX-level checker. This instance (fp16, 64×64×64 tile, K = 320, one 384-thread CTA) was compiled with nvcc 12.6 for sm_90a:

#include "cutlass/cutlass.h"
#include "cutlass/gemm/collective/collective_builder.hpp"
#include "cutlass/epilogue/collective/collective_builder.hpp"
#include "cutlass/gemm/kernel/gemm_universal.hpp"
using namespace cute;
using BH12Tile = Shape<_64,_64,_64>;
using Cluster = Shape<_1,_1,_1>;
using H = cutlass::half_t;
using Main = cutlass::gemm::collective::CollectiveBuilder<
  cutlass::arch::Sm90, cutlass::arch::OpClassTensorOp,
  H,cutlass::layout::RowMajor,8,H,cutlass::layout::ColumnMajor,8,
  float,BH12Tile,Cluster,cutlass::gemm::collective::StageCount<2>,
  cutlass::gemm::KernelCpAsyncWarpSpecialized>::CollectiveOp;
using Epi = cutlass::epilogue::collective::CollectiveBuilder<
  cutlass::arch::Sm90,cutlass::arch::OpClassTensorOp,BH12Tile,Cluster,
  cutlass::epilogue::collective::EpilogueTileAuto,float,float,
  void,cutlass::layout::RowMajor,8,H,cutlass::layout::RowMajor,8,
  cutlass::epilogue::NoSmemWarpSpecialized>::CollectiveOp;
using Kernel = cutlass::gemm::kernel::GemmUniversal<Shape<int,int,int,int>,Main,Epi>;
extern "C" __global__ __launch_bounds__(Kernel::MaxThreadsPerBlock)
void bh12_cpasync_gmma(H const* A,H const* B,H* D) {
  __shared__ __align__(128) char smem[Kernel::SharedStorageSize];
  Kernel::Params p{};
  p.mode = cutlass::gemm::GemmUniversalMode::kGemm;
  p.problem_shape = make_shape(64,64,320,1);
  p.mainloop.ptr_A=A; p.mainloop.ptr_B=B;
  p.mainloop.dA = make_stride(int64_t(320),_1{},int64_t(0));
  p.mainloop.dB = make_stride(int64_t(320),_1{},int64_t(0));
  p.epilogue.thread = typename Epi::ThreadEpilogueOp::Params(1.0f);
  p.epilogue.ptr_D=D;
  p.epilogue.dD=make_stride(int64_t(64),_1{},int64_t(0));
  Kernel{}(p,smem);
}

Its PTX has 8 cp.async and 8 wgmma.mma_async instructions and no fence.proxy (grep -c fence.proxy gives 0). Our checker finds the 16-byte cp.async that writes shared bytes [0, 16) unordered with the warpgroup's wgmma read of those bytes (it attributes the read to thread 256). The clear() path is from source reading; this instance has no residue.

Expected behavior

A fence.proxy.async.shared::cta orders the operand writes before the wgmma reads.

Environment details

Source and PTX analysis; no runtime environment involved. Hopper (sm_90a).

Additional context

The _rs_ variant (sm90_mma_multistage_gmma_rs_warpspecialized.hpp) has the same pattern for the B operand, by source reading. There, A goes through ldmatrix into registers, and its TransposeB path already fences.

Dominant language
C++
Stars
10.7k
Forks
2.2k
Avg merge
3d 20h
Merged PRs (30d)
4

Getting set up

This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from NVIDIA/cutlass

All issues in NVIDIA/cutlass

Similar issues

More C++ issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.