feat(attention): add strict bitwise ROCm path - #319
Open
inaniloquentee wants to merge 46 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:
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
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
…-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>
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.
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, sameglobal_block_indexmerge 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
B>1rejected at the corekey_padding_maskrejectedBitwise 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.
num_splits; bind the dense non-split API (split_kv_control="dense_non_split_api"). Non-DISABLEDSplitKVSpecraises._validate_inputsrejectsq.size(0) != 1. The batched launch does not exist, so it cannot disagree.key_padding_maskrejected; each unpadded logical row is materialized.(batch row, KV group), so a shard's result never depends on the launch head count.all_gather+ root-ownedscatter;reduce_scattergathers then evaluates a fixed balanced rank tree locally.aiter.ops.mhasource file in provenance — a version number does not pin Python-level dispatch.deterministic_attention.cuhipified 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:
1.5625e-027.8125e-03Changes
KernelRegistry.register_attention_backend/get_attention_op(contract, requested_backend=...), candidate list separate from the legacyattnpriority maps and empty by default, so a WS2 caller can never be served by an SDPA-shaped wrapper without attention-domain LSE.aiter.rocm.ck_dense_mhaonly whenaiter.ops.mhagenuinely 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.query_position_ids/key_position_idsso 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.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.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.csrc/ops.cpp— the merge fromfeat/rocm-deterministic-collectivesdropped an#if !defined(USE_ROCM)around theprefix_shared_attentionregistration but kept its#endif(13#endifvs 12#if). ROCm builds failed with#endif without #if.Validation
284 passed71 passed0Every 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
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 barev_exp_f32, ~1 ULP from theexpfthe 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_launchis one launch for all heads;per_kv_groupis what the provider runs.raw_launchout max-absper_kv_group007.8125e-030003.9e-03/1.9e-030003.9e-030Cost of the per-KV-group schedule — corrects the earlier ~3x estimate
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-aiteris the core called once for all heads — not the production schedule; see the table above.sdpais a speed baseline only, with no accuracy comparison mixed in.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-aiterpeaks at 16.6 GiB whilereference-native— which writes a full FP32[B, Hq, Sq, Skv]buffer — peaks at 4.2 GiB. This belongs to AITER'smha_bwd, not to the integration: toggling only thedeterministicflag 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.tp1_cp2tp2_cp2tp1_cp4tp2_cp2_x2tp2_cp4tp1_cp80 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
Every measured cell is 0;
n/mmarks the FP16 gradient columns, which are not measured.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-nativeandtriton-bitwiseallocate identical buffers, so their memory curves coincide and the later-drawn series hides the earlier.Known limits
Stated rather than worked around, because a silent approximation would be worse than a refusal.
expf/logfreductions are not ported and no CUDA device was available to verify them, so the op raises on CUDA unless the caller passesrequire_bitwise_libm=False. A test pins this on both platforms._Cdoes not registerdeterministic_attention_forward_fp32. The.cudefines it but the pybind registration is missing, so the nativeforward_fp32raisesAttributeError. Pre-existing, untouched.vime/attention.pystill 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 reportstp_degree_invariant: True.strict-fa4replacesstrict-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.Reproduce