Skip to content

[ROCm] Jax Add softmax sink (learnable off-by-one) support for the ROCm/CK fused attention backend - #678

Open
shurale-nkn wants to merge 14 commits into
devfrom
sink_support_ck
Open

[ROCm] Jax Add softmax sink (learnable off-by-one) support for the ROCm/CK fused attention backend#678
shurale-nkn wants to merge 14 commits into
devfrom
sink_support_ck

Conversation

@shurale-nkn

@shurale-nkn shurale-nkn commented Jul 24, 2026

Copy link
Copy Markdown

Description

Adds forward and backward support for softmax sink (NVTE_LEARNABLE_SOFTMAX) to the CK/AITER fused-attention backend on ROCm.

  • Backward pass computes and returns d_sink (accumulated via atomics inside CK, one scalar per head); the gradient buffer is zero-initialized before launch.
  • Since the AITER ASM v3 fwd/bwd kernels don't support sink, uses_fwd_v3/uses_bwd_v3 are forced off whenever has_sink is true, falling back to the (slower but correct) CK tile path.
  • Fixes the JAX extension's aux-tensor indexing (### PrepareFusedAttnBackwardAuxTensors). Upstream mixes static (fixed, assume-always-present) and dynamic (config-dependent) positions for optional aux tensors: forward builds the pack dynamically based on the real bias_type, while backward forces a "dummy" always-bias-present bias_type/backend so nvte_fused_attn_bwd can pull from fixed, hardcoded slot indices internally. That static-slot assumption doesn't scale once a second independent optional tensor (sink) is added — bias and sink can each be present or absent independently, so a fixed index for either one is wrong in at least one of the four combinations. For ROCm, both forward and backward now build the pack the same way, with every optional tensor (bias, sink) appended sequentially and only when actually enabled — no dummy/static slot assumptions — and fused_attn.cpp/fused_attn_ck.cpp read the aux tensors back out using the same dynamic running index instead of hardcoded tensors[2]/tensors[3].
  • Fixes a pre-existing positional-argument bug in the CUDA path(PrepareFusedAttnForwardAuxTensors): pre-PR the call was ..., softmax_aux, softmax_offset), which bound softmax_offset to the rng_state_buf parameter

Fixes # (issue)
CK code in QoLa

  • fmha_bwd_kernel.hpp indexed sink_ptr
    as i_batch * nhead + i_nhead, but the buffer holds one value per head (shape
    [nhead]), matching how the forward kernels already read it, so i_batch must not
    factor into the offset.
    Affected all bwd tests
  • CK fwd: K/V windows double-offset with sink + local mask. In BlockFmhaPipelineQRKSVS the K/V/bias windows are created at kv_load_start (already == seqlen_k_start), and the kHasSink block moved them again by seqlen_k_start - sink_seq_end. TE uses no StreamingLLM prefix (sink_size = 0 → num_sink_loop == 0), so the guard i_total_loops == 0 fired on the first iteration instead of never — live here because this pipeline increments in while(++i_total_loops ...), dead in the async one, which is why only bias configs (qr) broke and no-bias (qr_async) was fine. Any query tile whose window starts past column 0 read wrong/out-of-range K/V → garbage output
    18 tests fixed: POST_SCALE_BIAS-1HSS-{Mask,Seqlens,SegmentIDs}-SWA-DROP_0.0-<cfg>-LEARNABLE_SOFTMAX-<mask>

Type of change

  • Documentation change (change only to the documentation, either a fix or a new content)
  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Infra/Build change
  • Code refactoring

Changes

Please list the changes introduced in this PR:

  • Change A
  • Change B

Checklist:

  • I have read and followed the contributing guidelines
  • The functionality is complete
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

Comment thread transformer_engine/jax/csrc/extensions/attention.cpp Outdated
Comment thread transformer_engine/common/fused_attn_rocm/fused_attn_ck.cpp Outdated
Comment thread transformer_engine/jax/csrc/extensions/attention.cpp Outdated
Comment thread transformer_engine/common/fused_attn_rocm/fused_attn_ck.cpp Outdated
@github-actions

Copy link
Copy Markdown

Review summary

Reviewed the sink-support diff (fwd + bwd for CK/AITER, JAX aux-tensor plumbing, one new test). Scope focus was the actual PR changes (merge-base → PR head), i.e. fused_attn_rocm/fused_attn{.cpp,_ck.{cpp,h}}, ck_fused_attn/*, jax/csrc/extensions/attention.cpp, and tests/jax/test_fused_attn.py.

High-level verdict: approach is sound — sink is threaded through as a new optional aux tensor with dynamic slot indexing on both the fwd write and bwd read sides, the ASM v3 fallback is symmetric between fwd/bwd, and d_sink zeroing matches the CK atomicAdd contract. One likely CUDA regression and two comment/documentation nits — see inline.

Highlights:

  • 🔴 jax/csrc/extensions/attention.cpp:135 — CUDA dummy_backend changed from NVTE_F16_arbitrary_seqlen to NVTE_No_Backend; this looks like it silently disables the bwd aux-tensor-pack population path on CUDA. Please confirm this was intentional.
  • 🟡 jax/csrc/extensions/attention.cpp:300 — a latent positional-argument bug in the shared CUDA/ROCm path is fixed here; worth calling out in the PR description as a generic upstream-eligible fix.
  • 🟡 fused_attn_ck.cpp:151-159 and :80-88 — the two new is_ck_backend_supported guards would benefit from expanded comments explaining the exact failure mode / when they can be lifted.
  • 🟢 Minor: duplicate has_sink computation in fwd logging vs. args build.

Copyright headers: OK — all 8 modified files carry AMD headers with end-year 2026; no NVIDIA copyright years were touched.

Comment thread transformer_engine/jax/csrc/extensions/attention.cpp Outdated
Comment thread transformer_engine/common/fused_attn_rocm/fused_attn_ck.cpp Outdated
Comment thread transformer_engine/common/fused_attn_rocm/fused_attn_ck.cpp
Comment thread transformer_engine/jax/csrc/extensions/attention.cpp Outdated
@github-actions

Copy link
Copy Markdown

Re-review summary

Re-reviewed since the last pass (commit 85bb3d3); three commits have landed since — 342335f (swa + sink fix), 1120c76 (removed fixed code), 2fc6952 (merge origin/dev).

Prior findings — all addressed:

  • 🔴 CUDA dummy_backend regression: fixed. PrepareFusedAttnBackwardAuxTensors now restores NVTE_F16_arbitrary_seqlen on the CUDA path and only takes the dynamic-pack branch under USE_ROCM.
  • 🟡 Positional-argument fix at the fwd aux-prep call site: now called out in the PR description as a generic upstream-eligible fix.
  • 🟢 Duplicate has_sink computation in the fwd path: now computed once and reused.

New findings (4 inline):

  • 🟡 jax/csrc/extensions/attention.cpp:108 — the sink aux slot is written under softmax_offset_buf != nullptr while all three readers (CUDA and both ROCm) gate on softmax_type != NVTE_VANILLA_SOFTMAX. Benign today, but it's the one optional tensor whose writer/reader predicates disagree.
  • 🟡 fused_attn_rocm/fused_attn_ck.cpp:1032 — the new && !has_sink is intentionally not mirrored into the workspace-size query at :777, and that asymmetry is load-bearing because has_sink differs between JAX's sizing pass and execution. Needs a comment so it isn't "fixed" into an under-allocation.
  • 🟢 fused_attn_rocm/fused_attn_ck.cpp:80softmax_type is now an unused parameter of is_ck_backend_supported; no CK-side guard on sink remains.
  • 🟢 jax/csrc/extensions/attention.cpp:138-142[[maybe_unused]] spacing and trailing whitespace (5 spots across 4 files) that the repo's pre-commit hooks will rewrite.

Verdict: approach is sound and the dynamic aux-slot indexing now matches the CUDA reference implementation's structure. Nothing blocking; the two 🟡 items are worth resolving before merge.

Copyright headers: OK — all 8 modified source files carry AMD headers ending in 2026; no NVIDIA copyright years were touched.

@shurale-nkn shurale-nkn added the ci-level 3 CI test level 3 label Jul 30, 2026
@Micky774

Copy link
Copy Markdown
Contributor

Hey there @shurale-nkn, thanks for the contribution! While we're looking into it, could you provide us some context regarding why this feature enablement is wanted/needed? What use case are you trying to enable? Thanks!

@shurale-nkn

Copy link
Copy Markdown
Author

Hey there @shurale-nkn, thanks for the contribution! While we're looking into it, could you provide us some context regarding why this feature enablement is wanted/needed? What use case are you trying to enable? Thanks!

Hi @Micky774, I need sink support for train in MaxText. At the moment, TE is the only provider of fused attention for this framework, and without this PR, GPT-OSS will not work correctly.

bias_type,
attn_mask_type,
softmax_type,
dropout,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Dropping softmax_type from is_ck_backend_supported also flips the PyTorch ROCm path onto CK, which this PR neither mentions nor tests.

Chain: PyTorch's Python-level gating already permits FusedAttention for non-vanilla softmax, and the ROCm carve-outs are explicit — dot_product_attention/utils.py:1061 skips the thd/cuDNN-version disable under IS_HIP_EXTENSION, and :1479 skips the determinism disable the same way. Until this commit the only thing stopping it was the C++ layer: CK rejected sink here and AOTriton still does (fused_attn_aotriton.cpp:72), so nvte_get_fused_attn_backend returned NVTE_No_Backend and DPA silently fell back to UnfusedDotProductAttention. With the guard gone, CK is selected.

Concretely, tests/pytorch/attention/test_attention.py::test_dpa_softmax and ::test_dpa_softmax_thd (15 configs each, num_gqa_groups=8 + causal/padding/SWA (128,0)) now run against CK on ROCm — they aren't cuDNN-gated here because get_cudnn_version() returns (99, 0, 0) for HIP (pytorch/utils.py:698), and ci/pytorch.sh:89 runs the whole file at TEST_LEVEL 1. That's a meaningful surface: GQA dk/dv expansion plus THD, i.e. the atomicAdd-per-head d_sink paths, on a framework whose aux-pack plumbing this PR didn't touch.

The 486-test JAX sweep you cited answers the CK-kernel question, but not the PyTorch-binding one. Could you confirm the ROCm PyTorch attention job is green on this branch? If it isn't yet, gating on framework (or keeping a narrow CK-side guard until PyTorch is validated) would be safer than enabling both frameworks in one commit.

size_t *workspace_size,
cudaStream_t stream){

const bool has_sink = softmax_type != NVTE_VANILLA_SOFTMAX;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟢 Good call dropping the devPtrSoftmaxOffset != nullptr term — that's what makes the earlier uses_bwd_v3 concern go away rather than just move. has_sink is now derived purely from softmax_type, which JAX passes identically to the sizing call (jax/csrc/extensions/attention.cpp:547) and the execution call (:683), so folding && !has_sink into nvte_ck_uses_bwd_v3 at :738 reaches ws_size_args.uses_bwd_v3 at :779 with the same value in both passes. ck_attn_bwd_workspace_size then returns max(v2_bytes, 0) == v2_bytes, which is exactly what the tile path needs. The ck_fused_attn_bwd.cpp:480 comment about mirroring use_asm_v3 is literally true again too.

One thing the old term was incidentally covering: has_sink is now true even when the offset pointer is null, so ck_args.sink_ptr (:644, :1035) and ck_args.d_sink_ptr (:1036) can reach CK as nullptr with has_sink == true, and the bwd cudaMemsetAsync(devPtrDSoftmaxOffset, ...) at :936 would memset a null pointer. Both sizing passes return before those points (:547, :897), so JAX is fine, and PyTorch's DPA always allocates the buffer for non-vanilla softmax (dot_product_attention.py:490-499). But pytorch/csrc/extensions/attention.cpp:231 and :308 explicitly tolerate softmax_type != VANILLA with SoftmaxOffset == nullopt, and that combination now reaches CK instead of being rejected — previously a silent no-op, now a null deref.

Since the pointer is a hard requirement once has_sink is set, an NVTE_CHECK just after the sizing early-return would turn that into a clear error instead of a GPU fault:

NVTE_CHECK(!has_sink || devPtrSoftmaxOffset != nullptr,
           "softmax_offset is required for non-vanilla softmax");

Minor while you're in here: removing the old has_sink line left a double blank at :484-485, and .clang-format sets MaxEmptyLinesToKeep: 1.

void *softmax_buf,
void *rng_state_buf, void *bias_buf,
void *softmax_offset_buf = nullptr) {
#ifndef USE_ROCM

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟢 The sink slot is symmetric now, but the bias slot on this CUDA branch still isn't — and that only started mattering once a second optional tensor was appended behind it.

dummy_bias_type = NVTE_POST_SCALE_BIAS makes the writer always emit a bias slot at index 2. The CUDA bwd reader (common/fused_attn/fused_attn.cpp:701-711) walks the pack with a running index and consumes the bias slot only under the real bias_type:

if ((bias_type != NVTE_NO_BIAS) && (bias_type != NVTE_ALIBI)) {
  input_Bias = convertNVTETensorCheck(Aux_CTX_Tensors->tensors[i++]);
}
if (softmax_type != NVTE_VANILLA_SOFTMAX) {
  input_SoftmaxOffset = convertNVTETensorCheck(Aux_CTX_Tensors->tensors[i++]);
}

So for NO_BIAS + non-vanilla softmax the writer produces [softmax, rng, bias(dummy), sink] while the reader skips bias and picks up tensors[2] — the dummy bias slot, carrying bias_buf and a {bias_batch, bias_heads, q, kv} shape — as input_SoftmaxOffset. Before sink existed the extra trailing slot was simply ignored, which is presumably why the dummy survived this long.

Not a regression from this PR (the base had the same layout via softmax_offset_buf != nullptr), and I can't run the CUDA path to confirm, so treat this as a question rather than a claim. But the fix looks like the one you already applied on the ROCm side — pass the real bias_type here too, since the reader is dynamic on both slots:

Suggested change
#ifndef USE_ROCM
#ifndef USE_ROCM

…with dummy_bias_type replaced by bias_type at line 151. If that's deliberate scope-limiting for a ROCm fork PR, an upstream issue link in the description would be enough.

@github-actions

Copy link
Copy Markdown

Re-review summary

Re-reviewed since the last pass (commit 2fc6952); two commits have landed — 0368904 (format + relocation) and 17cec09 (CI fix for the distributed hlo check).

Prior findings — all addressed:

  • 🟡 Writer/reader predicate mismatch on the sink aux slot: fixed. PrepareFusedAttnForwardAuxTensors now takes NVTE_Softmax_Type and gates on softmax_type != NVTE_VANILLA_SOFTMAX, matching all three readers and the bias_type slot's symmetry.
  • 🟡 && !has_sink vs. the workspace-size query: resolved at the root rather than papered over. has_sink no longer depends on the offset pointer, so it is identical in JAX's sizing and execution passes and the two passes agree by construction.
  • 🟢 Dead softmax_type parameter on is_ck_backend_supported: removed, along with its declaration and call site.
  • 🟢 [[maybe_unused]] spacing and trailing whitespace: cleaned up across all four files.

New findings (3 inline):

  • 🟡 fused_attn_rocm/fused_attn.cpp:312 — removing the CK sink guard also switches the PyTorch ROCm path onto CK. test_dpa_softmax / test_dpa_softmax_thd run on ROCm and are in ci/pytorch.sh's TEST_LEVEL 1 scope, so 30 GQA/THD/SWA configs newly exercise the CK sink path on bindings this PR didn't touch. Asking for confirmation that the PyTorch job is green.
  • 🟢 fused_attn_rocm/fused_attn_ck.cpp:470 — the pointer null-check that used to be folded into has_sink is gone, so sink_ptr / d_sink_ptr can now reach CK as nullptr with has_sink == true via a call path PyTorch's C++ binding explicitly permits. Suggested an NVTE_CHECK after the sizing early-return.
  • 🟢 jax/csrc/extensions/attention.cpp:145 — the CUDA branch's dummy_bias_type leaves the bias slot asymmetric with the reader, which now shifts the sink slot for NO_BIAS + non-vanilla softmax. Pre-existing and CUDA-only, raised as a question.

Also verified the new distributed-test collective accounting: with_softmax_offset matches the library's LEARNABLE_SOFTMAX gate at cpp_extensions/attention.py:1244, and the tpsp_resource-only tp_size is correct because generate_configs() never sets a bare tp_resource.

Verdict: the aux-slot indexing is now consistent end to end and the v3-gating/workspace-sizing interaction is correct. Nothing blocking; the PyTorch-enablement question is the one worth answering before merge.

Copyright headers: OK — all 9 files in scope carry AMD headers ending in 2026; no copyright lines were modified by this PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci-level 3 CI test level 3

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants