feat(attention): add strict bitwise ROCm path - #319
Open
inaniloquentee wants to merge 18 commits into
Open
Conversation
inaniloquentee
requested review from
EthanZero2Hero,
Flink-ddd,
KJLdefeated and
bitborne
as code owners
August 19, 2026 09:22
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
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
…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
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR adds the ROCm strict Attention path and makes it reachable from Vime through explicit, contract-aware dispatch. It reuses qualified high-performance vendor operators where their reduction behavior is controllable, and keeps RL-Kernel implementations where vendor paths did not pass the expanded bitwise probes.
Scope note: #235 lists "ROCm / domestic accelerator backend" under Non-goals, and its PR1–PR8 stack is CUDA-only by design. This PR does not implement a #235 deliverable; it extends the #235 attention contract to ROCm — same
AttentionContract, same attention-domain LSE, same fixedglobal_block_indexmerge order, same fail-closed policy — so a ROCm deployment is held to the definition #235 established on CUDA.aiter.ops.mha.mha_fwd/mha_bwdnum_splits=1provenanceKernelRegistry.get_attention_oponAttentionContractaiter.ops.mhaloads; explicit request never degrades to another backendrl_engine.integrations.vime.attention.attention_provider(out, lse)+ provenance out; RL-Kernel never imports Vimerlkernel.rocm.deterministic_roperccl_ag_rsrlkernel.attention.strict_ring_state.v1The self-owned deterministic Attention core remains a reference/debug oracle, not the ROCm production arithmetic path. Strict equality is defined within the same ROCm hardware and runtime stack; this PR makes no cross-vendor bitwise claim.
Changes
KernelRegistry.register_attention_backendandget_attention_op(contract, requested_backend=...). The candidate list is separate from the legacyattn/attentionpriority maps and starts empty on every platform, so a WS2 caller can never be served by an SDPA-shaped wrapper that does not export attention-domain LSE, and the strict core can never be picked up by a legacyget_opcaller.aiter.rocm.ck_dense_mhaonly whenaiter.ops.mhagenuinely imports and exposesmha_fwd/mha_bwd. An explicit request for the native backend fails immediately if the vendor stack is missing; it will not silently turn into the PyTorch reference path.rl_engine/integrations/vime/attention.py: a runtime attention provider for Vime's Megatron backend. It accepts and returns structural objects, so RL-Kernel never imports Vime, and signals native fallback through anattention_provider_unavailablemarker rather than an imported exception type.mha_fwdis batch-composition sensitive in BF16 at some shapes (S=256B=4,S=512B=2/B=4; up to1.5625e-02) while invariant at others, so batching would otherwise change the bits for a subset of shapes only — the failure mode most likely to reach production unnoticed.query_position_ids/key_position_ids) so a training-side full-sequence call and a rollout-side chunk provably describe the same logical tokens.validate_cross_config_alignment()and bind the TP/CP degree. A head shard computed under TP=4 is not bit-identical to the same shard under TP=8 at some shapes (measured below), because the AITER reduction order depends on the launch head count. Making the path TP-degree invariant is possible — executing one KV group per launch removes the dependence — but costs 2.7–3.9x forward time, so the degree is bound and a mismatch fails closed instead.AttentionContract.cross_rank_fingerprint()— a rank-independent identity for cross-rank preflight, mirroringLogprobContract.cross_rank_fingerprint.requested_backend="auto"is rejected under CP>1 without it.benchmarks/benchmark_rocm_attention.pyplus the MI300X artifacts underbenchmarks/results/pr319_rocm_mi300x/.ROCm Validation
gfx942:sramecc+:xnack-)2.12.0+rocm7.14.0a20260608/7.14.60850aiter.ops.mha, source fingerprint recorded in every report2.14.0.dev0+e6ede467Hq=32,Hkv=8,D=128tests/test_attention_correctness.py+tests/test_flashinfer_pr7_attention.py156 passed, 41 skippedtests/test_cp_attention.py+tests/test_attention_comparison.py+rl_engine/tests/test_dispatch.py83 passedtests/test_vime_attention_provider.py+tests/test_attention_dispatch.py45 passed000Every strict report records
fallback=false,native_attention_arithmetic=true,actual_backend=aiter.rocm.ck_dense_mha, deterministic backward, disabled Split-KV, andcommunication_backend=rccl_ag_rs.Benchmark results
Operator-only benchmark — seeded Q/K/V, no checkpoint, tokenizer, or serving engine. BF16, causal prefill, Qwen3-8B dense head layout.
mi300xSingle device (BF16, causal prefill)
Forward is competitive: at
S=4096the strict path is0.7387 msagainst0.7059 msfor SDPA and1.4373 msfor the Triton FlashAttention backend — roughly 1.9× faster than Triton and within 4% of SDPA.What the strict contract costs
The forward is cheap; the cost is concentrated in the deterministic backward, and it belongs to AITER's
mha_bwdrather than to this integration. Measured with raw AITER atB=1, toggling only thedeterministicflag:Deterministic backward peak memory scales as O(S²): every doubling of sequence length quadruples it (1.2 → 4.3 → 16.8 GiB). This is the binding constraint on the path today — see Known limits. (Peak figures include the already-resident forward tensors, so the absolute det=on column is the meaningful scaling signal rather than the ratio between columns.)
That the integration is not the source is confirmed directly: at
S=4096raw AITER peaks at16657 MiBagainst the provider's16722 MiB— a65 MiBdifference that is exactly the provider's transposed input copies.Batch composition
Raw AITER
mha_fwdis batch-composition invariant for most shapes and not for others. Measured on MI300X, BF16,Hq=32/Hkv=8/D=128, causal, batch vs the same rows submitted one at a time:outmax abslsemax abs00001.5625e-029.5367e-0700007.8125e-039.5367e-071.5625e-029.5367e-0700Through the provider every shape above is bitwise identical (
0), because each logical row is materialized on its own.The shape-dependence is the point. A kernel that always broke would be caught on the first batched run; one that breaks only at
S=256/S=512passes casual testing and then silently loses bitwise consistency in production. The one-row-at-a-time rule is therefore load-bearing rather than defensive, andtests/test_vime_attention_provider.py::test_batch_composition_is_bitwise_invariantkeepsS=512in its parametrization deliberately.TP-degree invariance
TP shards attention by head and performs no cross-rank reduction, so a rank computing its own head slice must match the corresponding slice of an unsharded run. Raw AITER does not provide that: its reduction order depends on the launch head count, and measured on MI300X the same head shard drifts by up to
7.8125e-03between TP degrees at some shapes.The provider removes the dependence by pinning every launch to one batch row and one KV group. Through the provider, all five tensors are bitwise equal at every TP degree measured:
outlsedQdKdV00000000000000000000This is simulated TP: one process holding the full tensors and slicing heads per rank. Because attention TP has no collectives, the arithmetic a real rank performs is exactly this shard computation — but the real multi-GPU rerun after this change is still outstanding.
The cost is roughly 3x forward time. The cross-config contract therefore does not bind the TP degree: doing so would reject train/rollout comparisons that are in fact bitwise identical.
validate_cross_config_alignmentchecks only what changes the arithmetic (dtype, head layout, causal, Split-KV, reduction spec).Distributed (strict CP, RCCL AG+RS)
The strict CP path requires the native extension built for the active GPU platform; without it the run fails closed on the ROCm deterministic RoPE operator (
ROCm deterministic RoPE is unavailable; rebuild rl_engine._C for ROCm) rather than substituting a different one. Build withPYTORCH_ROCM_ARCH=gfx942 python setup.py build_ext --inplace.Artifacts:
benchmarks/results/pr319_rocm_mi300x/distributed/.Known limits
These are stated rather than worked around, because each one is a place where a silent approximation would be worse than a refusal:
S=4096the deterministic backward peaks at ~16.6 GiB for a single sequence. Long-context training on this path is not viable until AITER offers a deterministic backward with bounded workspace; the forward-only / rollout path is unaffected.(out, lse)merge inside the provider would introduce a second merge implementation with its own order, so it fails closed and points at the CP transport path.cache_position, block table, prefix-cache key) that the dense core does not materialize.Reproduce
Related to #235 and #294. Q/K RMSNorm wrapper integration is in #230. The Vime adapter boundary mirrors the selected-logprob provider in #328.