Skip to content

[CK_TILE] Use Unified Workspace for FMHA BWD - #6152

Merged
asleepzzz merged 12 commits into
developfrom
users/yiding12/fmha-bwd-workspace
May 7, 2026
Merged

[CK_TILE] Use Unified Workspace for FMHA BWD#6152
asleepzzz merged 12 commits into
developfrom
users/yiding12/fmha-bwd-workspace

Conversation

@DDEle

@DDEle DDEle commented Apr 3, 2026

Copy link
Copy Markdown
Contributor

Motivation

dq_acc is the intermediate accumulation buffer used in FMHA backward pass for deterministic mode. The current implementation allocates it as a single rectangular tensor:

shape = [shape_batch, nhead, nsplits, shape_seqlen_q, hdim_q]

where nsplits = launcher.dq_acc_splits (a single scalar), computed from max_seqlen_k and shared across all batches.

Problems

  1. Memory waste: In group mode, each batch may have a different seqlen_k, but nsplits is computed from max_seqlen_k, causing batches with shorter seqlen_k to over-allocate in the split dimension.

  2. Interface coupling: fmha_bwd_args exposes internal layout details such as stride_dq_acc, nhead_stride_dq_acc, batch_stride_dq_acc, and split_stride_dq_acc. The caller is responsible for computing these strides, but this logic belongs inside the kernel.

Goals

  1. Switch dq_acc buffer to a compact layout: batches are concatenated contiguously, with each batch occupying nhead * nsplits_i * seqq_i * hdim_q elements (nhead outermost).
  2. Remove all *_stride_dq_acc fields from fmha_bwd_args, replacing them with a single workspace_ptr; the kernel splits this internally using a fixed layout.
  3. fmha_bwd_launcher provides a workspace management interface: the caller only needs to allocate GPU memory and call prepare_workspace() — no layout computation required.
  4. Isolate kernel internals from the caller API: the dq_acc layout (nsplits, strides, buffer size) is determined entirely inside the launcher/kernel. Future changes to block shape, pipeline type, or persistent kernel strategy require no modifications to the caller's fmha_bwd_args or workspace allocation logic.

Technical Details

Interface Design

New fields in fmha_bwd_traits

struct fmha_bwd_traits
{
    int seqlen_q;
    int seqlen_k;
    int batch;
    int max_seqlen_q;
    int max_seqlen_k;
    int hdim_q;
    int hdim_v;
    int nhead_q;
    int nhead_k;
    std::string data_type;
    bool is_group_mode;
    mask_enum mask_type;
    bias_enum bias_type;
    bool has_dbias;
    bool has_dropout;
    bool is_store_randval;
    bool is_deterministic;
    // New: cumulative physical seqlen pointers for group mode (pass nullptr for batch mode).
    // seqstart_qs[i+1] - seqstart_qs[i] = physical seqlen_q of batch i (including padding); length = batch+1
    // seqstart_ks[i+1] - seqstart_ks[i] = physical seqlen_k of batch i (including padding); length = batch+1
    const int* seqstart_qs = nullptr;
    const int* seqstart_ks = nullptr;
};

fmha_bwd_launcher actual structure

struct fmha_bwd_launcher
{
    std::function<float(fmha_bwd_args, const ck_tile::stream_config&)> run{};

    // Total workspace size in bytes (host_ws_size + device_ws_size), computed by init().
    // Zero for kUseQrQtrDorPipeline (writes dq directly, no acc buffer needed).
    size_t workspace_size = 0;

    fmha_bwd_launcher(const fmha_bwd_traits&);

    // Copies auxiliary data (nsplits[], offsets[]) via hipMemcpy to the head of the GPU workspace,
    // and zeros the dq_acc buffer portion (tail of workspace) if required.
    // The memory pointed to by device_ws must be >= workspace_size bytes.
    std::function<void(void* device_ws)> prepare_workspace{};

    template <typename... Args>
    float operator()(Args&&... args) const { return run(std::forward<Args>(args)...); }

private:
    size_t host_ws_size   = 0;  // CPU workspace size (nsplits[] + offsets[] arrays)
    size_t device_ws_size = 0;  // GPU-only data size (dq_acc buffer)
    std::unique_ptr<char[]> ws_host;  // host-side workspace buffer

public:
    template <typename T0, typename T1, typename T2, typename Arch>
    void init(const fmha_bwd_traits& traits);
};

The init<>() template method (invoked by codegen dispatch branches as this->init<...>(t)) is responsible for:

  1. Setting the run lambda
  2. Calling FmhaBwdDQDKDVKernel::GetWorkspaceHostSize(batch) to obtain host_ws_size
  3. Allocating ws_host (host memory)
  4. Calling FmhaBwdDQDKDVKernel::PrepareWorkspaceHost(ws_host.get(), ...) to fill nsplits/offsets; return value is device_ws_size
  5. workspace_size = host_ws_size + device_ws_size
  6. Setting the prepare_workspace lambda (captures this, calls PrepareWorkspaceDevice)

When no kernel matches the given traits, both run and prepare_workspace are initialized to default lambdas that print a warning to std::cerr and return gracefully (no exception).

Workspace overall layout

The workspace is managed by FmhaBwdWorkspaceManager and consists of two segments:

Offset 0 (CPU-prepared segment, host_ws_size bytes; also hipMemcpy'd to the head of GPU workspace):
  index_t nsplits[batch or 1]       — per-batch nsplits array
                                      group mode: batch elements
                                      batch mode / non-deterministic: 1 element
  [group mode only] long_index_t dq_acc_offsets[batch+1]
                                    — per-batch element offset (inclusive prefix sum)
                                      offsets[0]=0, offsets[i+1] = offsets[i] + nhead*nsplits_i*seqq_i*hdim_q

Offset host_ws_size (device data segment, device_ws_size bytes):
  AccDataType dq_acc[total_elements] — compact dq_acc buffer (zeroed if required)
                                       total_elements = sum_i(nhead * nsplits_i * seqq_i * hdim_q)
                                       layout within each batch: [nhead, nsplits_i, seqq_i, hdim_q]
                                       note: seqq_i uses the physical length (including padding)

Alignment constant (ALIGNMENT = 16):

nsplits_size  = align_up(sizeof(index_t) * N, 16)          // N = batch (group) or 1 (batch/non-det)
offsets_size  = align_up(sizeof(long_index_t) * (batch+1), 16)  // group mode only
host_ws_size  = nsplits_size + offsets_size
dq_acc_offset = host_ws_size  // GetDqAccDataOffset(batch)

Key benefits:

  • The kernel reads nsplits/offsets directly from the workspace head — no device-side recomputation.
  • FmhaBwdConvertQGradKernel is completely decoupled from the pipeline block shape (kN0): nsplits is read from nsplits_ptr, kN0 is no longer a template parameter, and multiple dq_dk_dv tiles with different F_bn0 values now share a single convert_dq kernel instance (under receipt 1/2, deterministic convert_dq kernel count drops from ~300 to 60).
  • nsplits/offsets are computed on the host and transferred in one hipMemcpy; the dq_acc buffer follows immediately, at the offset given by GetDqAccDataOffset.

Workspace size by scenario

Scenario workspace_size Notes
kUseQrQtrDorPipeline (any mode) 0 Writes dq directly; no acc buffer; PrepareWorkspaceHost returns 0
Non-deterministic + batch mode > 0 nsplits[1]=1; dq_acc used for atomic add; workspace_size = host_ws_size + batch*nhead*seqlen_q*hdim_q*ebytes
Non-deterministic + group mode > 0 nsplits[1]=1; dq_acc contiguous layout; workspace_size = host_ws_size + nhead*seqstart_qs[batch]*hdim_q*ebytes
Deterministic + group mode > 0 nsplits[batch], offsets[batch+1], compact dq_acc; nsplits_i computed independently per batch
Deterministic + batch mode persistent > 0 nsplits[1] (uniform across batches); dq_acc batch*nhead*nsplits*seqlen_q*hdim_q

NeedsZeroDqAcc (determines whether PrepareWorkspaceDevice calls hipMemset):

  • Persistent kernel (deterministic batch mode) or non-deterministic: must zero (atomic add requires zero initialization)
  • Deterministic group mode + no mask: no zeroing needed (every tile writes its full region)
  • Deterministic + with mask: must zero (some blocks are skipped, leaving uninitialized tiles that would contribute to the reduction)

Caller usage

// 1. Create launcher (traits include seqstart_qs/ks pointers; workspace_size is computed during construction)
fmha_bwd_launcher launcher(fmha_traits);

// 2. Read launcher.workspace_size directly
const auto ws_size = launcher.workspace_size;

// 3. Allocate a single GPU workspace
ck_tile::DeviceMem ws_buf(ws_size);

// 4. Copy nsplits/offsets to GPU head and zero dq_acc if required
launcher.prepare_workspace(ws_buf.GetDeviceBuffer());

// 5. Build args with a single workspace pointer; the kernel splits it internally
fmha_bwd_args args{
    ...,
    ws_size > 0 ? ws_buf.GetDeviceBuffer() : nullptr,  // workspace_ptr
};
launcher(args, stream_config);

Key Code Structure

FmhaBwdWorkspaceManager (fmha_bwd_kernel.hpp, new class)

template <typename AccDataType, bool kIsGroupMode, bool kIsDeterministic>
struct FmhaBwdWorkspaceManager
{
    static constexpr size_t ALIGNMENT = 16;

    // CPU workspace (nsplits + offsets) sizes
    static size_t GetDqAccSplitsSize(int batch);   // align_up(sizeof(index_t)*N, 16)
    static size_t GetDqAccOffsetsSize(int batch);  // group mode only: align_up(sizeof(long_index_t)*(batch+1), 16)
    static size_t GetWorkspaceHostSize(int batch);  // = SplitsSize + OffsetsSize

    // Starting offset of dq_acc data within the full workspace (= host_ws_size)
    static size_t GetDqAccDataOffset(int batch);   // = GetWorkspaceHostSize(batch)

    // Fills nsplits/offsets in the CPU workspace; returns device_ws_size (dq_acc buffer bytes)
    template <bool kUseQrQtrDorPipeline, index_t kN0>
    static size_t PrepareWorkspaceHost(void* cpu_ws, index_t batch_size, index_t hdim_q,
                                       index_t nhead_q, index_t seqlen_q, index_t seqlen_k,
                                       const index_t* seqstart_qs, const index_t* seqstart_ks);

    // hipMemcpy's cpu_ws to device_ws head; hipMemset's the dq_acc portion to 0 if required
    template <bool kUseQrQtrDorPipeline, bool kHasMask>
    static void PrepareWorkspaceDevice(void* device_ws, const void* host_ws,
                                       size_t device_ws_size, size_t host_ws_size);
};

workspace_ptr parsing (inside the kernel)

The kernel parses three address regions from kargs.workspace_ptr:

Group mode (FmhaBwdDQDKDVKernel::MakeKargs):

const uint8_t* ws = reinterpret_cast<uint8_t*>(workspace_ptr);
// dq_acc_ptr (stored in FmhaBwdCommonKargs)
ws + WorkspaceManager::GetDqAccDataOffset(batch)
// dq_acc_batch_offset_ptr (FmhaBwdGroupModeKargs field)
reinterpret_cast<const long_index_t*>(ws + WorkspaceManager::GetDqAccOffsetsOffset(batch))

Batch mode:

ws + WorkspaceManager::GetDqAccDataOffset(batch)  // dq_acc_ptr
// No offsets pointer; batch offset is computed inside run_() from nsplits

FmhaBwdConvertQGradKernel follows the same pattern:

  • Group mode: extracts dq_acc_ptr, dq_acc_batch_offset_ptr, and nsplits_ptr (GetDqAccSplitsOffset(batch)) from workspace
  • Batch mode: reads nsplits from nsplits_ptr[0]; batch offset computed internally

Addressing in run_() (group mode)

// Per-batch processing:
const long_index_t batch_offset_dq_acc = kargs.dq_acc_batch_offset_ptr[i_batch];
// seqq_i (physical length) derived from seqstart_q_ptr
const index_t seqq_i = kargs.seqstart_q_ptr[i_batch+1] - kargs.seqstart_q_ptr[i_batch];
// nsplits_i read from nsplits_ptr (convert_dq kernel) or from GetDqAccSplits
const long_index_t split_stride_i = static_cast<long_index_t>(seqq_i) * kargs.hdim_q;
const long_index_t nhead_stride_i = static_cast<long_index_t>(nsplits_i) * split_stride_i;
// Final address:
dq_acc_base + batch_offset_dq_acc + i_nhead * nhead_stride_i + i_split * split_stride_i

nsplits computation (PrepareWorkspaceHost)

PrepareWorkspaceHost is a template method of FmhaBwdWorkspaceManager that still takes kN0 as a template parameter (from BlockFmhaShape::kN0 of the dq_dk_dv pipeline). However, this parameter is only used inside this host-side function to compute nsplits — it is no longer passed into the convert_dq kernel.

Mode nsplits computation
kUseQrQtrDorPipeline Writes dq directly; nsplits[0]=0; returns device_ws_size=0
Non-deterministic nsplits[0]=1; dq_acc used for atomic add
Deterministic + group mode ceil((seqstart_ks[i+1]-seqstart_ks[i]) / kN0) computed per batch
Deterministic + batch mode persistent Same logic as the original GetDqAccSplits (dqdqkdv_workers based)

Removing kN0 dependency from FmhaBwdConvertQGradKernel

FmhaBwdConvertQGradKernel previously required kN0 as a template parameter (via BlockFmhaBwdConvertQGradPipelineProblem) for two purposes:

  1. In batch mode operator(): self-computing nsplits = ceil(seqlen_k / kN0)
  2. The b{kM0}x{kN0} component of the kernel name string

Both have been removed in this refactor:

  • Batch mode: now reads kargs.nsplits_ptr[0] directly (guarded by if constexpr(kIsDeterministic) to avoid accessing a non-existent field in non-deterministic instances)
  • Kernel name: simplified to b{kM0}, no longer includes kN0
  • Template parameters: BlockFmhaBwdConvertQGradPipelineProblem drops the kN0_ parameter; fmha_bwd_convert_dq_traits_ drops the kN0 parameter; F_bn0/convert_dq_bn0 fields removed from codegen

Effect: all dq_dk_dv tiles sharing the same (hdim, dtype, mode, pad, deterministic) combination — regardless of F_bn0 value (16/64/128/192/256) — now share a single convert_dq kernel instance.


Test Plan

Test Result

Submission Checklist

@DDEle
DDEle force-pushed the users/yiding12/fmha-bwd-workspace branch from 4cb0424 to 029ed27 Compare April 3, 2026 10:49

@poyenc poyenc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Nice refactoring — the unified workspace approach is a clear win for API cleanliness, memory efficiency in group mode, and binary size reduction (convert_dq kernel deduplication). A few issues to address:


Bug

Batch-mode deterministic (persistent) offset mismatch in FmhaBwdDQDKDVKernel::run_()

Two places in run_() use integer_divide_ceil(seqlen_k, kN0) (i.e. jobs_per_head) as nsplits to compute offsets into dq_acc:

// batch offset
batch_offset_dq_acc = i_batch * nhead_q *
    integer_divide_ceil(seqlen_k, kN0) * seqlen_q * hdim_q;

// nhead+split offset
const auto nsplits = integer_divide_ceil(kargs.seqlen_k, FmhaPipeline::kN0);
return batch_offset_dq_acc + (i_nhead_ * nsplits + i_split) * split_stride;

But PrepareWorkspaceHost sizes the workspace using the persistent-formula nsplits (nsplits[0]), which is typically smaller than jobs_per_head. For example with batch=4, nhead=32, seqlen_k=2048, kN0=64, num_cus=120: jobs_per_head=32 but nsplits[0]=1. The kernel would use a stride 32x larger than what was allocated.

The convert_dq kernel handles this correctly by reading kargs.nsplits_ptr[0] from the workspace. The DQ-DK-DV kernel should do the same.


Latent UB

  1. PrepareWorkspaceHost QrQtrDor path writes to unallocated memory. When kUseQrQtrDorPipeline=true, GetDqAccSplitsSize returns 0 so ws_host is never allocated, but the function unconditionally writes nsplits[0] = 0. Today unreachable (guarded by if(host_ws_size > 0) in init()), but the function itself is unsafe if called directly. Consider an early return before touching nsplits.

  2. Null pointer arithmetic in group-mode QrQtrDor MakeKargs. dq_acc_batch_offset_ptr is always initialized from ws + GetDqAccOffsetsOffset(batch). When QrQtrDor, workspace_ptr is nullptr, so ws = nullptr. The arithmetic nullptr + 0 is UB in C++ even though the pointer is never dereferenced. Gate with if constexpr(!kUseQrQtrDorPipeline).


Design

  1. prepare_workspace lambda captures this. The move/copy deletions prevent dangling today, but the design is fragile — future changes allowing move/copy would silently break. Capturing the needed state by value (ws_host.get(), device_ws_size, host_ws_size) would be self-contained and let the class become movable if needed.

  2. init() is public. It's only called by codegen dispatch inside the constructor. Being public, a caller could accidentally reinitialize the launcher. Consider making it private.


Cleanup

  1. Dead kN0 in block_fmha_bwd_convert_dq.hpp:18. static constexpr index_t kN0 = Problem::kN0;Problem no longer has kN0. Compiles because the class template member is never ODR-used, but should be removed.

  2. Unnecessary const_cast in PrepareWorkspaceDevice. hipMemcpy source is const void* — no cast needed.

  3. GetDqAccOffsetsSize lacks kUseQrQtrDorPipeline template guard. GetDqAccSplitsSize has it but GetDqAccOffsetsSize does not. Results happen to be consistent, but the asymmetry is confusing.


Minor

  1. fmha_bwd_traits lifetime requirement. The new seqstart_qs/seqstart_ks raw pointers must remain valid through launcher construction. Worth a comment documenting this.

  2. Lost diagnostic info. Old runner output: workspace:42MiB|8splits. New: workspace:42MiB. The nsplits info was useful for debugging deterministic mode.

@DDEle

DDEle commented Apr 8, 2026

Copy link
Copy Markdown
Contributor Author

@poyenc,

Comments 1/2/3/5/6/7/8/9: Fixed, thanks. For comment 8: GetDqAccOffsetsSize is kept without a template parameter — GetWorkspaceHostSize<true> now returns 0 early, so GetDqAccOffsetsSize is never reached on the QrQtrDor path.

Comment 4: Capturing by raw pointer creates the same lifetime dependency, just less explicitly. Since move/copy are deleted the lambda cannot dangle, so I'd prefer to keep this capture as the clearer expression of ownership.

Comment 10: In group mode each batch now has its own nsplits_i, so there's no single scalar to print. Left it out intentionally.

@poyenc

poyenc commented Apr 8, 2026

Copy link
Copy Markdown
Contributor

All review comments addressed — the critical bug (persistent offset mismatch) and UB issues are fixed, and the declined items (this-capture, nsplits diagnostic) are reasonable. LGTM once marked ready for review.

@shumway

shumway commented Apr 16, 2026

Copy link
Copy Markdown
Contributor

This PR may further break the device/host code separation required for integration in rocm-libraries. We need to review this carefully. @afagaj @vidyasagar-amd @dpeabody-amd @aosewski

@shumway

shumway commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

This PR may further break the device/host code separation required for integration in rocm-libraries. We need to review this carefully. @afagaj @vidyasagar-amd @dpeabody-amd @aosewski

I really like this design. We’ll need to think carefully about how to include it rock-ck, the dispatcher, and kpack/rfc0008/host-device separation. It’s a great example of the kinds of generalization for kernel launches we’ll need to support.

@aosewski

Copy link
Copy Markdown
Contributor

Thanks @DDEle — the direction here looks right for the host/device separation we need on the rocm-libraries integration side. A few observations and one concrete ask:

What works well for us

  • Collapsing dq_acc_ptr + the four *_stride_dq_acc fields into a single opaque workspace_ptr is exactly the ABI shape a host-only driver wants — no layout knowledge leaks past the boundary.
  • Having the kernel read its own nsplits[] / per-batch offsets from the head of the workspace makes the device side self-describing given a correctly laid-out buffer. That removes a real coupling from kargs.
  • Per-batch nsplits_i in group mode is a meaningful memory + correctness win independent of integration concerns.
  • Dropping kN0 from FmhaBwdConvertQGradKernel collapses many precompiled instances into one — strictly good for a binary-distribution path.

Where we hit friction
The remaining coupling is in FmhaBwdWorkspaceManager<Pipeline, ConvertDqPipeline>::PrepareWorkspaceHost(...) and fmha_bwd_launcher::init<T0,T1,T2,Arch>(traits). Both are pure host code by body (CPU arithmetic, plus hipMemcpy/hipMemset driven from the host), but they are templated on device-side pipeline types just to read Pipeline::kN0, the persistent/deterministic flavor, and the accumulator element size. For a multi-arch host TU that does not (and should not) instantiate CK Tile pipelines, that's the awkward part.

Concrete ask
Would you be open to splitting FmhaBwdWorkspaceManager into:

  1. a small POD FmhaBwdWorkspaceSpec { kN0; is_persistent; is_deterministic; is_group_mode; acc_element_size; needs_zero_dq_acc; }
  2. a non-template host function prepare_workspace_host(const FmhaBwdWorkspaceSpec&, …) containing today's body
  3. a template factory make_fmha_bwd_workspace_spec<Pipeline, ConvertDqPipeline>() that lives in the device-side template header and produces the POD

The in-tree example launcher uses the factory at compile time and is unchanged in behavior. A host-only driver fills the POD from operator metadata. The kernel, the workspace memory layout, and the codegen branches stay as you have them.

The same POD is also the natural extension point on our side for expressing the workspace contract under a Signature/Algorithm split — which we'd otherwise have to bolt on adapter-side anyway.

Minor: worth a one-line comment in fmha_bwd_traits documenting the seqstart_qs/ks lifetime requirement (must remain valid through launcher construction). poyenc already noted this as a "minor"; it'd help downstream callers.

DDEle added 3 commits April 22, 2026 02:07
Wrap fmha_bwd_launcher constructor with std::chrono and prepare_workspace
with ck_tile::gpu_timer; append "init:Xms, prws:Yms" to the benchmark
header line. Also reorder launcher construction to occur after device
buffer allocation so its timing is isolated.
Mark the FmhaBwdWorkspaceManager size/offset accessors as CK_TILE_HOST
(they are only invoked from host-side workspace setup), and pad
GetWorkspaceHostSize up to a 4K boundary so the GPU dq_acc buffer always
starts on a page-aligned offset.
@DDEle

DDEle commented Apr 23, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @aosewski — glad the direction works for the integration side.

On the FmhaBwdWorkspaceSpec proposal

Agree with the direction. The POD + factory + non-template host function split is the right shape, both for an out-of-tree host-only driver and as the extension point for the Signature/Algorithm split.

I'd like to defer it to a follow-up PR though. I prototyped locally and the surface is larger than the workspace API alone — kargs assembly wants the same treatment (it currently pulls in pipeline types for stride / padding / group-mode offset computation), and once that's pulled out the change spreads across kernel template, codegen, launcher, and the new host TU. One thing I'd want to be careful about in the layout: host-side workspace prep and kernel-side workspace consumption are tightly coupled by contract, so from a kernel developer's perspective keeping them at nearby paths is more intuitive than splitting purely along host-only / device-only TU boundaries — worth thinking through how to satisfy both the integration requirement and code locality. I'd rather keep this PR scoped to the unified-workspace ABI and land the host/device split cleanly in a follow-up where we can agree on the spec boundary and file layout up front.

Re minor (seqstart_qs/ks lifetime)

Already documented at fmha_bwd.hpp:542-544 since bf364d7 — likely an earlier diff snapshot.

DDEle added 2 commits April 27, 2026 01:54
In group mode the dq_acc workspace layout uses physical (padded)
seqlen_q for the per-nhead stride (see FmhaBwdWorkspaceManager doc;
also matches FmhaBwdConvertQGradKernel reads). The unified-workspace
refactor inlined this stride as kargs.seqlen_q, which is the LOGICAL
length when seqlen_q_ptr is provided. The result: main kernel writes
batch i nhead>0 dq_acc at offsets that the convert kernel never reads,
so dQ ends up zero for those positions.

Hoist physical_seqlen_q to the outer scope and use it for both the
non-deterministic and deterministic stride computations in the
dq_dram_window lambda. Batch mode is unaffected since kargs.seqlen_q
already equals the physical length there.

Fixes 135 padding-related failures in test_ck_tile_fmha_bwd_fp16
(BasicQPadding / MultiBatchPadding / PaddingWithMask / QKVPadding /
VariedPaddingRatios / ZeroLengthPadding / Deterministic /
ElementwiseBias). Verified locally: full suite 672 PASSED / 0 FAILED.
SGPR usage drops by 1; VGPR/AGPR/spill/occupancy unchanged.
@DDEle
DDEle marked this pull request as ready for review April 30, 2026 01:14
@DDEle
DDEle requested a review from a team as a code owner April 30, 2026 01:14
@asleepzzz
asleepzzz enabled auto-merge (squash) May 7, 2026 01:53
@asleepzzz
asleepzzz merged commit 36b016a into develop May 7, 2026
36 checks passed
@asleepzzz
asleepzzz deleted the users/yiding12/fmha-bwd-workspace branch May 7, 2026 02:22
assistant-librarian Bot pushed a commit to ROCm/composable_kernel that referenced this pull request May 7, 2026
[CK_TILE] Use Unified Workspace for FMHA BWD
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

## Motivation
`dq_acc` is the intermediate accumulation buffer used in FMHA backward
pass for deterministic mode. The current implementation allocates it as
a **single rectangular tensor**:

```
shape = [shape_batch, nhead, nsplits, shape_seqlen_q, hdim_q]
```

where `nsplits = launcher.dq_acc_splits` (a single scalar), computed
from `max_seqlen_k` and shared across all batches.

### Problems

1. **Memory waste**: In group mode, each batch may have a different
`seqlen_k`, but `nsplits` is computed from `max_seqlen_k`, causing
batches with shorter `seqlen_k` to over-allocate in the split dimension.

2. **Interface coupling**: `fmha_bwd_args` exposes internal layout
details such as `stride_dq_acc`, `nhead_stride_dq_acc`,
`batch_stride_dq_acc`, and `split_stride_dq_acc`. The caller is
responsible for computing these strides, but this logic belongs inside
the kernel.

### Goals

1. Switch `dq_acc` buffer to a **compact layout**: batches are
concatenated contiguously, with each batch occupying `nhead * nsplits_i
* seqq_i * hdim_q` elements (nhead outermost).
2. **Remove all `*_stride_dq_acc` fields** from `fmha_bwd_args`,
replacing them with a single `workspace_ptr`; the kernel splits this
internally using a fixed layout.
4. `fmha_bwd_launcher` provides a **workspace management interface**:
the caller only needs to allocate GPU memory and call
`prepare_workspace()` — no layout computation required.
5. **Isolate kernel internals from the caller API**: the `dq_acc` layout
(nsplits, strides, buffer size) is determined entirely inside the
launcher/kernel. Future changes to block shape, pipeline type, or
persistent kernel strategy require no modifications to the caller's
`fmha_bwd_args` or workspace allocation logic.

## Technical Details

### Interface Design

#### New fields in `fmha_bwd_traits`

```cpp
struct fmha_bwd_traits
{
    int seqlen_q;
    int seqlen_k;
    int batch;
    int max_seqlen_q;
    int max_seqlen_k;
    int hdim_q;
    int hdim_v;
    int nhead_q;
    int nhead_k;
    std::string data_type;
    bool is_group_mode;
    mask_enum mask_type;
    bias_enum bias_type;
    bool has_dbias;
    bool has_dropout;
    bool is_store_randval;
    bool is_deterministic;
    // New: cumulative physical seqlen pointers for group mode (pass nullptr for batch mode).
    // seqstart_qs[i+1] - seqstart_qs[i] = physical seqlen_q of batch i (including padding); length = batch+1
    // seqstart_ks[i+1] - seqstart_ks[i] = physical seqlen_k of batch i (including padding); length = batch+1
    const int* seqstart_qs = nullptr;
    const int* seqstart_ks = nullptr;
};
```

#### `fmha_bwd_launcher` actual structure

```cpp
struct fmha_bwd_launcher
{
    std::function<float(fmha_bwd_args, const ck_tile::stream_config&)> run{};

    // Total workspace size in bytes (host_ws_size + device_ws_size), computed by init().
    // Zero for kUseQrQtrDorPipeline (writes dq directly, no acc buffer needed).
    size_t workspace_size = 0;

    fmha_bwd_launcher(const fmha_bwd_traits&);

    // Copies auxiliary data (nsplits[], offsets[]) via hipMemcpy to the head of the GPU workspace,
    // and zeros the dq_acc buffer portion (tail of workspace) if required.
    // The memory pointed to by device_ws must be >= workspace_size bytes.
    std::function<void(void* device_ws)> prepare_workspace{};

    template <typename... Args>
    float operator()(Args&&... args) const { return run(std::forward<Args>(args)...); }

private:
    size_t host_ws_size   = 0;  // CPU workspace size (nsplits[] + offsets[] arrays)
    size_t device_ws_size = 0;  // GPU-only data size (dq_acc buffer)
    std::unique_ptr<char[]> ws_host;  // host-side workspace buffer

public:
    template <typename T0, typename T1, typename T2, typename Arch>
    void init(const fmha_bwd_traits& traits);
};
```

The `init<>()` template method (invoked by codegen dispatch branches as
`this->init<...>(t)`) is responsible for:
1. Setting the `run` lambda
2. Calling `FmhaBwdDQDKDVKernel::GetWorkspaceHostSize(batch)` to obtain
`host_ws_size`
3. Allocating `ws_host` (host memory)
4. Calling `FmhaBwdDQDKDVKernel::PrepareWorkspaceHost(ws_host.get(),
...)` to fill nsplits/offsets; return value is `device_ws_size`
5. `workspace_size = host_ws_size + device_ws_size`
6. Setting the `prepare_workspace` lambda (captures `this`, calls
`PrepareWorkspaceDevice`)

When no kernel matches the given traits, both `run` and
`prepare_workspace` are initialized to default lambdas that print a
warning to `std::cerr` and return gracefully (no exception).

#### Workspace overall layout

The workspace is managed by `FmhaBwdWorkspaceManager` and consists of
two segments:

```
Offset 0 (CPU-prepared segment, host_ws_size bytes; also hipMemcpy'd to the head of GPU workspace):
  index_t nsplits[batch or 1]       — per-batch nsplits array
                                      group mode: batch elements
                                      batch mode / non-deterministic: 1 element
  [group mode only] long_index_t dq_acc_offsets[batch+1]
                                    — per-batch element offset (inclusive prefix sum)
                                      offsets[0]=0, offsets[i+1] = offsets[i] + nhead*nsplits_i*seqq_i*hdim_q

Offset host_ws_size (device data segment, device_ws_size bytes):
  AccDataType dq_acc[total_elements] — compact dq_acc buffer (zeroed if required)
                                       total_elements = sum_i(nhead * nsplits_i * seqq_i * hdim_q)
                                       layout within each batch: [nhead, nsplits_i, seqq_i, hdim_q]
                                       note: seqq_i uses the physical length (including padding)
```

Alignment constant (`ALIGNMENT = 16`):
```
nsplits_size  = align_up(sizeof(index_t) * N, 16)          // N = batch (group) or 1 (batch/non-det)
offsets_size  = align_up(sizeof(long_index_t) * (batch+1), 16)  // group mode only
host_ws_size  = nsplits_size + offsets_size
dq_acc_offset = host_ws_size  // GetDqAccDataOffset(batch)
```

**Key benefits**:
- The kernel reads nsplits/offsets directly from the workspace head — no
device-side recomputation.
- `FmhaBwdConvertQGradKernel` is completely decoupled from the pipeline
block shape (`kN0`): nsplits is read from `nsplits_ptr`, `kN0` is no
longer a template parameter, and multiple dq_dk_dv tiles with different
`F_bn0` values now share a single convert_dq kernel instance (under
receipt 1/2, deterministic convert_dq kernel count drops from ~300 to
60).
- nsplits/offsets are computed on the host and transferred in one
`hipMemcpy`; the dq_acc buffer follows immediately, at the offset given
by `GetDqAccDataOffset`.

#### Workspace size by scenario

| Scenario | `workspace_size` | Notes |
|----------|-----------------|-------|
| **kUseQrQtrDorPipeline** (any mode) | `0` | Writes dq directly; no acc
buffer; `PrepareWorkspaceHost` returns 0 |
| **Non-deterministic + batch mode** | `> 0` | nsplits[1]=1; dq_acc used
for atomic add; `workspace_size = host_ws_size +
batch*nhead*seqlen_q*hdim_q*ebytes` |
| **Non-deterministic + group mode** | `> 0` | nsplits[1]=1; dq_acc
contiguous layout; `workspace_size = host_ws_size +
nhead*seqstart_qs[batch]*hdim_q*ebytes` |
| **Deterministic + group mode** | `> 0` | nsplits[batch],
offsets[batch+1], compact dq_acc; nsplits_i computed independently per
batch |
| **Deterministic + batch mode persistent** | `> 0` | nsplits[1]
(uniform across batches); dq_acc `batch*nhead*nsplits*seqlen_q*hdim_q` |

**NeedsZeroDqAcc** (determines whether `PrepareWorkspaceDevice` calls
`hipMemset`):
- Persistent kernel (deterministic batch mode) or non-deterministic:
**must zero** (atomic add requires zero initialization)
- Deterministic group mode + no mask: **no zeroing needed** (every tile
writes its full region)
- Deterministic + with mask: **must zero** (some blocks are skipped,
leaving uninitialized tiles that would contribute to the reduction)

#### Caller usage

```cpp
// 1. Create launcher (traits include seqstart_qs/ks pointers; workspace_size is computed during construction)
fmha_bwd_launcher launcher(fmha_traits);

// 2. Read launcher.workspace_size directly
const auto ws_size = launcher.workspace_size;

// 3. Allocate a single GPU workspace
ck_tile::DeviceMem ws_buf(ws_size);

// 4. Copy nsplits/offsets to GPU head and zero dq_acc if required
launcher.prepare_workspace(ws_buf.GetDeviceBuffer());

// 5. Build args with a single workspace pointer; the kernel splits it internally
fmha_bwd_args args{
    ...,
    ws_size > 0 ? ws_buf.GetDeviceBuffer() : nullptr,  // workspace_ptr
};
launcher(args, stream_config);
```
DDEle added a commit to ROCm/aiter that referenced this pull request May 7, 2026
DDEle added a commit to ROCm/flash-attention that referenced this pull request May 7, 2026
valarLip pushed a commit to ROCm/aiter that referenced this pull request May 14, 2026
* [CK_TILE] mha bwd: switch to fmha_bwd_launcher usage

PR #2321 inlined launcher.dq_acc_splits / needs_zero_dq_acc as a
hardcoded `nsplits = ceil(seqlen_k/16)` and unconditional zero, because
the CK branch it bumped to had temporarily removed fmha_bwd_launcher.
The pinned CK now has the launcher back, so restore the #2216 pattern:
construct fmha_bwd_traits + fmha_bwd_launcher and read nsplits and
needs_zero_dq_acc from it.

Functional behavior is unchanged with the current pinned CK; this is a
prep commit to keep the diff for the upcoming #6152 (unified workspace)
adaptation small.

Touched files:
- csrc/py_itfs_ck/mha_bwd_kernels.cu (batch CK entry)
- csrc/py_itfs_ck/mha_varlen_bwd_kernels.cu (group CK entry)
- op_tests/cpp/mha/benchmark_mha_bwd.cpp (benchmark host)

* [CK_TILE] mha bwd: adapt to CK #6152 unified workspace API

CK PR #6152 replaces fmha_bwd_args.dq_acc_ptr + the four dq_acc_*
strides with a single opaque workspace_ptr, and exposes
fmha_bwd_launcher.workspace_size + prepare_workspace(void*) instead of
dq_acc_splits + needs_zero_dq_acc.

aiter::mha_bwd_args is unchanged (no new fields). The existing
dq_acc_ptr field doubles as the CK workspace pointer for the CK path;
the four dq_acc_* stride fields are kept for the ASM v3 path that
still consumes them. The torch entries (mha_bwd_kernels.cu /
mha_varlen_bwd_kernels.cu / benchmark_mha_bwd.cpp) construct the
launcher, allocate an at::Tensor workspace of launcher.workspace_size
bytes, call launcher.prepare_workspace, and pass workspace.data_ptr()
through dq_acc_ptr.

For group mode, the launcher requires host-side seqstart arrays. The
torch varlen entry copies cu_seqlens_q_padded (or cu_seqlens_q if no
padded variant was provided) via .to(at::kCPU) — using the same
"physical seqstart" convention that the kernel itself indexes dq_acc
with. The aiter::mha_bwd dispatcher does its own hipMemcpy from
a.seqstart_q_ptr / a.seqstart_k_ptr for the CK fallback path. Neither
host buffer crosses the call boundary.

Submodule bumped to 8a59f8afa58 (subtree-split of monorepo
users/yiding12/fmha-bwd-workspace tip, includes the per-nhead
dq_acc stride fix for group mode).

* [CK_TILE] mha bwd: single workspace_alloc callback for both paths

Removes the dual-purpose dq_acc_ptr field and the four dq_acc_* stride
fields from aiter::mha_bwd_args, replacing them with one callback that
serves both dispatch paths:

  std::function<void*(size_t bytes, bool zero_init)> workspace_alloc;

- CK fallback (in aiter::mha_bwd):
  Constructs fmha_bwd_launcher, queries workspace_size, calls
  workspace_alloc(size, zero_init=false), then forwards the pointer to
  launcher.prepare_workspace + launcher.run. The torch entries no
  longer construct fmha_bwd_launcher themselves; py_itfs_ck/* shrinks
  considerably (traits + launcher + workspace blocks deleted, varlen
  also drops its private cu_seqlens D2H copy).

- ASM v3 (in fmha_v3_bwd):
  Determines dq_accum shape and byte count internally (based on
  is_group_mode, v3_atomic_fp32, hdim, batch, nhead, seqlen) and calls
  workspace_alloc(bytes, zero_init=true). The torch entries no longer
  allocate or zero dq_accum themselves; py_itfs_cu/asm_mha_*.cu shrinks
  to a small lambda.

The zero_init flag lets each backing storage pick its efficient zero
path (torch::zeros, DeviceMem::SetZero, ...) instead of forcing the
dispatch path to know HIP memset semantics. ASM kernels need it
because they atomically accumulate into dq_accum; the CK launcher
fills its own workspace and does not need a pre-zero.

* [CK_TILE] mha bwd: address review comments on workspace_alloc

- mha_bwd CK fallback: explicitly reject group mode with missing seqstart
  pointers (LOG_ERROR + return -1) instead of silently passing nullptr to
  the launcher. Matches the existing AITER_LOG_WARNING pattern in
  fmha_v3_bwd for unsupported configurations.

- benchmark workspace_alloc: replace silent return-nullptr-on-oversize
  with AITER_CHECK so the failure is loud at the actual call site rather
  than as an opaque kernel crash later. Also tighten the zero-init path
  to hipMemset only `bytes` rather than the full pre-allocated buffer.

* Update CK pin as ROCm/rocm-libraries#6152 merged
DDEle added a commit to ROCm/flash-attention that referenced this pull request May 14, 2026
* [CK_TILE] Use Unified Workspace for FMHA BWD

Bump composable_kernel submodule to mono-split/users/yiding12/fmha-bwd-workspace
HEAD and adapt the FMHA BWD host wrappers to the new unified workspace API:

- Replace dq_acc tensor argument with workspace_ptr in get_ck_fmha_bwd_args
  / get_ck_fmha_varlen_bwd_args
- Drop dq_acc strides that have been removed from fmha_bwd_args
- In mha_bwd / mha_varlen_bwd, allocate the device workspace based on
  fmha_bwd_launcher::workspace_size and call launcher.prepare_workspace()
- Invoke launcher.run(args, stream_config) instead of fmha_bwd(...)

* Update CK pin as ROCm/rocm-libraries#6152 merged
aledudek pushed a commit that referenced this pull request May 20, 2026
## Motivation
`dq_acc` is the intermediate accumulation buffer used in FMHA backward
pass for deterministic mode. The current implementation allocates it as
a **single rectangular tensor**:

```
shape = [shape_batch, nhead, nsplits, shape_seqlen_q, hdim_q]
```

where `nsplits = launcher.dq_acc_splits` (a single scalar), computed
from `max_seqlen_k` and shared across all batches.

### Problems

1. **Memory waste**: In group mode, each batch may have a different
`seqlen_k`, but `nsplits` is computed from `max_seqlen_k`, causing
batches with shorter `seqlen_k` to over-allocate in the split dimension.

2. **Interface coupling**: `fmha_bwd_args` exposes internal layout
details such as `stride_dq_acc`, `nhead_stride_dq_acc`,
`batch_stride_dq_acc`, and `split_stride_dq_acc`. The caller is
responsible for computing these strides, but this logic belongs inside
the kernel.

### Goals

1. Switch `dq_acc` buffer to a **compact layout**: batches are
concatenated contiguously, with each batch occupying `nhead * nsplits_i
* seqq_i * hdim_q` elements (nhead outermost).
2. **Remove all `*_stride_dq_acc` fields** from `fmha_bwd_args`,
replacing them with a single `workspace_ptr`; the kernel splits this
internally using a fixed layout.
4. `fmha_bwd_launcher` provides a **workspace management interface**:
the caller only needs to allocate GPU memory and call
`prepare_workspace()` — no layout computation required.
5. **Isolate kernel internals from the caller API**: the `dq_acc` layout
(nsplits, strides, buffer size) is determined entirely inside the
launcher/kernel. Future changes to block shape, pipeline type, or
persistent kernel strategy require no modifications to the caller's
`fmha_bwd_args` or workspace allocation logic.

## Technical Details

### Interface Design

#### New fields in `fmha_bwd_traits`

```cpp
struct fmha_bwd_traits
{
    int seqlen_q;
    int seqlen_k;
    int batch;
    int max_seqlen_q;
    int max_seqlen_k;
    int hdim_q;
    int hdim_v;
    int nhead_q;
    int nhead_k;
    std::string data_type;
    bool is_group_mode;
    mask_enum mask_type;
    bias_enum bias_type;
    bool has_dbias;
    bool has_dropout;
    bool is_store_randval;
    bool is_deterministic;
    // New: cumulative physical seqlen pointers for group mode (pass nullptr for batch mode).
    // seqstart_qs[i+1] - seqstart_qs[i] = physical seqlen_q of batch i (including padding); length = batch+1
    // seqstart_ks[i+1] - seqstart_ks[i] = physical seqlen_k of batch i (including padding); length = batch+1
    const int* seqstart_qs = nullptr;
    const int* seqstart_ks = nullptr;
};
```

#### `fmha_bwd_launcher` actual structure

```cpp
struct fmha_bwd_launcher
{
    std::function<float(fmha_bwd_args, const ck_tile::stream_config&)> run{};

    // Total workspace size in bytes (host_ws_size + device_ws_size), computed by init().
    // Zero for kUseQrQtrDorPipeline (writes dq directly, no acc buffer needed).
    size_t workspace_size = 0;

    fmha_bwd_launcher(const fmha_bwd_traits&);

    // Copies auxiliary data (nsplits[], offsets[]) via hipMemcpy to the head of the GPU workspace,
    // and zeros the dq_acc buffer portion (tail of workspace) if required.
    // The memory pointed to by device_ws must be >= workspace_size bytes.
    std::function<void(void* device_ws)> prepare_workspace{};

    template <typename... Args>
    float operator()(Args&&... args) const { return run(std::forward<Args>(args)...); }

private:
    size_t host_ws_size   = 0;  // CPU workspace size (nsplits[] + offsets[] arrays)
    size_t device_ws_size = 0;  // GPU-only data size (dq_acc buffer)
    std::unique_ptr<char[]> ws_host;  // host-side workspace buffer

public:
    template <typename T0, typename T1, typename T2, typename Arch>
    void init(const fmha_bwd_traits& traits);
};
```

The `init<>()` template method (invoked by codegen dispatch branches as
`this->init<...>(t)`) is responsible for:
1. Setting the `run` lambda
2. Calling `FmhaBwdDQDKDVKernel::GetWorkspaceHostSize(batch)` to obtain
`host_ws_size`
3. Allocating `ws_host` (host memory)
4. Calling `FmhaBwdDQDKDVKernel::PrepareWorkspaceHost(ws_host.get(),
...)` to fill nsplits/offsets; return value is `device_ws_size`
5. `workspace_size = host_ws_size + device_ws_size`
6. Setting the `prepare_workspace` lambda (captures `this`, calls
`PrepareWorkspaceDevice`)

When no kernel matches the given traits, both `run` and
`prepare_workspace` are initialized to default lambdas that print a
warning to `std::cerr` and return gracefully (no exception).

#### Workspace overall layout

The workspace is managed by `FmhaBwdWorkspaceManager` and consists of
two segments:

```
Offset 0 (CPU-prepared segment, host_ws_size bytes; also hipMemcpy'd to the head of GPU workspace):
  index_t nsplits[batch or 1]       — per-batch nsplits array
                                      group mode: batch elements
                                      batch mode / non-deterministic: 1 element
  [group mode only] long_index_t dq_acc_offsets[batch+1]
                                    — per-batch element offset (inclusive prefix sum)
                                      offsets[0]=0, offsets[i+1] = offsets[i] + nhead*nsplits_i*seqq_i*hdim_q

Offset host_ws_size (device data segment, device_ws_size bytes):
  AccDataType dq_acc[total_elements] — compact dq_acc buffer (zeroed if required)
                                       total_elements = sum_i(nhead * nsplits_i * seqq_i * hdim_q)
                                       layout within each batch: [nhead, nsplits_i, seqq_i, hdim_q]
                                       note: seqq_i uses the physical length (including padding)
```

Alignment constant (`ALIGNMENT = 16`):
```
nsplits_size  = align_up(sizeof(index_t) * N, 16)          // N = batch (group) or 1 (batch/non-det)
offsets_size  = align_up(sizeof(long_index_t) * (batch+1), 16)  // group mode only
host_ws_size  = nsplits_size + offsets_size
dq_acc_offset = host_ws_size  // GetDqAccDataOffset(batch)
```

**Key benefits**:
- The kernel reads nsplits/offsets directly from the workspace head — no
device-side recomputation.
- `FmhaBwdConvertQGradKernel` is completely decoupled from the pipeline
block shape (`kN0`): nsplits is read from `nsplits_ptr`, `kN0` is no
longer a template parameter, and multiple dq_dk_dv tiles with different
`F_bn0` values now share a single convert_dq kernel instance (under
receipt 1/2, deterministic convert_dq kernel count drops from ~300 to
60).
- nsplits/offsets are computed on the host and transferred in one
`hipMemcpy`; the dq_acc buffer follows immediately, at the offset given
by `GetDqAccDataOffset`.

#### Workspace size by scenario

| Scenario | `workspace_size` | Notes |
|----------|-----------------|-------|
| **kUseQrQtrDorPipeline** (any mode) | `0` | Writes dq directly; no acc
buffer; `PrepareWorkspaceHost` returns 0 |
| **Non-deterministic + batch mode** | `> 0` | nsplits[1]=1; dq_acc used
for atomic add; `workspace_size = host_ws_size +
batch*nhead*seqlen_q*hdim_q*ebytes` |
| **Non-deterministic + group mode** | `> 0` | nsplits[1]=1; dq_acc
contiguous layout; `workspace_size = host_ws_size +
nhead*seqstart_qs[batch]*hdim_q*ebytes` |
| **Deterministic + group mode** | `> 0` | nsplits[batch],
offsets[batch+1], compact dq_acc; nsplits_i computed independently per
batch |
| **Deterministic + batch mode persistent** | `> 0` | nsplits[1]
(uniform across batches); dq_acc `batch*nhead*nsplits*seqlen_q*hdim_q` |

**NeedsZeroDqAcc** (determines whether `PrepareWorkspaceDevice` calls
`hipMemset`):
- Persistent kernel (deterministic batch mode) or non-deterministic:
**must zero** (atomic add requires zero initialization)
- Deterministic group mode + no mask: **no zeroing needed** (every tile
writes its full region)
- Deterministic + with mask: **must zero** (some blocks are skipped,
leaving uninitialized tiles that would contribute to the reduction)

#### Caller usage

```cpp
// 1. Create launcher (traits include seqstart_qs/ks pointers; workspace_size is computed during construction)
fmha_bwd_launcher launcher(fmha_traits);

// 2. Read launcher.workspace_size directly
const auto ws_size = launcher.workspace_size;

// 3. Allocate a single GPU workspace
ck_tile::DeviceMem ws_buf(ws_size);

// 4. Copy nsplits/offsets to GPU head and zero dq_acc if required
launcher.prepare_workspace(ws_buf.GetDeviceBuffer());

// 5. Build args with a single workspace pointer; the kernel splits it internally
fmha_bwd_args args{
    ...,
    ws_size > 0 ? ws_buf.GetDeviceBuffer() : nullptr,  // workspace_ptr
};
launcher(args, stream_config);
```

---

### Key Code Structure

#### FmhaBwdWorkspaceManager (`fmha_bwd_kernel.hpp`, new class)

```cpp
template <typename AccDataType, bool kIsGroupMode, bool kIsDeterministic>
struct FmhaBwdWorkspaceManager
{
    static constexpr size_t ALIGNMENT = 16;

    // CPU workspace (nsplits + offsets) sizes
    static size_t GetDqAccSplitsSize(int batch);   // align_up(sizeof(index_t)*N, 16)
    static size_t GetDqAccOffsetsSize(int batch);  // group mode only: align_up(sizeof(long_index_t)*(batch+1), 16)
    static size_t GetWorkspaceHostSize(int batch);  // = SplitsSize + OffsetsSize

    // Starting offset of dq_acc data within the full workspace (= host_ws_size)
    static size_t GetDqAccDataOffset(int batch);   // = GetWorkspaceHostSize(batch)

    // Fills nsplits/offsets in the CPU workspace; returns device_ws_size (dq_acc buffer bytes)
    template <bool kUseQrQtrDorPipeline, index_t kN0>
    static size_t PrepareWorkspaceHost(void* cpu_ws, index_t batch_size, index_t hdim_q,
                                       index_t nhead_q, index_t seqlen_q, index_t seqlen_k,
                                       const index_t* seqstart_qs, const index_t* seqstart_ks);

    // hipMemcpy's cpu_ws to device_ws head; hipMemset's the dq_acc portion to 0 if required
    template <bool kUseQrQtrDorPipeline, bool kHasMask>
    static void PrepareWorkspaceDevice(void* device_ws, const void* host_ws,
                                       size_t device_ws_size, size_t host_ws_size);
};
```

#### workspace_ptr parsing (inside the kernel)

The kernel parses three address regions from `kargs.workspace_ptr`:

**Group mode (`FmhaBwdDQDKDVKernel::MakeKargs`)**:
```cpp
const uint8_t* ws = reinterpret_cast<uint8_t*>(workspace_ptr);
// dq_acc_ptr (stored in FmhaBwdCommonKargs)
ws + WorkspaceManager::GetDqAccDataOffset(batch)
// dq_acc_batch_offset_ptr (FmhaBwdGroupModeKargs field)
reinterpret_cast<const long_index_t*>(ws + WorkspaceManager::GetDqAccOffsetsOffset(batch))
```

**Batch mode**:
```cpp
ws + WorkspaceManager::GetDqAccDataOffset(batch)  // dq_acc_ptr
// No offsets pointer; batch offset is computed inside run_() from nsplits
```

**`FmhaBwdConvertQGradKernel`** follows the same pattern:
- Group mode: extracts `dq_acc_ptr`, `dq_acc_batch_offset_ptr`, and
`nsplits_ptr` (`GetDqAccSplitsOffset(batch)`) from workspace
- Batch mode: reads nsplits from `nsplits_ptr[0]`; batch offset computed
internally

### Addressing in `run_()` (group mode)

```cpp
// Per-batch processing:
const long_index_t batch_offset_dq_acc = kargs.dq_acc_batch_offset_ptr[i_batch];
// seqq_i (physical length) derived from seqstart_q_ptr
const index_t seqq_i = kargs.seqstart_q_ptr[i_batch+1] - kargs.seqstart_q_ptr[i_batch];
// nsplits_i read from nsplits_ptr (convert_dq kernel) or from GetDqAccSplits
const long_index_t split_stride_i = static_cast<long_index_t>(seqq_i) * kargs.hdim_q;
const long_index_t nhead_stride_i = static_cast<long_index_t>(nsplits_i) * split_stride_i;
// Final address:
dq_acc_base + batch_offset_dq_acc + i_nhead * nhead_stride_i + i_split * split_stride_i
```

#### nsplits computation (`PrepareWorkspaceHost`)

`PrepareWorkspaceHost` is a template method of `FmhaBwdWorkspaceManager`
that still takes `kN0` as a template parameter (from
`BlockFmhaShape::kN0` of the dq_dk_dv pipeline). However, this parameter
is **only used inside this host-side function** to compute nsplits — it
is no longer passed into the convert_dq kernel.

| Mode | nsplits computation |
|------|---------------------|
| kUseQrQtrDorPipeline | Writes dq directly; nsplits[0]=0; returns
device_ws_size=0 |
| Non-deterministic | nsplits[0]=1; dq_acc used for atomic add |
| Deterministic + group mode | `ceil((seqstart_ks[i+1]-seqstart_ks[i]) /
kN0)` computed per batch |
| Deterministic + batch mode persistent | Same logic as the original
`GetDqAccSplits` (`dqdqkdv_workers` based) |

### Removing kN0 dependency from `FmhaBwdConvertQGradKernel`

`FmhaBwdConvertQGradKernel` previously required `kN0` as a template
parameter (via `BlockFmhaBwdConvertQGradPipelineProblem`) for two
purposes:
1. In batch mode `operator()`: self-computing `nsplits = ceil(seqlen_k /
kN0)`
2. The `b{kM0}x{kN0}` component of the kernel name string

Both have been removed in this refactor:
- **Batch mode**: now reads `kargs.nsplits_ptr[0]` directly (guarded by
`if constexpr(kIsDeterministic)` to avoid accessing a non-existent field
in non-deterministic instances)
- **Kernel name**: simplified to `b{kM0}`, no longer includes `kN0`
- **Template parameters**: `BlockFmhaBwdConvertQGradPipelineProblem`
drops the `kN0_` parameter; `fmha_bwd_convert_dq_traits_` drops the
`kN0` parameter; `F_bn0`/`convert_dq_bn0` fields removed from codegen

Effect: all dq_dk_dv tiles sharing the same `(hdim, dtype, mode, pad,
deterministic)` combination — regardless of `F_bn0` value
(16/64/128/192/256) — now share a **single** convert_dq kernel instance.

---

## Test Plan

<!-- Explain any relevant testing done to verify this PR. -->

## Test Result

<!-- Briefly summarize test outcomes. -->

## Submission Checklist

- [x] Look over the contributing guidelines at
https://github.com/ROCm/ROCm/blob/develop/CONTRIBUTING.md#pull-requests.
shumway pushed a commit to ROCm/composable_kernel that referenced this pull request May 27, 2026
[CK_TILE] Use Unified Workspace for FMHA BWD (#6152)

## Motivation
`dq_acc` is the intermediate accumulation buffer used in FMHA backward
pass for deterministic mode. The current implementation allocates it as
a **single rectangular tensor**:

```
shape = [shape_batch, nhead, nsplits, shape_seqlen_q, hdim_q]
```

where `nsplits = launcher.dq_acc_splits` (a single scalar), computed
from `max_seqlen_k` and shared across all batches.

### Problems

1. **Memory waste**: In group mode, each batch may have a different
`seqlen_k`, but `nsplits` is computed from `max_seqlen_k`, causing
batches with shorter `seqlen_k` to over-allocate in the split dimension.

2. **Interface coupling**: `fmha_bwd_args` exposes internal layout
details such as `stride_dq_acc`, `nhead_stride_dq_acc`,
`batch_stride_dq_acc`, and `split_stride_dq_acc`. The caller is
responsible for computing these strides, but this logic belongs inside
the kernel.

### Goals

1. Switch `dq_acc` buffer to a **compact layout**: batches are
concatenated contiguously, with each batch occupying `nhead * nsplits_i
* seqq_i * hdim_q` elements (nhead outermost).
2. **Remove all `*_stride_dq_acc` fields** from `fmha_bwd_args`,
replacing them with a single `workspace_ptr`; the kernel splits this
internally using a fixed layout.
4. `fmha_bwd_launcher` provides a **workspace management interface**:
the caller only needs to allocate GPU memory and call
`prepare_workspace()` — no layout computation required.
5. **Isolate kernel internals from the caller API**: the `dq_acc` layout
(nsplits, strides, buffer size) is determined entirely inside the
launcher/kernel. Future changes to block shape, pipeline type, or
persistent kernel strategy require no modifications to the caller's
`fmha_bwd_args` or workspace allocation logic.

## Technical Details

### Interface Design

#### New fields in `fmha_bwd_traits`

```cpp
struct fmha_bwd_traits
{
    int seqlen_q;
    int seqlen_k;
    int batch;
    int max_seqlen_q;
    int max_seqlen_k;
    int hdim_q;
    int hdim_v;
    int nhead_q;
    int nhead_k;
    std::string data_type;
    bool is_group_mode;
    mask_enum mask_type;
    bias_enum bias_type;
    bool has_dbias;
    bool has_dropout;
    bool is_store_randval;
    bool is_deterministic;
    // New: cumulative physical seqlen pointers for group mode (pass nullptr for batch mode).
    // seqstart_qs[i+1] - seqstart_qs[i] = physical seqlen_q of batch i (including padding); length = batch+1
    // seqstart_ks[i+1] - seqstart_ks[i] = physical seqlen_k of batch i (including padding); length = batch+1
    const int* seqstart_qs = nullptr;
    const int* seqstart_ks = nullptr;
};
```

#### `fmha_bwd_launcher` actual structure

```cpp
struct fmha_bwd_launcher
{
    std::function<float(fmha_bwd_args, const ck_tile::stream_config&)> run{};

    // Total workspace size in bytes (host_ws_size + device_ws_size), computed by init().
    // Zero for kUseQrQtrDorPipeline (writes dq directly, no acc buffer needed).
    size_t workspace_size = 0;

    fmha_bwd_launcher(const fmha_bwd_traits&);

    // Copies auxiliary data (nsplits[], offsets[]) via hipMemcpy to the head of the GPU workspace,
    // and zeros the dq_acc buffer portion (tail of workspace) if required.
    // The memory pointed to by device_ws must be >= workspace_size bytes.
    std::function<void(void* device_ws)> prepare_workspace{};

    template <typename... Args>
    float operator()(Args&&... args) const { return run(std::forward<Args>(args)...); }

private:
    size_t host_ws_size   = 0;  // CPU workspace size (nsplits[] + offsets[] arrays)
    size_t device_ws_size = 0;  // GPU-only data size (dq_acc buffer)
    std::unique_ptr<char[]> ws_host;  // host-side workspace buffer

public:
    template <typename T0, typename T1, typename T2, typename Arch>
    void init(const fmha_bwd_traits& traits);
};
```

The `init<>()` template method (invoked by codegen dispatch branches as
`this->init<...>(t)`) is responsible for:
1. Setting the `run` lambda
2. Calling `FmhaBwdDQDKDVKernel::GetWorkspaceHostSize(batch)` to obtain
`host_ws_size`
3. Allocating `ws_host` (host memory)
4. Calling `FmhaBwdDQDKDVKernel::PrepareWorkspaceHost(ws_host.get(),
...)` to fill nsplits/offsets; return value is `device_ws_size`
5. `workspace_size = host_ws_size + device_ws_size`
6. Setting the `prepare_workspace` lambda (captures `this`, calls
`PrepareWorkspaceDevice`)

When no kernel matches the given traits, both `run` and
`prepare_workspace` are initialized to default lambdas that print a
warning to `std::cerr` and return gracefully (no exception).

#### Workspace overall layout

The workspace is managed by `FmhaBwdWorkspaceManager` and consists of
two segments:

```
Offset 0 (CPU-prepared segment, host_ws_size bytes; also hipMemcpy'd to the head of GPU workspace):
  index_t nsplits[batch or 1]       — per-batch nsplits array
                                      group mode: batch elements
                                      batch mode / non-deterministic: 1 element
  [group mode only] long_index_t dq_acc_offsets[batch+1]
                                    — per-batch element offset (inclusive prefix sum)
                                      offsets[0]=0, offsets[i+1] = offsets[i] + nhead*nsplits_i*seqq_i*hdim_q

Offset host_ws_size (device data segment, device_ws_size bytes):
  AccDataType dq_acc[total_elements] — compact dq_acc buffer (zeroed if required)
                                       total_elements = sum_i(nhead * nsplits_i * seqq_i * hdim_q)
                                       layout within each batch: [nhead, nsplits_i, seqq_i, hdim_q]
                                       note: seqq_i uses the physical length (including padding)
```

Alignment constant (`ALIGNMENT = 16`):
```
nsplits_size  = align_up(sizeof(index_t) * N, 16)          // N = batch (group) or 1 (batch/non-det)
offsets_size  = align_up(sizeof(long_index_t) * (batch+1), 16)  // group mode only
host_ws_size  = nsplits_size + offsets_size
dq_acc_offset = host_ws_size  // GetDqAccDataOffset(batch)
```

**Key benefits**:
- The kernel reads nsplits/offsets directly from the workspace head — no
device-side recomputation.
- `FmhaBwdConvertQGradKernel` is completely decoupled from the pipeline
block shape (`kN0`): nsplits is read from `nsplits_ptr`, `kN0` is no
longer a template parameter, and multiple dq_dk_dv tiles with different
`F_bn0` values now share a single convert_dq kernel instance (under
receipt 1/2, deterministic convert_dq kernel count drops from ~300 to
60).
- nsplits/offsets are computed on the host and transferred in one
`hipMemcpy`; the dq_acc buffer follows immediately, at the offset given
by `GetDqAccDataOffset`.

#### Workspace size by scenario

| Scenario | `workspace_size` | Notes |
|----------|-----------------|-------|
| **kUseQrQtrDorPipeline** (any mode) | `0` | Writes dq directly; no acc
buffer; `PrepareWorkspaceHost` returns 0 |
| **Non-deterministic + batch mode** | `> 0` | nsplits[1]=1; dq_acc used
for atomic add; `workspace_size = host_ws_size +
batch*nhead*seqlen_q*hdim_q*ebytes` |
| **Non-deterministic + group mode** | `> 0` | nsplits[1]=1; dq_acc
contiguous layout; `workspace_size = host_ws_size +
nhead*seqstart_qs[batch]*hdim_q*ebytes` |
| **Deterministic + group mode** | `> 0` | nsplits[batch],
offsets[batch+1], compact dq_acc; nsplits_i computed independently per
batch |
| **Deterministic + batch mode persistent** | `> 0` | nsplits[1]
(uniform across batches); dq_acc `batch*nhead*nsplits*seqlen_q*hdim_q` |

**NeedsZeroDqAcc** (determines whether `PrepareWorkspaceDevice` calls
`hipMemset`):
- Persistent kernel (deterministic batch mode) or non-deterministic:
**must zero** (atomic add requires zero initialization)
- Deterministic group mode + no mask: **no zeroing needed** (every tile
writes its full region)
- Deterministic + with mask: **must zero** (some blocks are skipped,
leaving uninitialized tiles that would contribute to the reduction)

#### Caller usage

```cpp
// 1. Create launcher (traits include seqstart_qs/ks pointers; workspace_size is computed during construction)
fmha_bwd_launcher launcher(fmha_traits);

// 2. Read launcher.workspace_size directly
const auto ws_size = launcher.workspace_size;

// 3. Allocate a single GPU workspace
ck_tile::DeviceMem ws_buf(ws_size);

// 4. Copy nsplits/offsets to GPU head and zero dq_acc if required
launcher.prepare_workspace(ws_buf.GetDeviceBuffer());

// 5. Build args with a single workspace pointer; the kernel splits it internally
fmha_bwd_args args{
    ...,
    ws_size > 0 ? ws_buf.GetDeviceBuffer() : nullptr,  // workspace_ptr
};
launcher(args, stream_config);
```

---

### Key Code Structure

#### FmhaBwdWorkspaceManager (`fmha_bwd_kernel.hpp`, new class)

```cpp
template <typename AccDataType, bool kIsGroupMode, bool kIsDeterministic>
struct FmhaBwdWorkspaceManager
{
    static constexpr size_t ALIGNMENT = 16;

    // CPU workspace (nsplits + offsets) sizes
    static size_t GetDqAccSplitsSize(int batch);   // align_up(sizeof(index_t)*N, 16)
    static size_t GetDqAccOffsetsSize(int batch);  // group mode only: align_up(sizeof(long_index_t)*(batch+1), 16)
    static size_t GetWorkspaceHostSize(int batch);  // = SplitsSize + OffsetsSize

    // Starting offset of dq_acc data within the full workspace (= host_ws_size)
    static size_t GetDqAccDataOffset(int batch);   // = GetWorkspaceHostSize(batch)

    // Fills nsplits/offsets in the CPU workspace; returns device_ws_size (dq_acc buffer bytes)
    template <bool kUseQrQtrDorPipeline, index_t kN0>
    static size_t PrepareWorkspaceHost(void* cpu_ws, index_t batch_size, index_t hdim_q,
                                       index_t nhead_q, index_t seqlen_q, index_t seqlen_k,
                                       const index_t* seqstart_qs, const index_t* seqstart_ks);

    // hipMemcpy's cpu_ws to device_ws head; hipMemset's the dq_acc portion to 0 if required
    template <bool kUseQrQtrDorPipeline, bool kHasMask>
    static void PrepareWorkspaceDevice(void* device_ws, const void* host_ws,
                                       size_t device_ws_size, size_t host_ws_size);
};
```

#### workspace_ptr parsing (inside the kernel)

The kernel parses three address regions from `kargs.workspace_ptr`:

**Group mode (`FmhaBwdDQDKDVKernel::MakeKargs`)**:
```cpp
const uint8_t* ws = reinterpret_cast<uint8_t*>(workspace_ptr);
// dq_acc_ptr (stored in FmhaBwdCommonKargs)
ws + WorkspaceManager::GetDqAccDataOffset(batch)
// dq_acc_batch_offset_ptr (FmhaBwdGroupModeKargs field)
reinterpret_cast<const long_index_t*>(ws + WorkspaceManager::GetDqAccOffsetsOffset(batch))
```

**Batch mode**:
```cpp
ws + WorkspaceManager::GetDqAccDataOffset(batch)  // dq_acc_ptr
// No offsets pointer; batch offset is computed inside run_() from nsplits
```

**`FmhaBwdConvertQGradKernel`** follows the same pattern:
- Group mode: extracts `dq_acc_ptr`, `dq_acc_batch_offset_ptr`, and
`nsplits_ptr` (`GetDqAccSplitsOffset(batch)`) from workspace
- Batch mode: reads nsplits from `nsplits_ptr[0]`; batch offset computed
internally

### Addressing in `run_()` (group mode)

```cpp
// Per-batch processing:
const long_index_t batch_offset_dq_acc = kargs.dq_acc_batch_offset_ptr[i_batch];
// seqq_i (physical length) derived from seqstart_q_ptr
const index_t seqq_i = kargs.seqstart_q_ptr[i_batch+1] - kargs.seqstart_q_ptr[i_batch];
// nsplits_i read from nsplits_ptr (convert_dq kernel) or from GetDqAccSplits
const long_index_t split_stride_i = static_cast<long_index_t>(seqq_i) * kargs.hdim_q;
const long_index_t nhead_stride_i = static_cast<long_index_t>(nsplits_i) * split_stride_i;
// Final address:
dq_acc_base + batch_offset_dq_acc + i_nhead * nhead_stride_i + i_split * split_stride_i
```

#### nsplits computation (`PrepareWorkspaceHost`)

`PrepareWorkspaceHost` is a template method of `FmhaBwdWorkspaceManager`
that still takes `kN0` as a template parameter (from
`BlockFmhaShape::kN0` of the dq_dk_dv pipeline). However, this parameter
is **only used inside this host-side function** to compute nsplits — it
is no longer passed into the convert_dq kernel.

| Mode | nsplits computation |
|------|---------------------|
| kUseQrQtrDorPipeline | Writes dq directly; nsplits[0]=0; returns
device_ws_size=0 |
| Non-deterministic | nsplits[0]=1; dq_acc used for atomic add |
| Deterministic + group mode | `ceil((seqstart_ks[i+1]-seqstart_ks[i]) /
kN0)` computed per batch |
| Deterministic + batch mode persistent | Same logic as the original
`GetDqAccSplits` (`dqdqkdv_workers` based) |

### Removing kN0 dependency from `FmhaBwdConvertQGradKernel`

`FmhaBwdConvertQGradKernel` previously required `kN0` as a template
parameter (via `BlockFmhaBwdConvertQGradPipelineProblem`) for two
purposes:
1. In batch mode `operator()`: self-computing `nsplits = ceil(seqlen_k /
kN0)`
2. The `b{kM0}x{kN0}` component of the kernel name string

Both have been removed in this refactor:
- **Batch mode**: now reads `kargs.nsplits_ptr[0]` directly (guarded by
`if constexpr(kIsDeterministic)` to avoid accessing a non-existent field
in non-deterministic instances)
- **Kernel name**: simplified to `b{kM0}`, no longer includes `kN0`
- **Template parameters**: `BlockFmhaBwdConvertQGradPipelineProblem`
drops the `kN0_` parameter; `fmha_bwd_convert_dq_traits_` drops the
`kN0` parameter; `F_bn0`/`convert_dq_bn0` fields removed from codegen

Effect: all dq_dk_dv tiles sharing the same `(hdim, dtype, mode, pad,
deterministic)` combination — regardless of `F_bn0` value
(16/64/128/192/256) — now share a **single** convert_dq kernel instance.

---

## Test Plan

<!-- Explain any relevant testing done to verify this PR. -->

## Test Result

<!-- Briefly summarize test outcomes. -->

## Submission Checklist

- [x] Look over the contributing guidelines at
https://github.com/ROCm/ROCm/blob/develop/CONTRIBUTING.md#pull-requests.
aosewski added a commit that referenced this pull request Jun 3, 2026
const_cast t_randval.ptr (const void*) when assigning the non-const
rand_val_ptr Kargs field. Without it the dropout variant
(fmha_bwd_dqdkdv_fp16_d128_batch_dropout) fails to compile.

Also clarify the conditional-base placeholder comments and update the
DQ_ACC stride doc to reflect the workspace-derived layout (CK Tile
#6152), with NSPLITS / DQ_ACC_BATCH_OFFSET slot notes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
aosewski added a commit that referenced this pull request Jun 3, 2026
## Motivation

2 upstream CK Tile PRs were pushed and broke `rocm_ck`, so it needed to
be adapted to the changes. This PR updates the `rocm_ck` bridge and
tests.

- **PR #5504 — `[CK Tile] Add sink token gradient support in FMHA
backward pass`**
`OGradDotO` `lse_ptr` / `sink_ptr` / `d_sink_ptr` / `p_undrop` /
`seqlen_q` / `hdim_v` / `nhead` added to common kargs, `*_stride_d`
renamed to `*_stride_lsed` (LSE and D has the same layout, so it covers
both, mode dependent kargs split, shifting the `LSEDataType` parameter.

- **PR #6152 — `[CK_TILE] Use Unified Workspace for FMHA BWD`**
`dq_acc` is not provided by `acc_buf` field anymore. It is now a device
tensor together with `nsplits_ptr` and, in group/varlen mode,
`dq_acc_batch_offset_ptr` (per-batch element offset into the `dq_acc`
buffer).

## Technical Details
Fix plan is described in:
#7865

| File | Description |
| ---- | ----------- |
| args.hpp | Increase `kMaxTensors` to 20 and update `Args`
size/static_asserts. |
| tests/test_args.cpp | Update ABI/size expectations for `Args` and
capacity constants. |
| tests/test_signature.cpp | Update capacity-limit expectation
(`kMaxTensors`). |
| ops/fmha_bwd/dqdkdv_spec.hpp | Add deterministic workspace slots
(`NSPLITS`, `DQ_ACC_BATCH_OFFSET`) and update `requiredTensors()`. |
| include/rocm_ck/ops/fmha_bwd/dqdkdv_api.hpp | Extend debug validation
to include new slots and skip group-only slots in batch mode. |
| include/rocm_ck/ops/fmha_bwd/dqdkdv_dev.hpp | Update DqDkDv device
bridge to match CK Tile deterministic `Kargs` changes. |
| tests/test_fmha_bwd_validate_args.cpp | Update death test to populate
newly-required deterministic workspace slot. |
| include/rocm_ck/ops/fmha_bwd/convert_dq_spec.hpp | Update ConvertDQ
slot layout to include workspace-derived `NSPLITS`/offsets and revised
`requiredTensors()`. |
| include/rocm_ck/ops/fmha_bwd/convert_dq_dev.hpp | Update ConvertDQ
device bridge to match CK Tile `Kargs` changes (nsplits ptr, nhead). |
| tests/test_fmha_bwd_convert_dq.cpp | Update required-tensor-count
expectations for the new slot layout. |
| include/rocm_ck/ops/fmha_bwd/ograd_dot_o_dev.hpp | Update OGradDotO
device bridge for CK Tile `Kargs` signature changes (LSE/sink fields). |
</details>


## Test Plan


## Test Result

- [x] `ctest -L rocm_ck --output-on-failure`: 64/64 pass
- [x] `ctest -L compile_fail --output-on-failure`: 45/45 pass
- [x] `ninja kpack_archive` produces non-zero .hsaco files for every
entry in `KERNEL_VARIANTS` against `GPU_TARGETS=gfx942`
- [x] `kernels.kpack` archive is produced and contains entries for all
variants.
- [x] `pack.py` integrity check (no duplicates, every CMake-listed
variant present in manifest, every manifest entry has matching `.hsaco`)
passes.

## Submission Checklist

- [x] Look over the contributing guidelines at
https://github.com/ROCm/ROCm/blob/develop/CONTRIBUTING.md#pull-requests.

---

## Resolves

Closes #7865
Closes #7879
Closes #7880
Closes #7881

### Additional fixes folded in (beyond the CK Tile interface-drift sync)

To reach a fully clean `ninja kpack_archive` *and* host-example build,
four
follow-up commits were added on top of the Arg-structure sync:

- **dqdkdv `rand_val_ptr`** — `const_cast<void*>(t_randval.ptr)`;
pre-existing
  const-discard that broke the `*_dropout` variants (`TensorArg::ptr` is
  `const void*`, CK Tile's `rand_val_ptr` is `void*`).
- **ConvertDQ Kargs** — two-path init (`{}` placeholder + named
`nsplits_ptr`
  under `if constexpr(K.is_deterministic)`), mirroring CK Tile's own
`MakeKargs`, so a non-deterministic ConvertDQ instantiation also
compiles.
- **dqdkdv wave64 guard** — fall back to `__GFX9__` because clang ≥23
dropped
the `__AMDGCN_WAVEFRONT_SIZE` predefine, which otherwise breaks every
dqdkdv
  variant on rocm7.13+.
- **host example** — `variant.spec.mode` (the flattened spec has no
nested
  `signature` member); unblocks the `kpack_rocm_ck_fmha_bwd` executable.

**Verified** on `rocm7.13` / clang 23 / `gfx942`: all 40
`KERNEL_VARIANTS`
compile with 0 errors, `kernels.kpack` (933 KB) is produced, and the
host
loader links.

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Adam Osewski <Adam.Osewski@amd.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
micmelesse pushed a commit to Dao-AILab/flash-attention that referenced this pull request Jul 6, 2026
* Add sink_ptr/d_sink_ptr to fmha_bwd_args to match updated CK submodule

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* update submodule

* [CK_TILE] Use Unified Workspace for FMHA BWD (#182)

* [CK_TILE] Use Unified Workspace for FMHA BWD

Bump composable_kernel submodule to mono-split/users/yiding12/fmha-bwd-workspace
HEAD and adapt the FMHA BWD host wrappers to the new unified workspace API:

- Replace dq_acc tensor argument with workspace_ptr in get_ck_fmha_bwd_args
  / get_ck_fmha_varlen_bwd_args
- Drop dq_acc strides that have been removed from fmha_bwd_args
- In mha_bwd / mha_varlen_bwd, allocate the device workspace based on
  fmha_bwd_launcher::workspace_size and call launcher.prepare_workspace()
- Invoke launcher.run(args, stream_config) instead of fmha_bwd(...)

* Update CK pin as ROCm/rocm-libraries#6152 merged

* [CK_TILE] FMHA BWD: stream-async workspace prepare (#183)

* [CK_TILE] FMHA BWD: stream-async workspace prepare

Bump composable_kernel submodule to mono-split/users/yiding12/fmha-bwd-
async-prepare HEAD and adapt the FMHA BWD host wrappers to the new
async workspace prepare API (CK PR #7331):

- Replace launcher.prepare_workspace() with prepare_workspace_async(),
  which enqueues the full workspace setup (dq_acc zero, group-mode D2H
  of seqstart, host-side metadata pack via hipLaunchHostFunc, H2D back
  to device) on the caller's stream. No host-blocking sync remains in
  the BWD launch path.
- Pass a pinned_host_alloc lambda backed by PyTorch's CachingHostAllocator
  (torch::empty(..., pin_memory=true)). The launcher keeps the returned
  shared_ptr alive via a stream-tail hipLaunchHostFunc keepalive so the
  pinned buffer is not recycled while async copies are still in flight.
- mha_varlen_bwd: drop the cu_seqlens_q.cpu() / cu_seqlens_k.cpu() host
  copies; the launcher now reads device seqstart directly via async D2H.
  get_ck_fmha_varlen_bwd_traits no longer takes seqstart_qs/ks.

* [CK_TILE] FMHA BWD: bump CK submodule to develop tip (#7331 merged)

ROCm/rocm-libraries#7331 (async workspace prepare for FMHA BWD launcher)
landed on develop. Move csrc/composable_kernel from the pre-merge fork
tip ce838e19e5 to ROCm/composable_kernel develop tip 83566edb0f, which
is the split commit for #7331 (rocm-libraries 5692db0).

* [CK_TILE] FMHA BWD: explicit at::kCPU on pinned host TensorOptions

* Update CK and enable RDNA backward

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Yi DING <yi.ding@amd.com>
Co-authored-by: Hosang Yoon <hosang.yoon@amd.com>
MatthewBonanni added a commit to vllm-project/flash-attention that referenced this pull request Jul 13, 2026
* [Fwd,Sm100] fix: decode↔prefill exp2 emulation consistency (Dao-AILab#2595)

apply_exp2_convert selected the exp2 implementation based on mask_fn
presence: hardware ex2.approx.ftz for causal-masked tiles, polynomial
emulation for unmasked tiles. Different q_stage values (1 for decode,
2 for prefill) compute different m_block for the same logical Q row,
shifting which tiles are processed with vs without mask_fn. The same
K tile could receive different exp2 methods across variants.

Fix: always pass self.ex2_emu_freq regardless of mask_fn presence.
Add regression test for decode↔prefill bitwise consistency on MLA
(192,128) shapes.

* replace deprecated apis (Dao-AILab#2602)

* Bump nvidia-cutlass-dsl to >=4.5.2 and quack-kernels to >=0.5.0 (Dao-AILab#2605)

cutlass 4.5.2 is safe to update, and quack 0.5.0 has been published, so
bump the FA4 (flash_attn/cute) requirement floors to match. Updates the
dependencies and the cu13 extra in pyproject.toml, and the documented
versions in CLAUDE.md.

Verified on NVIDIA GB300 (SM100, CUDA 13.2): deps resolve cleanly
(nvidia-cutlass-dsl 4.5.2 base+cu13, quack-kernels 0.5.0), imports OK,
and a representative GPU sample of tests/cute/test_flash_attn.py passes
(6 passed / 6 skipped / 0 failed across hd 64/96/128/192, causal,
mha/gqa/mqa, fwd+bwd).

* [CuTe,Fwd,Sm100] refactor mla sm100 forward and add page table (Dao-AILab#2558)

* refactor mla sm100 forward

* add benchmark; address deprecation warnings; tweak ptx gemm dispatch

* update interface and tests

* ci: bump Jimver/cuda-toolkit to v0.2.35 for CUDA 13.2 support (Dao-AILab#2617)

v0.2.30 only ships URLs up to CUDA 13.1.0; bumping to v0.2.35 adds
13.1.1, 13.2.0, and the matching aarch64 SBSA installers.

Signed-off-by: oliver könig <okoenig@nvidia.com>

* [ROCm] Bump Triton to >=3.6.0 and aiter submodule (Dao-AILab#2614)

* [Triton] Fix graph capture issues and env var (Dao-AILab#2620)

* graph capture fix

* rm env flag

* [CuTe,Bwd,Sm100] allow 2cta with score mod and mask mod in bwd (Dao-AILab#2557)

* [CuTe] Fix lint failures (Dao-AILab#2625)

stack-info: PR: Dao-AILab#2625, branch: drisspg/stack/42

* [CuTe] Fix lint failure in flash_bwd_sm100.py (Dao-AILab#2627)

ruff format flagged flash_attn/cute/flash_bwd_sm100.py (trailing
whitespace in a comment and an over-split call). It was missed by
the lint sweep in Dao-AILab#2625.

* fix: add weights_only=True to all torch.load call sites (Dao-AILab#2622)

Passing weights_only=False (the pre-2.4 default) to torch.load allows
arbitrary Python object deserialization from the checkpoint file.
A malicious .pt/.pth file can execute arbitrary code on the machine
loading it — a well-known PyTorch deserialization vector (CWE-502).

Four call sites updated:
  training/src/utils/checkpoint.py  load_checkpoint()
  training/src/eval.py               eval checkpoint loader
  flash_attn/utils/pretrained.py    partial(torch.load, ...) loader
  flash_attn/models/llama.py        state_dicts_from_checkpoint()

weights_only=True restricts deserialization to tensors, dicts, lists,
tuples, and other primitive types — no arbitrary Python objects.
Requires PyTorch >= 1.13; FA4's CuTeDSL dependency already requires
a modern PyTorch 2.x build, so no compatibility regression.

Fixes Dao-AILab#2583

* use correction warps if not tma store; remove outdated packgqa guard (Dao-AILab#2629)

* Add aux-scalars to interface to enable dynamic ints and floats in expressions (Dao-AILab#2616)

stack-info: PR: Dao-AILab#2616, branch: drisspg/stack/41

* fix: build and select cu13.2 prebuilt wheels (Dao-AILab#2618)

* ci: use 1 ninja job for cu13.2

Signed-off-by: oliver könig <okoenig@nvidia.com>

* fix(setup): request cu13 prebuilt wheels for CUDA 13 torch

get_wheel_url() binned every CUDA >= 12 to major '12', so under a CUDA 13
torch it requested cu12 wheels and never matched the published cu13
artifacts, falling back to a multi-hour source build. Add a CUDA 13
branch so the guessed wheel name uses cu13, matching WHEEL_CUDA_VERSION
in _build.yml.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: oliver könig <okoenig@nvidia.com>

---------

Signed-off-by: oliver könig <okoenig@nvidia.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci(fa4): enforce cutlass-dsl/quack dep floors and rebake cu130 image (Dao-AILab#2636)

* ci(fa4): assert cute dep floors in CI; fail loudly on a stale SIF

run_fa4_ci.py installs FA4 with --no-deps (to keep the SIF's baked
torch/cudnn), so the nvidia-cutlass-dsl>=4.5.2 / quack-kernels>=0.5.0 floors
in flash_attn/cute/pyproject.toml are not enforced at install time. A SIF
baked before a floor bump keeps a stale dep — e.g. cutlass-dsl 4.4.2, which
can't convert the AuxData JIT arg and dies with a cryptic DSLRuntimeError
deep in SM100 kernel launch (reproduced on B200).

Upgrading the dep in-place is not viable: the --writable-tmpfs overlay is
RAM-backed and too small for a cutlass-dsl reinstall (ENOSPC, and a partial
removal corrupts the baked torch). So instead of installing, add
assert_dsl_floor.py — it reads the floors from pyproject (no hardcoded
version to drift) and fails with an actionable "rebake the image" message
when the installed cutlass-dsl/quack are below them. Wired into run_step
right after the editable install.

The durable fix is to rebake the image at the current floors and bump the
digest in .github/workflows/ci.yml; this guard makes future drift fail fast
instead of silently.

* ci(fa4): bump cu130 image to 26.06.10 (cutlass-dsl 4.5.2 / quack 0.5.0)

* ci(fa4): fall back to tomli when tomllib is unavailable (Python 3.10)

* Fix SM100 FP8 fwd with cutlass-dsl >=4.5.2 (MmaF8F6F4Op) (Dao-AILab#2640)

cutlass-dsl >=4.5.2 changed make_trivial_tiled_mma to build plain FP8
MMAs as MmaF8F6F4Op (its _F8F6F4_TYPES branch) instead of the now-legacy
MmaFP8Op. The two are siblings under MmaOp, so _tcgen05_mma_kind's
isinstance(op, MmaFP8Op) check missed the new type and raised
"Unsupported tcgen05 MMA op kind: MmaF8F6F4Op", breaking the FP8 forward
path on Blackwell. Worked on 4.4.2.

Accept both ops in the f8f6f4 branch (both map to kind::f8f6f4).
mma_op_to_idesc only reads generic op attrs and is unaffected.

Validated on B200: FP8 fwd passes for all configs in the issue
(incl. hd=64) plus hd=128, causal and non-causal; mean abs err vs bf16
~0.002-0.01.

Fixes Dao-AILab#2639

* [cute] Fix int32 overflow in SM100 LPT tile scheduler for long context (Dao-AILab#2662)

The LPT tile scheduler sizes its L2 swizzle from

    seqlen_k * (headdim + headdim_v) * element_size

in int32. For long context this overflows once it exceeds 2**31
(seqlen_k > ~4M for hdim-128 bf16), making size_one_head negative.
That corrupts the swizzle and the L2 divmods, so get_current_work
decodes an out-of-bounds batch_idx and the kernel performs an illegal
memory access (cudaErrorIllegalAddress) on SM100.

Compute the byte size in int64. swizzle stays small and is cast back
to int32 for the device-side divmods, so there is no behavior or perf
change for non-overflowing shapes.

Fixes both SingleTileLPTScheduler (forward; selected for causal/local)
and SingleTileLPTBwdScheduler (backward; its extra
seqlen_k * headdim * 4 term overflows even sooner).

Repro on SM100 (e.g. GB200), causal forward at seqlen_k = 2**22:

    import torch
    from flash_attn.cute.interface import flash_attn_func

    sq, sk = 2048, 4_194_304  # seqlen_k = 2**22 -> int32 overflow
    q = torch.randn(1, sq, 8, 128, dtype=torch.bfloat16, device="cuda")
    k = torch.randn(1, sk, 1, 128, dtype=torch.bfloat16, device="cuda")
    v = torch.randn(1, sk, 1, 128, dtype=torch.bfloat16, device="cuda")
    out = flash_attn_func(q, k, v, causal=True)
    torch.cuda.synchronize()  # cudaErrorIllegalAddress here before the fix

Crashes before this change, runs clean after; seqlen_k = 2**22 - 128 is
clean both ways (the int32 boundary). Verified clean under
compute-sanitizer memcheck.

* [Fwd,Sm100] Tune FP8 causal hd128 ex2_emu_freq (8 vs inherited 16) (Dao-AILab#2642)

FP8 fwd is MUFU/ex2-bound on Blackwell, so the optimal exp2-emulation
frequency differs from bf16. The causal hd128 key (False,True,128,False)
had no FP8 entry and inherited bf16's freq=16; freq=8 offloads more exp
from the MUFU unit.

Thermally-matched back-to-back A/B on B200 (locked-ish clock, hot GPU,
median of 300 iters, nheads=16 = benchmark default) across the official
benchmark's causal hd128 shapes:

  b   s      f16 TFLOP  f8 TFLOP  delta
  32  512    500.6      516.3     +3.1%
  16  1024   796.5      832.5     +4.5%
  8   2048   1124.6     1175.1    +4.5%
  4   4096   1407.9     1481.3    +5.2%
  2   8192   1604.1     1661.7    +3.6%
  1   16384  1683.7     1726.0    +2.5%

Accuracy-neutral (FP8-vs-bf16 mean-abs-err unchanged; benchmark --check
passes 24/24). Keyed on is_causal=True only: freq=8 would regress
non-causal hd128 (0.94x), which keeps its existing freq=10.

* Make q_subtile_factor default to identity (Dao-AILab#2660)

* fix(hd256/sm100): make q/k/v contiguous before dedicated hd256 kernel (Dao-AILab#2666)

The BlackwellFusedMultiHeadAttentionForward kernel builds tensor layouts
with hardcoded contiguous strides computed from shape dimensions, so
non-contiguous inputs (e.g. from .transpose()) cause wrong memory accesses
and silently corrupt outputs on B200 (SM100) with head_dim=256.

maybe_contiguous() only guarantees stride(-1)==1; add explicit full
contiguity checks in both the forward and backward paths when the hd256
dedicated kernel is selected.

Fixes: Dao-AILab#2665

* [Cute,Bwd,Sm100] add sparse MLA (Deepseek v4) backward kernels (Dao-AILab#2621)

* add backward sparse mla kernels

* add dk gemm

* fix errors

* fix dq errors

* rename bwd kernels

* refactor interface

* fix predicate error in dq kernel

* update tests

* mla fwd fixes

* improve varlen fwd perf

* use cluster idx scheduling in fwd

* use packed scheduler for mqa 128

* fix int32 overflow in swizzle

* simplify bwd preprocess

* refactor bwd

* simplify preprocess

* update benchmark script

* add safety check

* remove test code

* ruff format

* ensure scale is 0 for masked out rows

* fix: sync callers with new _flash_attn_fwd 4-tuple return signature (Dao-AILab#2674)

* Fix compatibility issues with CuTe DSL 4.6.0+ (Dao-AILab#2648)

* Prepare for 4.6 release

* Bump version

* Update pyproject.toml

* Update nvidia-cutlass-dsl version in pyproject.toml

* Pass tmem scalar fields as .ptr to TmemAllocator on SM100 (Dao-AILab#2679)

The DSL now warns when a struct scalar is used directly as a pointer
("Use explicit struct.scalar.ptr for pointer instead"), so these fire
on every tmem_holding_buf / dealloc mbar access. Just pass .ptr like the
other SM100 kernels already do.

* Add FLASHATTENTION_DISABLE_SPLIT_ALIGNMENT (Dao-AILab#2680)

* ci: rebake cu130 image for cutlass-dsl 4.6.0.dev0 floor (Dao-AILab#2684)

PR Dao-AILab#2648 bumped the flash_attn/cute/pyproject.toml floor to
nvidia-cutlass-dsl==4.6.0.dev0, but the CI image (26.06.10) still ships
4.5.2. assert_dsl_floor.py correctly fails every push to main with
"installed 4.5.2 does not satisfy floor ==4.6.0.dev0", so FA4 CI has
been red since Dao-AILab#2648 landed.

- Dockerfile: add --prerelease=allow to the FA4 install. The dev-build
  floor pulls transitive pre-releases (nvidia-cutlass-dsl-libs-base==
  4.6.0.dev0 ...) that uv refuses without it; the old stable 4.5.2 floor
  didn't need it.
- ci.yml: bump fa4_image_cu130 to the rebaked 26.06.27 image
  (cutlass-dsl 4.6.0.dev0, quack-kernels 0.5.3, torch 2.12.1).

E2e verified on B200: assert_dsl_floor passes, compile + run + benchmark
all green (run_fa4_ci.py, exit 0).

* Update FA4 cute quack compatibility (Dao-AILab#2676)

* Update FA4 cute quack compatibility

* Use quack 0.5.3 make_smem_layout instead of vendored copy

Tri re-added the major_mode_size arg to quack.sm90_utils.make_smem_layout
in quack 0.5.3 (commit 68888e2), so FA4 no longer needs the local
sm90_layout helper. Revert the 4 backward call sites to quack's helper and
bump the floor to >=0.5.3 (0.5.2 lacks the arg).

---------

Co-authored-by: Johnsonms <lizhaofu@gmail.com>

* ci: install cutlass-dsl/quack at runtime to decouple from the baked image (Dao-AILab#2685)

* [Cute,Bwd,Sm100] Assume 16B stride divisibility for LSE/dPsum bulk-copy inputs (Dao-AILab#2686)

The SM100 backward stats (LSE, dPsum) are loaded via cp.async.bulk
(CopyBulkG2SOp), which - unlike cp.async.bulk.tensor - needs the source
pointer alignment provable at compile time. After slicing, the newer
cute-dsl can't deduce 16B alignment unless the input strides carry the
divisibility assumption, so the bulk copy fails to compile on real
tensors (the FakeTensor path masks it).

- flash_bwd_mla_sm100.py: add mdPsum to the new_stride divisibility list
  (it already covered ScaleP and the other stats; mdPsum was omitted).
- flash_bwd_sm100.py: the ordinary backward had no divisibility
  assumption at all; add it for both mLSE and mdPsum.

Only these two SM100 kernels use CopyBulkG2SOp; the SM90/SM80/SM120 and
MLA dK/dQ backward kernels use other copy paths and are unaffected.

Addresses the dPsum stride-divisibility finding (Finding 1) in Dao-AILab#2677.

* fix(hd256/sm100): forward reads actual input strides, drop .contiguous() patch (Dao-AILab#2670)

* follow up to Dao-AILab#2666: fixing the layouts in the sm100 hd256 kernels and removing the temporary fix of calling .contiguous everywhere

* respond to PR comments

* respond to PR comments-2: move to utils file

* Add tests

---------

Co-authored-by: drisspg <drisspguessous@gmail.com>

* ci: run MLA backward cases so CI exercises flash_bwd_mla_sm100.py (Dao-AILab#2690)

FA4_TEST_FILTER selected no MLA test, so the MLA backward kernels
(flash_bwd_mla_sm100.py + dq_dqv + dk) had zero CI coverage. Add four
small test_flash_attn_mla_absorbed cases covering the distinct backward
paths: sparse (kv_sparsity=True) non-causal and causal, dense
(kv_sparsity=False), and shared_kv=True. The ordinary SM100 backward is
already covered by the existing test_flash_attn_output cases.

Cold-cache cost on B200 (full 8-case filter): pass-1 compile ~4:54,
GPU run ~1:03 — well under the 60-min job timeout.

Stacked on Dao-AILab#2685 (runtime cutlass-dsl/quack install).

* Parallelize splitkv alignment templated kernels, remove flag (Dao-AILab#2683)

* [FA3] uv installation support (Dao-AILab#2458)

* Expose flash_attn_3 as package so imports work correctly.

* Add flash_attn_config package shim and fix uv packaging details

Builds on the flash_attn_3 package exposure so both import styles work
for downstream frameworks and uv/pyproject.toml installs:

- Add flash_attn_3/flash_attn_config.py re-export so
  `from flash_attn_3 import flash_attn_config` works (previously only the
  top-level module was importable), matching the interface shim.
- Un-ignore the committed shim in .gitignore; the bare `flash_attn_config.py`
  pattern (for the build-time generated top-level file) also matched the
  package shim and would have silently dropped it from the commit.
- Read flash_attn_3.__version__ from installed package metadata with a
  fallback, avoiding drift from setup.py's version source.
- README: move `dependencies` under `[project]` so the uv snippet is valid
  PEP 621.

Verified on H100 (SM90): editable `uv pip install -e .` now succeeds (fails
on main), both `import flash_attn_interface` and
`from flash_attn_3 import flash_attn_interface` resolve, `flash_attn_config`
imports both ways, and fp16 hdim128 forward matches a torch reference
(max_abs_err <= 2e-3). ruff check passes.

---------

Co-authored-by: Johnsonms <lizhaofu@gmail.com>

* [AMD ROCm] Enable RDNA backward and adopt CK unified workspace (Dao-AILab#2675)

* Add sink_ptr/d_sink_ptr to fmha_bwd_args to match updated CK submodule

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* update submodule

* [CK_TILE] Use Unified Workspace for FMHA BWD (#182)

* [CK_TILE] Use Unified Workspace for FMHA BWD

Bump composable_kernel submodule to mono-split/users/yiding12/fmha-bwd-workspace
HEAD and adapt the FMHA BWD host wrappers to the new unified workspace API:

- Replace dq_acc tensor argument with workspace_ptr in get_ck_fmha_bwd_args
  / get_ck_fmha_varlen_bwd_args
- Drop dq_acc strides that have been removed from fmha_bwd_args
- In mha_bwd / mha_varlen_bwd, allocate the device workspace based on
  fmha_bwd_launcher::workspace_size and call launcher.prepare_workspace()
- Invoke launcher.run(args, stream_config) instead of fmha_bwd(...)

* Update CK pin as ROCm/rocm-libraries#6152 merged

* [CK_TILE] FMHA BWD: stream-async workspace prepare (#183)

* [CK_TILE] FMHA BWD: stream-async workspace prepare

Bump composable_kernel submodule to mono-split/users/yiding12/fmha-bwd-
async-prepare HEAD and adapt the FMHA BWD host wrappers to the new
async workspace prepare API (CK PR #7331):

- Replace launcher.prepare_workspace() with prepare_workspace_async(),
  which enqueues the full workspace setup (dq_acc zero, group-mode D2H
  of seqstart, host-side metadata pack via hipLaunchHostFunc, H2D back
  to device) on the caller's stream. No host-blocking sync remains in
  the BWD launch path.
- Pass a pinned_host_alloc lambda backed by PyTorch's CachingHostAllocator
  (torch::empty(..., pin_memory=true)). The launcher keeps the returned
  shared_ptr alive via a stream-tail hipLaunchHostFunc keepalive so the
  pinned buffer is not recycled while async copies are still in flight.
- mha_varlen_bwd: drop the cu_seqlens_q.cpu() / cu_seqlens_k.cpu() host
  copies; the launcher now reads device seqstart directly via async D2H.
  get_ck_fmha_varlen_bwd_traits no longer takes seqstart_qs/ks.

* [CK_TILE] FMHA BWD: bump CK submodule to develop tip (#7331 merged)

ROCm/rocm-libraries#7331 (async workspace prepare for FMHA BWD launcher)
landed on develop. Move csrc/composable_kernel from the pre-merge fork
tip ce838e19e5 to ROCm/composable_kernel develop tip 83566edb0f, which
is the split commit for #7331 (rocm-libraries 5692db0).

* [CK_TILE] FMHA BWD: explicit at::kCPU on pinned host TensorOptions

* Update CK and enable RDNA backward

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Yi DING <yi.ding@amd.com>
Co-authored-by: Hosang Yoon <hosang.yoon@amd.com>

* Fix CuTe SM120 compile-time argument handling (Dao-AILab#2671)

* Fix CuTe SM120 compile-time argument handling

* clean up

* guard empty SM120 local backward tiles

---------

Co-authored-by: Kevin-Li-2025 <2242139@qq.com>
Co-authored-by: drisspg <drisspguessous@gmail.com>

* [NVIDIA][CuTe,Fwd,sm120] Implement Pack-GQA on SM120 (+ graceful SplitKV fallback) (Dao-AILab#2656)

* [CuTe,Fwd,sm120] Fix use_tma_O crash on SM120 (issue Dao-AILab#2649)

On SM120 (Blackwell GeForce / RTX PRO 6000 / DGX Spark) the forward kernel set
`use_tma_O = self.arch >= Arch.sm_90`, enabling the TMA-based O-store epilogue.
But SM120 does not build the TMA store atom (tma_atom_O is None), so any forward
call crashes in cpasync.tma_partition with:

    AttributeError: 'NoneType' object has no attribute '_trait'

This makes the CuTe-DSL forward unusable on every SM120 GPU.

Restrict the TMA O-store to sm_90..sm_119, which is where the WGMMA-era epilogue
path is actually available:

    self.use_tma_O = Arch.sm_90 <= self.arch < Arch.sm_120

SM120 falls back to the non-TMA register->gmem O store (already used for the
SM80 path), which is correct and what the CpAsync SM120 kernel expects.

Verified on RTX PRO 6000 Blackwell (sm_120, cc 12.0), torch 2.12.0+cu130,
nvidia-cutlass-dsl 4.5.2: forward now runs and matches PyTorch SDPA reference
for hdim 64/96/128, causal and non-causal (max abs err <= 8e-3 in bf16). Before
this fix every SM120 forward call raised the AttributeError above.

* [CuTe,Fwd,sm120] Implement Pack-GQA on SM120; graceful SplitKV fallback

Pack-GQA was only half-wired in the SM80/SM120 CpAsync forward: the epilogue
referenced PackGQA.store_O/store_LSE, but the Q-load and head-indexing used the
plain (unpacked) path. So pack_gqa=True crashed in pack_gqa.store_O (crd2idx on a
packed (h_idx, m_idx) coordinate against an unpacked mO layout).

This implements Pack-GQA end to end on SM120 (and SM80), mirroring the SM90 path:
- Reshape mQ/mO (head_idx=2) and mLSE (head_idx=1) via pack_gqa_layout so
  qhead_per_kvhead folds into the seqlen mode ((qhead, seqlen)).
- Scheduler args use cute.size(mQ.shape[0]) (packed total rows) and seqlen_q_static
  = mQ.shape[0][1] (logical seqlen), so causal/mask q_idx stay correct.
- Kernel head-indexing: when pack_gqa, num_head from the scheduler already indexes
  the KV head (mQ/mK share nheads_kv); no division.
- Q-load: gather rows via PackGQA.load_Q (per-row (h_idx, m_idx) gmem pointers)
  instead of the contiguous local_tile path.

SplitKV (num_splits>1) is an SM100-only feature (SM80/SM90 also assert it
unsupported); SM120 has no forward+combine path. Fall back to num_splits=1, which
is numerically correct, instead of crashing in _check_type on the fp32 partials.

Verified on RTX PRO 6000 Blackwell (sm_120): pack_gqa=True matches PyTorch SDPA
GQA/MQA reference (err <= 8.4e-3 bf16) AND is bit-identical to the unpacked path
(max |packed - unpacked| = 0.0) across MHA/GQA/MQA, causal/non-causal, hd 64/128,
seqlen 512-2048. num_splits=3 falls back and matches reference (err 6.8e-4).
Stacked on the SM120 use_tma_O fix (Dao-AILab#2649).

* re-enable SM120 pack-gqa after rebase

* clean up SM120 pack-gqa split handling

* fix SM120 varlen pack-gqa offset

---------

Co-authored-by: drisspg <drisspguessous@gmail.com>

* Fix pre-commit

Signed-off-by: Matthew Bonanni <mbonanni@redhat.com>

---------

Signed-off-by: oliver könig <okoenig@nvidia.com>
Signed-off-by: Matthew Bonanni <mbonanni@redhat.com>
Co-authored-by: 鐘天楽 <tianle.zhong@bytedance.com>
Co-authored-by: brandonsun <brandons@nvidia.com>
Co-authored-by: Johnsonms <lizhaofu@gmail.com>
Co-authored-by: jayhshah <jayhshah@gmail.com>
Co-authored-by: oliver könig <okoenig@nvidia.com>
Co-authored-by: Michael Melesse <micmelesse@gmail.com>
Co-authored-by: Reuben Stern <107093092+reubenconducts@users.noreply.github.com>
Co-authored-by: Driss Guessous <32754868+drisspg@users.noreply.github.com>
Co-authored-by: aryan <aryansputta@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: sryap <17482891+sryap@users.noreply.github.com>
Co-authored-by: Yunwei Li <yunweili372423@gmail.com>
Co-authored-by: Zihao Wang <rekind133@outlook.com>
Co-authored-by: Anakin(Yancheng) Zheng <103552181+anakinxc@users.noreply.github.com>
Co-authored-by: Prashant Kumar <prashant.kumar@cohere.com>
Co-authored-by: Jane (Yuan) Xu <31798555+janeyx99@users.noreply.github.com>
Co-authored-by: Omar Attia <oy.attia@gmail.com>
Co-authored-by: drisspg <drisspguessous@gmail.com>
Co-authored-by: Björn Buschkämper <bjoern.buschkaemper@gmail.com>
Co-authored-by: rocking <ChunYu.Lai@amd.com>
Co-authored-by: Yi DING <yi.ding@amd.com>
Co-authored-by: Hosang Yoon <hosang.yoon@amd.com>
Co-authored-by: Yin Li <kxl474@student.bham.ac.uk>
Co-authored-by: Kevin-Li-2025 <2242139@qq.com>
Co-authored-by: Johnny <johnnynuca14@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants