Skip to content

feat(attention): add strict bitwise ROCm path - #319

Open
inaniloquentee wants to merge 46 commits into
testfrom
codex/ws2-rocm-strict-attention
Open

feat(attention): add strict bitwise ROCm path#319
inaniloquentee wants to merge 46 commits into
testfrom
codex/ws2-rocm-strict-attention

Conversation

@inaniloquentee

@inaniloquentee inaniloquentee commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Adds the strict ROCm Attention path and makes it reachable from Vime through contract-aware dispatch, plus a Triton core that is bitwise-identical to the native reference core.

Scope: #235 lists ROCm under Non-goals and its PR1–PR8 stack is CUDA-only. This does not implement a #235 deliverable; it extends the #235 attention contract to ROCm — same AttentionContract, same attention-domain LSE, same global_block_index merge order, same fail-closed policy.

Operator-only throughout: seeded Q/K/V, no checkpoint or serving engine. 8×MI300X (gfx942), torch 2.12.0+rocm7.14.0, HIP 7.14.60850, Triton 3.7.0. Qwen3-8B shapes (Hq=32, Hkv=8, D=128), BF16 causal unless noted.

What is claimed, and what is not

Scope Claim
Batch composition bitwiseB>1 rejected at the core
Padding bitwisekey_padding_mask rejected
TP degree bitwise — one KV group per launch
CP degree bitwise — RCCL used as transport only
Triton core vs native reference core bitwise
ROCm production core vs reference core not claimed — different kernels
CUDA vs ROCm production core not claimed — FA4 CuTe vs AITER CK

Bitwise equality is asserted within one ROCm hardware and runtime stack. The self-owned deterministic core stays a reference oracle, not the production arithmetic path.

Algorithmic arrangements

The core does not reimplement attention. It removes each source of arithmetic variation from the vendor kernel and records what was removed, so a mismatch is a contract violation rather than a debugging session.

# Arrangement Mechanism
1 Split-KV structurally impossible AITER has no num_splits; bind the dense non-split API (split_kv_control="dense_non_split_api"). Non-DISABLED SplitKVSpec raises.
2 Batch composition cannot vary _validate_inputs rejects q.size(0) != 1. The batched launch does not exist, so it cannot disagree.
3 Padding never enters a reduction key_padding_mask rejected; each unpadded logical row is materialized.
4 TP-degree independence One launch per (batch row, KV group), so a shard's result never depends on the launch head count.
5 RCCL is transport, never reduction all_gather + root-owned scatter; reduce_scatter gathers then evaluates a fixed balanced rank tree locally.
6 Vendor kernel fingerprinted sha256 of the aiter.ops.mha source file in provenance — a version number does not pin Python-level dispatch.
7 Fail closed Missing AITER entry point, missing extension, or a dispatch that resolves elsewhere all raise. No substitution.
8 Reference core shared with CUDA deterministic_attention.cu hipified to .hip; the new Triton core is bit-identical to it.

Arrangements 2 and 4 are load-bearing rather than defensive, because raw AITER fails both only at some shapes — the failure mode most likely to reach production unnoticed:

Failure Shapes measured Worst drift
Batch composition fails at B=2/S=512, B=4/S=256, B=4/S=512; clean at B=2/4 × S=128, 1024–4096 1.5625e-02
TP degree fails at 5 of 12 (S, TP) points; clean at S=512 all TP, and S=4096/TP=2 7.8125e-03

Changes

  • Contract-aware dispatch: KernelRegistry.register_attention_backend / get_attention_op(contract, requested_backend=...), candidate list separate from the legacy attn priority maps and empty by default, so a WS2 caller can never be served by an SDPA-shaped wrapper without attention-domain LSE.
  • Register aiter.rocm.ck_dense_mha only when aiter.ops.mha genuinely imports; an explicit request fails rather than silently becoming the PyTorch path.
  • rl_engine/integrations/vime/attention.py: runtime provider taking and returning structural objects, so RL-Kernel never imports Vime.
  • Derive and validate query_position_ids / key_position_ids so a training full-sequence call and a rollout chunk provably describe the same logical tokens.
  • AttentionContract.cross_rank_fingerprint() for cross-rank preflight; requested_backend="auto" rejected under CP>1 without it.
  • New: rl_engine/kernels/ops/triton/attention/deterministic_attn.py — Triton core bitwise-identical to _C.deterministic_attention_*, so the reference arithmetic is exercisable without the vendor kernel. tests/test_triton_deterministic_attention.py, 71 tests.
  • New: benchmarks/benchmark_ws2_rocm_attention.py + benchmarks/results/ws2_rocm_mi300x/ — measurement matrix and charts, reusing the PR feat(ffn): add deterministic distributed Triton FFN for ROCm #325 / feat(logprob): add deterministic ROCm vocab-parallel path #328 helpers and figure style.
  • Fix: csrc/ops.cpp — the merge from feat/rocm-deterministic-collectives dropped an #if !defined(USE_ROCM) around the prefix_shared_attention registration but kept its #endif (13 #endif vs 12 #if). ROCm builds failed with #endif without #if.

Validation

Gate Result
Attention contracts, CP harness, provider + dispatch 284 passed
Triton bitwise core 71 passed
Train vs rollout, backward determinism max abs 0
Fail-closed (decode, dropout, sliding window, soft-cap, ALiBi, FP32, non-contiguous positions) all refused

Every strict report records fallback=false, native_attention_arithmetic=true, actual_backend=aiter.rocm.ck_dense_mha, deterministic backward, disabled Split-KV, communication_backend=rccl_ag_rs.

Results

Measured after the per-KV-group rule landed. Full data: benchmarks/results/ws2_rocm_mi300x/.

Triton core vs native reference core — 0 mismatched elements

dtype S out lse dQ dK dV
bf16 512 / 1024 / 2048 / 4096 0 0 0 0 0
fp16 512 / 1024 / 2048 / 4096 0 0

Three things had to be reproduced rather than re-derived: the sequential FMA chain (contraction index is the loop, head dim is the vector), the 256-lane partial + stride-halving softmax fold, and expf/logf. The last is the trap — every Triton exp/log intrinsic lowers to a bare v_exp_f32, ~1 ULP from the expf the C++ kernel calls, which alone broke parity on ~14% of elements. The helpers re-emit hipcc's two-term argument reduction around that same instruction, with an inline-asm barrier to stop LLVM refolding it into an FMA. Verified over 4M+ inputs including subnormals, ±inf and NaN.

TP-degree invariance

raw_launch is one launch for all heads; per_kv_group is what the provider runs.

S TP raw_launch out max-abs per_kv_group
512 2 / 4 / 8 0 0
1024 2 / 4 / 8 7.8125e-03 0
2048 2 0 0
2048 4 / 8 3.9e-03 / 1.9e-03 0
4096 2 / 4 0 0
4096 8 3.9e-03 0

Cost of the per-KV-group schedule — corrects the earlier ~3x estimate

S sdpa (ms) raw_launch (ms) per_kv_group (ms) vs raw vs sdpa
512 0.0712 0.2579 1.7759 6.89x 24.95x
1024 0.1302 0.2513 1.9985 7.95x 15.35x
2048 0.2802 0.2917 1.7507 6.00x 6.25x
4096 0.7046 0.5682 2.0472 3.60x 2.91x

The real multiplier is 4.11–7.31x, not ~3x, worst at short sequence where per-launch overhead dominates. Still worth buying, given the invariance table above, but the bill should be stated correctly.

Single device (BF16, B=1)

strict-aiter is the core called once for all heads — not the production schedule; see the table above. sdpa is a speed baseline only, with no accuracy comparison mixed in.

S Platform Path Fwd (ms) Fwd+bwd (ms) Fwd peak MiB Fwd+bwd peak MiB out max-abs vs FP64
512 mi300x sdpa 0.0785 0.2836 12.1 32.2 8.20e-03
512 mi300x pytorch-native 0.1981 0.9766 44.2 76.3 1.39e-02
512 mi300x strict-aiter 0.2475 0.6328 14.1 288.2 2.47e-02
512 mi300x reference-native 1.0180 3.1217 36.1 78.1 7.74e-03
512 mi300x triton-bitwise 1.3627 4.8656 36.1 78.1 7.74e-03
512 cpu sdpa 31.5270 53.7609 0.0 0.0 8.61e-03
512 cpu pytorch-native 15.3921 24.7810 32.7 46.4 1.95e-02
2048 mi300x sdpa 0.2875 1.0837 48.3 128.8 7.99e-03
2048 mi300x pytorch-native 1.0848 2.3871 564.0 1076.0 1.80e-02
2048 mi300x strict-aiter 0.2938 1.9570 56.3 4224.8 2.03e-02
2048 mi300x reference-native 12.7428 47.8157 528.2 1080.5 7.80e-03
2048 mi300x triton-bitwise 19.4210 76.7789 528.3 1080.5 7.80e-03
2048 cpu sdpa 304.6115 533.5067 50.5 31.7 9.31e-03
2048 cpu pytorch-native 216.1569 422.9041 543.6 776.1 1.79e-02
4096 mi300x sdpa 0.6965 3.2163 96.5 257.5 9.60e-03
4096 mi300x pytorch-native 3.9513 9.3608 2160.0 4208.0 1.40e-02
4096 mi300x strict-aiter 0.5569 5.7263 112.5 16641.5 2.14e-02
4096 mi300x reference-native 49.3536 173.3246 2080.5 4209.0 7.81e-03
4096 mi300x triton-bitwise 105.4049 306.5966 2080.5 4209.0 7.81e-03

S=1024 and the FP16 sweep are in the report; same shape of result. The host run covers S<=2048 and only the two paths that exist there; its peak memory is an RSS high-water delta from /proc, not an allocator statistic, so it is an approximation and not directly comparable to the device figures.

Two readings. The deterministic backward is the binding constraint, and it is worse than materializing the whole score matrix: at S=4096 strict-aiter peaks at 16.6 GiB while reference-native — which writes a full FP32 [B, Hq, Sq, Skv] buffer — peaks at 4.2 GiB. This belongs to AITER's mha_bwd, not to the integration: toggling only the deterministic flag on raw AITER gives 1188 → 4329 → 16753 MiB at S=1024/2048/4096 (O(S²)) against 180 → 265 → 433 MiB with it off, and raw AITER at S=4096 peaks within 65 MiB of the provider. And the deterministic cores are the most accurate of the four against an FP64 oracle (7.8e-03 vs 9.6e-03 SDPA, 2.1e-02 AITER) — determinism is not bought with accuracy here.

Distributed CP (RCCL AG/RS)

All-gather Q/K/V and position ids over the CP group, strict core once on the full sequence, reduce-scatter (out, lse) back to this rank's query range. Acceptance is bitwise against CP=1. S=4096.

Topology World TP CP Rep local Hq/Hkv Fwd (ms) p95 MiB/rank out / lse / repeat
tp1_cp2 2 1 2 1 32/8 1.8352 1.8726 160.5 bitwise
tp2_cp2 4 2 2 1 16/4 1.2114 1.3138 80.3 bitwise
tp1_cp4 4 1 4 1 32/8 1.3973 1.4363 160.5 bitwise
tp2_cp2_x2 8 2 2 2 16/4 1.2297 1.2948 80.3 bitwise
tp2_cp4 8 2 4 1 16/4 1.2594 1.3294 80.3 bitwise
tp1_cp8 8 1 8 1 32/8 1.4112 2.2029 160.5 bitwise

0 mismatched elements summed across every rank. Requires the native extension built for the platform; otherwise it fails closed on the ROCm deterministic RoPE operator rather than substituting another. The separate multi-rank acceptance run of the full strict shared core (all five tensors, RoPE included) is under benchmarks/results/pr319_rocm_mi300x/distributed/; the stale pre-per-KV-group performance artifacts that used to sit beside it have been removed.

Charts

Single-device latency and memory

Bitwise exactness matrix

Every measured cell is 0; n/m marks the FP16 gradient columns, which are not measured.

TP-degree invariance

Blue is one launch for all heads, orange is one per KV group; the flat orange line at the 1e-12 floor is bitwise. reference-native and triton-bitwise allocate identical buffers, so their memory curves coincide and the later-drawn series hides the earlier.

Distributed CP latency

Known limits

Stated rather than worked around, because a silent approximation would be worse than a refusal.

  • Backward memory is O(S²) — ~16.6 GiB for one sequence at S=4096. Long-context training on this path is not viable until AITER offers a deterministic backward with bounded workspace; forward-only / rollout is unaffected.
  • TP invariance costs 4.1–7.3x forward time. The single-launch alternative is faster and not invariant.
  • CP>1 and decode are refused by the provider. Doing the cross-rank merge inside it would add a second merge order; decode needs KV-cache identity metadata the dense core does not materialize. CP goes through the transport path above.
  • The Triton core's bitwise claim does not extend to CUDA. The nvcc expf/logf reductions are not ported and no CUDA device was available to verify them, so the op raises on CUDA unless the caller passes require_bitwise_libm=False. A test pins this on both platforms.
  • The Triton core is a parity core, not a FlashAttention replacement — it materializes the full FP32 score matrix.
  • _C does not register deterministic_attention_forward_fp32. The .cu defines it but the pybind registration is missing, so the native forward_fp32 raises AttributeError. Pre-existing, untouched.
  • A stale comment contradicts the shipped TP policy. vime/attention.py still says RL-Kernel "binds the degree rather than paying ~3x forward time", from before the per-KV-group loop; the provenance dict below it correctly reports tp_degree_invariant: True.
  • H100 not measured yet. The benchmark supports it (strict-fa4 replaces strict-aiter, the CP transport dispatches to CUDA IPC, and the Triton core is built with the bitwise opt-out since the nvcc libm sequence is unported); the run itself is pending. No cross-vendor claim.
  • The host run covers S<=2048. S=4096 on CPU was dropped after a first attempt was killed; the host column is absolute-latency context, not a headline.

Reproduce

PYTORCH_ROCM_ARCH=gfx942 python setup.py build_ext --inplace

python -m pytest -q \
  tests/test_attention_correctness.py tests/test_flashinfer_pr7_attention.py \
  tests/test_vime_attention_provider.py tests/test_attention_dispatch.py \
  tests/test_triton_deterministic_attention.py

python benchmarks/benchmark_ws2_rocm_attention.py \
  --seq-lens 512,1024,2048,4096 --dtypes bf16,fp16 \
  --warmup 5 --samples 20 --training-samples 10 \
  --output-dir benchmarks/results/ws2_rocm_mi300x

@coderabbitai

coderabbitai Bot commented Aug 19, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3c2674e4-e976-4ae3-b404-79b191f36cfb

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

Resolved conflicts in envs.py and setup.py; both sides were additive:
- envs.py: keep RL_KERNEL_REQUIRE_EXT alongside the new
  KERNEL_ALIGN_FORCE_ASCEND / KERNEL_ALIGN_ASCEND_ARCH constants.
- setup.py: keep both import sets; append the Ascend extensions before
  the native-extension-required check so an Ascend-only build does not
  trip the CUDA/ROCm 'no build environment' error.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QfN3b2ep5DvxVDir7J36j3
@zhangj1an
zhangj1an changed the base branch from main to test August 24, 2026 12:07
zhangj1an and others added 2 commits August 24, 2026 13:36
…patch

Adds the dispatch seam and Vime adapter that make the strict ROCm attention
core reachable as an explicit, fail-closed backend, mirroring the WS2
logprob provider in #328.

Registry:
- register_attention_backend / get_attention_op over AttentionContract, with
  a candidate list kept separate from the legacy attn/attention priority maps
  and empty on every platform by default. A WS2 caller can therefore never be
  served by an SDPA-shaped wrapper that does not export attention-domain LSE,
  and the strict core can never be selected by a legacy get_op caller.
- aiter.rocm.ck_dense_mha is registered only when aiter.ops.mha genuinely
  imports and exposes mha_fwd/mha_bwd. An explicit request for it fails
  loudly when the vendor stack is absent instead of degrading to a different
  backend.

Vime adapter (rl_engine/integrations/vime/attention.py):
- Structural request in, (out, lse) plus provenance out; RL-Kernel never
  imports Vime, and native fallback is signalled through an
  attention_provider_unavailable marker rather than an imported type.
- Each logical batch row is materialized on its own. Raw AITER mha_fwd is
  batch-composition sensitive in BF16 at some shapes (S=256 B=4, S=512
  B=2/B=4; up to 1.5625e-02) while invariant at others, so batching would
  otherwise change the bits for a subset of shapes only.
- Position identity is derived and validated, so a training-side full
  sequence and a rollout-side chunk provably describe the same tokens.
- CP>1 and decode fail closed rather than being served by a core that does
  not own the cross-rank merge or KV-cache identity.

Contract:
- AttentionContract.cross_rank_fingerprint() for cross-rank preflight,
  mirroring LogprobContract; auto dispatch stays rejected under CP>1.

Benchmarks (MI300X, gfx942, torch 2.12.0+rocm7.14, BF16, Qwen3-8B heads):
forward is within 4% of SDPA and ~1.9x faster than the Triton FlashAttention
backend at S=4096. The deterministic backward costs ~2.4x time and scales
O(S^2) in memory (16.8 GiB at S=4096); that cost is AITER's mha_bwd, not the
integration - raw AITER peaks at 16657 MiB against the provider's 16722 MiB.

Tests: 38 new cases; the full suite is unchanged against the pre-change
baseline (27 failed / 224 skipped / 607 errors both before and after, all
from the unbuilt native extension), with passed rising 1068 -> 1102.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QfN3b2ep5DvxVDir7J36j3
The strict ROCm path is not TP-degree invariant. TP shards attention by head
and performs no cross-rank reduction, so a rank's head slice ought to match
the same slice of an unsharded run - but AITER's reduction order depends on
how many heads shared the launch. Measured on MI300X (BF16, Hq=32/Hkv=8/
D=128, causal), local shard vs unsharded:

    S=512   TP=2/4/8   bitwise
    S=1024  TP=2/4/8   out max abs 3.90625e-03
    S=2048  TP=4/8     out max abs 7.8125e-03   (TP=2 bitwise)
    S=4096  TP=8       out max abs 7.8125e-03   (TP=2/4 bitwise)

This is the batch-composition sensitivity again, on the head axis, and it is
just as shape-dependent: training on TP=4 and rolling out on TP=8 would not
compare bitwise, while most shapes would look fine.

Executing one KV group per launch removes the dependence entirely (verified 0
at TP=1 vs 4 vs 8) but costs 2.7-3.9x forward time. We bind the degree
instead:

- validate_cross_config_alignment(training, rollout) fails closed and names
  the field that diverged; CROSS_CONFIG_BOUND_DEGREES records what is bound.
- cross_rank_fingerprint() already includes tp/cp_world_size, so the standard
  distributed preflight separates degrees without extra work.
- Provider results carry a cross_config_binding provenance block recording
  tp_degree_invariant=false and the bound degrees.

Multi-rank strict CP now validated on 8xMI300X via rccl_ag_rs, out/lse/dQ/dK/
dV bitwise at 2 ranks (TP=1,CP=2), 4 ranks (TP=2,CP=2), and 8 ranks (TP=2,
CP=2, 2 replicas). This needs the native extension built for the platform;
without it the strict path fails closed on the ROCm deterministic RoPE
operator rather than substituting a different one. Artifacts under
benchmarks/results/pr319_rocm_mi300x/distributed/.

With the extension built the suite goes from 27 failed / 1102 passed / 607
errors to 6 failed / 1797 passed / 0 errors; the 6 remaining are pre-existing
and unrelated (3 multi-process collectives, a benchmarks package shadowed by
site-packages, a CPU/ROCm linear_logp routing mismatch, one ws1 chain case).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QfN3b2ep5DvxVDir7J36j3
@Flink-ddd Flink-ddd added platform: rocm Specific tasks specific to AMD graphics cards (such as CK, bpreshuffle/FA) and removed platform: rocm Specific tasks specific to AMD graphics cards (such as CK, bpreshuffle/FA) labels Aug 24, 2026
zhangj1an and others added 2 commits August 24, 2026 16:57
Also makes the strict path TP-degree bitwise invariant.

Merge resolution (12 conflicted files):
- registry.py: test grew its own get_attention_op / _attention_policy_mismatch
  / _platform / _get_or_create_backend, keyed off a `ws2_attention` priority
  list and a flat _attention_capabilities dict. Dropped our parallel
  implementation and rebuilt on test's, keeping only the register_attention_
  backend seam (a vendor-conditional backend cannot be declared in a static
  list) and the auto-under-CP guard. Corrects an error on our side:
  requested_backend="deterministic" was rejected by copying a logprob-dispatch
  rule, but AttentionBackendCapability admits "deterministic" and it is test's
  default.
- csrc/ops.cpp: test registers the CUDA-IPC collectives unguarded, but their
  declarations stay ROCm-guarded, so taking that side fails to compile on ROCm.
  Kept our guard, which is the version this ROCm build actually succeeded with.
- flashinfer_paged_attention.py (9 hunks), ws2_p2p_nccl_*.py (3): test reverted
  to CUDA-only expressions (hardcoded cuda_ag_rs, direct StrictFlashAttention4
  Core). Ours is the platform-aware superset.
- cp_attention.py: took test's new saved-forward-state validation (#284).
- test_flashinfer_pr7_attention.py: the two sides were different tests at the
  same position (ROCm core accepted vs reference core rejected). Kept both.
- setup.py and pyproject.toml are EMPTY on test (0 bytes, from 3e04a63) and
  pyproject.toml auto-merged to empty with no conflict. Restored both; test
  cannot build a native extension in its current state.

TP-degree invariance: every launch is now pinned to one batch row and one KV
group. AITER's reduction order is launch-shape dependent, so a head shard
computed under TP=4 was not bit-identical to the same shard under TP=8 (up to
7.8125e-03 at some shapes). Pinning removes it: out/lse/dQ/dK/dV are bitwise
equal at TP=2/4/8 across S=512..4096, at roughly 3x forward time. The
cross-config contract therefore no longer binds the TP degree -- doing so would
reject comparisons that are in fact identical -- and checks only what changes
the arithmetic.

ROCm test skips: CUDA reports through the same device API on ROCm, so
device_count guards do not exclude it and CUDA-exclusive tests failed instead
of skipping. Added a `cuda_only` marker plus a conftest hook, applied to the
CUDA-IPC collectives, the CUDA det_gemm K-tree cases, and the FA4-selection
test (ROCm correctly resolves to the AITER core there, so its monkeypatch is
never consulted).

Attention suites: 323 passed, 42 skipped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QfN3b2ep5DvxVDir7J36j3
Conflicts in setup.py and pyproject.toml were whole-file: both files are
empty blobs at the merge base (64fe25a), so git had no common ancestor to
three-way merge against.

pyproject.toml: took test's file (adds the vllm general_plugins entry
point, drift-viewer extra, and pytest markers) and kept this branch's
flashinfer-python>=0.6.0,<0.7 pin from b83141e.

setup.py: kept this branch's ROCm gating -- deterministic_collective.cu
stays inside 'if not is_rocm' (it needs CUDA IPC), platform_define selects
KERNEL_ALIGN_WITH_ROCM, and _ascend_extensions is preserved. Grafted test's
-lcuda for the collective's driver-API calls, but gated on 'not is_rocm'
rather than test's os.name-only check, and guarded the SM90 append so the
flag is not added twice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0197vRbpULa4sh7aM47uQRrS
@Flink-ddd Flink-ddd added the platform: rocm Specific tasks specific to AMD graphics cards (such as CK, bpreshuffle/FA) label Aug 27, 2026
Flink-ddd and others added 26 commits August 28, 2026 16:48
…-attention

# Conflicts:
#	rl_engine/integrations/vime/__init__.py
#	tests/test_flashinfer_pr7_attention.py
…ves' into codex/ws2-rocm-strict-attention

# Conflicts:
#	csrc/ops.cpp
#	rl_engine/kernels/ops/cuda/attention/cp_comm.py
#	setup.py
Auto-merging csrc/ops.cpp while merging feat/rocm-deterministic-collectives
dropped the "#if !defined(USE_ROCM)" that guards the prefix_shared_attention
registration but kept its "#endif", leaving 13 #endif against 12 #if. A ROCm
build failed with "#endif without #if" and "'prefix_shared_attention' was not
declared in this scope".

Restore the guard around the registration so it matches the guard the
declaration already carries.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01824WkgMDxtBKD4Ex2NkfYA
Port csrc/cuda/attention/deterministic_attention.cu to Triton with a bitwise
contract: out/lse/P/dQ/dK/dV are bit-identical to _C.deterministic_attention_*
for the same inputs on the same device, forward and backward.

Three things had to be reproduced rather than re-derived:

- Dot products stay sequential FMA chains. The contraction index is the loop
  and the head dim is the vector, so no tree reduction is introduced where the
  C++ kernel accumulates one element at a time in a single thread.
- The row softmax keeps the 256-lane partial layout and the stride-halving
  shared-memory fold. _tree_sum_256 reproduces that fold exactly.
- expf/logf are re-emitted instruction for instruction from what hipcc
  generates. Every Triton exp/log intrinsic lowers to a bare v_exp_f32 /
  v_log_f32, roughly 1 ULP away from the vendor libm the C++ kernel calls, and
  LLVM otherwise refolds the argument reduction into an FMA (hence the inline-asm
  barrier). The helpers are verified bitwise over 4M+ inputs including
  subnormals, +/-inf and NaN.

The nvcc expf/logf sequences are not ported, so TritonDeterministicAttentionOp
refuses to construct on CUDA unless the caller passes require_bitwise_libm=False,
rather than silently returning non-bitwise results.

Forward runs about 1.4x the native reference kernel's time on MI300X; like that
kernel it materialises the full FP32 score matrix and is a parity core, not a
FlashAttention replacement.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01824WkgMDxtBKD4Ex2NkfYA
Applies the PR #325 / #328 measurement matrix and presentation to the strict ROCm
Attention path: their timing and accuracy helpers, their spawned distributed world,
and their figure style, so the three reports read side by side. Operator-only, no
checkpoint; Qwen3-8B shapes (Hq=32, Hkv=8, D=128).

Headline: the Triton core is bit-identical to _C.deterministic_attention_* on all
eight (dtype, sequence) cases -- out, lse, dQ, dK and dV, zero mismatched elements.

TP-degree invariance reproduces PR #319 on independent inputs and shows why the
per-KV-group launch schedule is load-bearing. Raw AITER is non-invariant at 5 of 12
(S, TP) points, up to 7.8e-03 out max-abs, and which points fail is shape-dependent:
S=512 and S=4096/TP=2 both look clean. The per-KV-group schedule is bitwise at 12/12.

Distributed CP runs the real AG/RS schedule -- all-gather Q/K/V and position ids,
strict core on the full sequence, reduce-scatter (out, lse) -- and is bitwise against
CP=1 on all six topologies including the 8-rank TP=2/CP=2 x2-replica case.

Three things the numbers say that were not obvious:
- the strict production core is faster than SDPA at S=4096 forward (0.81x), so the
  bitwise arrangements cost almost nothing on the production path;
- AITER's backward peaks at 16.6 GiB at S=4096, 4x the materializing reference core;
- both deterministic cores are closer to an FP64 oracle than SDPA or AITER.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01824WkgMDxtBKD4Ex2NkfYA
…3x estimate

The strict-aiter row measures the core called once for all heads, which is NOT the
production schedule -- the Vime provider launches it once per (batch row, KV group).
Reading that row as the production cost understated the bill badly.

Measured directly, per_kv_group vs raw_launch forward:

    S=512   1.7717 ms vs 0.2488 ms   7.12x
    S=1024  1.8047 ms vs 0.2467 ms   7.31x
    S=2048  1.7629 ms vs 0.3067 ms   5.75x
    S=4096  2.4506 ms vs 0.5965 ms   4.11x

So the real multiplier is 4.11-7.31x, not the "roughly 3x" the PR description carried,
and it is worst at short sequence where per-launch overhead dominates. The invariance is
still worth buying -- raw AITER is non-invariant at 5 of 12 (S, TP) points -- but the
bill is now stated correctly, and the methodology relabels strict-aiter so the row is not
mistaken for the production schedule again.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01824WkgMDxtBKD4Ex2NkfYA
…artifacts

benchmarks/results/pr319_rocm_mi300x/{report.md,results.json} hold forward and
backward timings taken before the per-KV-group launch rule landed, so they measure
a schedule the code no longer runs. The PR description already carried a "these
numbers are stale" banner over them; benchmarks/results/ws2_rocm_mi300x/ supersedes
them in full.

The distributed/ subdirectory stays: it is multi-rank bitwise acceptance evidence for
the full strict shared core including ROCm RoPE, which is a different gate from the
new benchmark and does not go stale the way timings do.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01824WkgMDxtBKD4Ex2NkfYA
Three gaps, all found by comparing against what PR #325 actually plots and by
walking the code paths an H100 run would take.

Charts. PR #325's most distinctive figure is a mismatch heatmap (topology x tensor
category, annotated, RdYlGn), and we had no equivalent even though the data was
already collected. exactness_matrix.png adds it: Triton-vs-reference on the left,
CP-topology-vs-CP=1 on the right. Cells that were never measured (fp16 gradients)
render as "n/m" on grey rather than 0, which would read as measured-and-equal. The
distributed chart also had no baseline, so it could not show what the transport
costs; it is now grouped bars against CP=1, which puts AG/RS at 2.2-3.3x.

Host support. Timing falls back to wall clock and peak memory to an RSS high-water
delta from /proc when there is no device, and NativeAttentionOp joins as
pytorch-native -- the one non-SDPA path that also runs on the host. --device
selects, and the GPU-only sections are skipped rather than failed.

CUDA support, which the H100 run would otherwise have hit head-on. AITER does not
exist there, so strict-fa4 (StrictFlashAttention4Core) is the production core on
CUDA; TritonDeterministicAttentionOp raises on CUDA by design, so it is now built
with require_bitwise_libm=BITWISE_LIBM_PARITY and measured without claiming parity;
and the distributed case had StrictRocmAiterCKAttentionCore and the RCCL transport
hard-coded, which would have failed every CP topology on an H100 -- it now dispatches
on torch.version.hip. reference-hip is renamed reference-native, since on CUDA that
same .cu is not a HIP build.

--compare-with LABEL=PATH merges another platform's results.json into the report, so
mi300x, cpu and h100 runs land in one table with a Platform column; a missing row
means the backend cannot exist there, not that it failed.

Verified on ROCm after the refactor: all six CP topologies still bitwise against
CP=1 with transport reported as rccl_ag_rs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01824WkgMDxtBKD4Ex2NkfYA
The previous commit renamed the reference path reference-hip -> reference-native in
the code (the row is the CUDA build of the same .cu on an NVIDIA host, so the old
name would be wrong in an H100 report) but left the recorded results.json and
report.md on the old key. Nothing errored: PATH_NAMES simply stopped matching, so
regenerating the report silently dropped the reference row entirely -- the one row
the Triton core's whole bitwise claim is measured against.

Rename the key in the stored single-GPU cases and batch-composition rows, record the
production_path the strict-vs-reference gap was measured against, and regenerate.
Values are untouched; this is the same kernel under a name that is honest on both
platforms.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01824WkgMDxtBKD4Ex2NkfYA
…e script

Three fixes plus the CPU numbers.

The per-KV-group schedule cost was measured by a throwaway script and merged into
results.json by hand, so the full MI300X re-run silently dropped it. It is now
_tp_schedule_cost() inside the benchmark and part of the normal flow. Re-measured:
3.60-7.95x the single-launch forward (previously reported 4.11-7.31x; same
conclusion, run-to-run variance).

_environment() reported device facts regardless of device, so the host column
claimed gpu_count=8, hip=7.14 and an RCCL collective for a run that never touched a
GPU. It now zeroes those on a host run.

Host results, S<=2048, BF16: sdpa 31.5/98.3/304.6 ms and pytorch-native
15.4/51.6/216.2 ms forward at S=512/1024/2048. Only those two paths exist on the
host -- strict-aiter is ROCm-only and the reference and Triton cores need a GPU --
which is the same shape as PR #328's CPU column. S=4096 was dropped after a first
attempt was killed at 25 minutes; the host column is absolute-latency context, not
a headline.

MI300X was re-run so every platform has pytorch-native, the common path, and so the
reference core carries its platform-neutral name.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01824WkgMDxtBKD4Ex2NkfYA
The CPU sweep at S=4096 could not be priced in advance. My S^2 estimate said 24
minutes; ten minutes in it was still inside the first path, because the materialized
score matrix (4.3-8.6 GB) leaves cache and the run becomes memory-bandwidth bound,
so the compute model does not hold at that size. Two runs were killed by hand on
guesswork.

Price each (path, case) from the one untimed call that already runs to capture the
outputs, project the sampling cost from it, and skip the cell when that exceeds
--path-budget-seconds (default 900). A skipped cell is reported, not dropped: the
report grows a "Skipped cells" section carrying the observed single-call cost and
the projection, and the latency row reads "skipped" rather than going blank. The
FP64 accuracy figure survives, since it only needs the one call.

The observed cost is the useful part. fp16 pytorch-native at S=2048 is 13.2 s per
forward against 216 ms for the same shape in bf16 -- a 61x gap that measures
PyTorch's missing fp16 CPU matmul, not this operator.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01824WkgMDxtBKD4Ex2NkfYA
Signed-off-by: vensen <vensenmu@gmail.com>
CPU host numbers for the two paths that exist without a device: torch SDPA and
NativeAttentionOp. S=512/1024/2048, bf16 and fp16. Timing is wall clock and peak
memory an RSS high-water delta from /proc, so the host figures approximate and are
not directly comparable to the device columns.

The budget is now enforced twice: a pre-flight projection from the one untimed call
that already runs to capture outputs, and a wall clock inside the sampling loops,
because the projection under-estimates once the materialized score matrix leaves
cache. A cell that runs out of budget is truncated and flagged rather than dropped,
and one that cannot start is listed under "Skipped cells" with its observed
single-call cost.

Worth knowing before reading the host column: fp16 pytorch-native is 21.1 s per
forward at S=2048 against 228 ms for the same shape in bf16. That is PyTorch having
no optimized fp16 CPU matmul, not a property of this operator.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01824WkgMDxtBKD4Ex2NkfYA
Signed-off-by: maxiaosong1124 <maxiaosong7890@outlook.com>
feat(cuda): promote deterministic cross-config runtime and kernel validation to main
# Conflicts:
#	csrc/ops.cpp
#	rl_engine/distributed/collectives.py
#	rl_engine/kernels/ops/pytorch/ffn/ffn.py
…tch path

CP orchestration lived only in StrictCUDAAttentionRuntime, so ROCm had nowhere
to put it: the AITER/CK core is single-rank arithmetic, the Vime provider failed
closed at CP>1, and the only working AG/core/RS sequence was in the benchmark
script. StrictRocmAttentionRuntime mirrors the CUDA runtime over the RCCL AG/RS
transport.

Two things differ from the CUDA runtime and both are load-bearing:

- The core is launched once per (batch row, KV group) rather than once per
  sequence. AITER/CK's reduction order depends on how many heads shared the
  launch, so a head shard computed under TP=N is otherwise not bit-identical to
  the same shard under a different TP degree. FA4 has no such dependence.
- RCCL moves tensors but never reduces them. The cross-rank (out, lse) combine
  is the transport's fixed balanced rank tree, not RCCL's own algorithm
  selection, which varies with message size and topology.

The sequence reorder and position validation are bound from the CUDA runtime
rather than reimplemented, so the two runtimes cannot drift into two different
global orderings. The per-KV-group launch loop moves out of the Vime provider
into the runtime, so CP=1 and CP>1 now share one schedule instead of keeping a
second copy in the provider.

Opening CP also required the registry to stop rejecting it: cp_world_sizes was
(1,) and deterministic_cp_merge was False, so AttentionBackendCapability
rejected CP>1 twice over. cp_world_sizes now matches the world sizes the RCCL
transport accepts and deterministic_cp_merge is True because the merge order is
ours. A test pins the two together so the declaration cannot drift from the
transport. zigzag fails closed: the strict CP plan describes one contiguous
block per rank, and a zigzag rank owns two discontiguous runs.

Measured on 8xMI300X through attention_provider, not the transport directly, so
the test also pins that CP is reachable from the production dispatch path:
CP=2/4/8 are bitwise against a CP=1 run of the same core on the same logical
sequence, 0 mismatched elements on out and lse, repeat-bitwise on every rank.

Also corrects the stale TP comment the PR description flagged in section 7: the
shipped policy removes the degree dependence with the per-KV-group launch rather
than binding the degree to avoid a ~3x cost.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NPU7GosGZSdj6pKBWX7n2Y
… collective

The ROCm attention CP path owned a second transport implementation.
RCCLAGRSAttentionCPCommunication overrode _get_collective to construct
_RCCLRankOrderedTransport, which bypassed the collective_for_group factory
that the CUDA adapter goes through and that already dispatches to
RCCLDeterministicCollective on HIP.

Both copies evaluated the same balanced rank tree, so the two platforms were
bit-identical -- but only by coincidence. Nothing pinned them together, so a
later change to the shared collective's reduction order would have left the
attention path on the old tree with no test failing.

Delete _RCCLRankOrderedTransport and the override. ROCm now inherits the CUDA
adapter's resolution, so one implementation serves both platforms. The
reduction expression is unchanged, so this is not expected to move any bit.

_RootReduceScatterSequence falls back from scatter() to reduce_scatter()
because the shared collective exposes no scatter entrypoint. That is the same
branch the CUDA path has always taken and it is semantically equivalent --
non-root ranks zero their input, so the tree sum returns the root's value and
adding zero is exact. It costs one extra all-gather plus tree per call.

The error messages the adapters raise are now keyed off a collective_label
class attribute so the inherited path still reports RCCL on ROCm instead of
mislabeling itself as CUDA.

Three tests pin the arrangement: the two adapters must share one
_get_collective, the ROCm adapter must resolve through collective_for_group,
and the registry's cp_world_sizes must equal the shared collective's
_SUPPORTED_WORLD_SIZES so the capability declaration cannot drift from what
the transport accepts.

Not verified on MI300X. The reduction expression is unchanged, but the
reduce_scatter fallback and the shared collective's capacity and signature
validation are new to this path, so a CP=2/4/8 bitwise run is still owed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: zhangj1an <jianmusings@gmail.com>
Signed-off-by: vensen <vensenmu@gmail.com>
Signed-off-by: vensen <vensenmu@gmail.com>
Signed-off-by: maxiaosong1124 <maxiaosong7890@outlook.com>
…2-rocm-strict-attention

Brings in the ROCm HIP IPC deterministic collective so ROCm gets a native
transport instead of the RCCL-only Python path. Because this branch already
routes the attention CP adapter through collective_for_group, the ROCm
attention CP path now resolves to that HIP IPC collective with no further
change.

Conflict resolutions:

* cp_comm.py -- PR #357 optimizes _RCCLRankOrderedTransport.scatter; this
  branch deleted that class in favour of the shared collective. Kept the
  deletion: the optimization targets code that no longer exists, and the
  shared collective supersedes it.
* collectives.py -- kept both sides' module constants (they are additive:
  CUDA staging-frame sizes and ROCm IPC tuning thresholds). reduce_scatter_many
  had diverged signatures, so the merged one takes the union: PR #357's
  inputs/outs plus this branch's validate_signature, forwarding both.
* ffn.py -- PR #357 restructured the backward to compute both gate and up
  input gradients up front and pack them into one reduce_scatter_many, while
  this branch renamed _gemm_fwd to _linear_da/_linear_dw. Took PR #357's
  structure with this branch's helper names; the old second reduce-scatter
  for the up lane is gone.
* setup.py -- kept this branch's Ascend build imports (sysconfig,
  CompileError, find_executable, Extension are used further down the file)
  and added PR #357's ROCm .hip source to cuda_sources.
* ops.cpp, _C.pyi -- both additive; kept both sides.

tests/distributed and tests/test_build_platform_collectives: 46 passed,
5 skipped. tests/test_qwen_ffn.py fails 24 here, but 25 of the same tests
fail on the pre-merge tree in this environment: the extension was built
without KERNEL_ALIGN_DET_GEMM_SM90=1, so strict GEMM refuses to run. The
merge removes one of those failures and adds none.

Not verified on MI300X. The HIP IPC path has no coverage in this
environment, so the ROCm CP bitwise run is still owed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: zhangj1an <jianmusings@gmail.com>
… into

The CUDA core checks the FA4 CuTe API by parameter name before it runs, so a
renamed or dropped strict control fails at load. ROCm had no equivalent:
inspect.signature reports (*args, **kwargs) for AITER's JIT wrapper, so the
only guard was a SHA-256 of the module source. That catches "something
changed" but cannot say what, and it fires on unrelated edits.

Read the registered Torch schema instead (torch.ops.aiter.<op>.default).

The check is an ordered prefix, not a name set, because the two call sites
pass positionally. An argument inserted upstream would shift the meaning of
every later argument while the call still type-checks -- dropout_p, the two
window sizes and sink_size are all int/bool, so nothing would raise and the
kernel would run with silently reinterpreted controls. Name presence alone
does not catch that; the FA4 path is exempt only because it calls by keyword.

This also pins something that was previously unprovable: the True at
backward position 11 is the schema's `deterministic`. ROCm's backward was
already deterministic, but nothing tied that literal to its parameter.

The source fingerprint stays as a second line: the schema check describes what
changed, the fingerprint still catches a same-schema implementation change.

Verified against the installed AITER; the forward prefix matches exactly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: zhangj1an <jianmusings@gmail.com>
StrictCUDAAttentionRuntime has forward_paged_with_lse; the ROCm runtime had
no decode entry point at all, so decode-stage KV-cache replay existed only in
the device-neutral comparison harness with no ROCm path behind it.

No AITER paged kernel can serve this contract. paged_attention_rocm, _ragged
and _v1 are all two-pass partition reducers -- partition_size, with
exp_sums/max_logits/tmp_out partials -- so the partition count tracks the
cached length and Split-KV cannot be turned off. AITER's
flash_attn_varlen_func takes a block_table but exposes no num_splits to pin,
unlike CUDA's FA4. Either way the contract could not prove Split-KV disabled,
which attention_binding checks from both runtime evidence and the contract.

So the pages are gathered into logical order and handed to the same dense
core the prefill path uses, at the same one-launch-per (batch row, KV group)
granularity. The arithmetic is then identical to a CP=1 prefill over the same
logical sequence, which is what makes decode replay comparable against it. The
cost is materializing the cached KV; a native paged kernel avoids that and can
replace this once AITER can pin its split count.

The registry deliberately does NOT gain AttentionMode.DECODE. Nothing routes
to the new entry point: the Vime provider always calls forward_with_lse and
builds its contract with kv_cache=None, and its request carries no page table.
Declaring the mode now would let the binding layer accept a decode path that
never executes. A test pins the omission so it flips together with the
dispatch wiring rather than drifting ahead of it.

Tests inject the core, so they run without ROCm. They pin the part that is
ours rather than AITER's: a shuffled page table still yields logical KV order,
the gather truncates to seqused_k instead of exposing the page tail, each
launch still sees exactly one KV group, and the provenance says
paged_kernel=none so no reader mistakes this for a native paged path.

Not verified on MI300X. The core arithmetic is unchanged, but the gather's
index_select/reshape/permute and the claimed bitwise equality with a CP=1
prefill over the same tokens both need a real run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: zhangj1an <jianmusings@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

platform: rocm Specific tasks specific to AMD graphics cards (such as CK, bpreshuffle/FA)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants