From 41bac5d8fdc33f08cc763c356588baffbe019581 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 19 Aug 2026 09:06:20 +0000 Subject: [PATCH 01/35] feat(attention): add strict ROCm Attention and RCCL path --- csrc/ops.cpp | 14 ++ csrc/rocm/deterministic_rope.cu | 91 ++++++++++ .../kernels/ops/cuda/attention/__init__.py | 2 + .../kernels/ops/cuda/attention/cp_comm.py | 167 +++++++++++++++++- .../ops/cuda/attention/deterministic_attn.py | 30 ++-- .../attention/flashinfer_paged_attention.py | 25 ++- .../kernels/ops/rocm/attention/__init__.py | 6 + .../ops/rocm/attention/deterministic_attn.py | 15 ++ .../ops/rocm/rotary_embedding/__init__.py | 6 + .../kernels/ops/rocm/rotary_embedding/rope.py | 103 +++++++++++ scripts/rocm_attention_acceptance.py | 84 +++++++++ .../ws2_p2p_nccl_attention_reference_check.py | 60 +++++-- setup.py | 2 + tests/test_deterministic_attention_rocm.py | 131 ++++++++++++++ 14 files changed, 697 insertions(+), 39 deletions(-) create mode 100644 csrc/rocm/deterministic_rope.cu create mode 100644 rl_engine/kernels/ops/rocm/attention/deterministic_attn.py create mode 100644 rl_engine/kernels/ops/rocm/rotary_embedding/__init__.py create mode 100644 rl_engine/kernels/ops/rocm/rotary_embedding/rope.py create mode 100644 scripts/rocm_attention_acceptance.py create mode 100644 tests/test_deterministic_attention_rocm.py diff --git a/csrc/ops.cpp b/csrc/ops.cpp index ad1c42fc..b1725c50 100644 --- a/csrc/ops.cpp +++ b/csrc/ops.cpp @@ -277,6 +277,14 @@ std::vector deterministic_attention_backward( double scale, torch::optional key_padding_mask); +#if defined(KERNEL_ALIGN_WITH_ROCM) +torch::Tensor deterministic_rope_apply_rocm( + torch::Tensor x, + torch::Tensor cos, + torch::Tensor sin, + double sin_sign); +#endif + #if !defined(USE_ROCM) // Prefix-Shared Attention Declarations & Wrappers (NVIDIA PTX only). @@ -450,5 +458,11 @@ PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) { "deterministic_attention_backward", &deterministic_attention_backward, "Deterministic standard softmax attention backward (dQ, dK, dV)"); +#if defined(KERNEL_ALIGN_WITH_ROCM) + m.def( + "deterministic_rope_apply_rocm", + &deterministic_rope_apply_rocm, + "Deterministic GPT-NeoX RoPE apply for ROCm"); +#endif #endif } diff --git a/csrc/rocm/deterministic_rope.cu b/csrc/rocm/deterministic_rope.cu new file mode 100644 index 00000000..91f1f0ff --- /dev/null +++ b/csrc/rocm/deterministic_rope.cu @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) 2026 RL-Kernel Contributors + +#include +#include +#include + +namespace { + +template +__global__ void deterministic_rope_kernel( + const scalar_t* __restrict__ x, + const float* __restrict__ cos, + const float* __restrict__ sin, + scalar_t* __restrict__ out, + int64_t n_rows, + int table_rows, + int half, + float sin_sign) { + const int64_t index = blockIdx.x * static_cast(blockDim.x) + threadIdx.x; + const int64_t count = n_rows * static_cast(half); + if (index >= count) { + return; + } + + const int64_t row = index / half; + const int pair = static_cast(index % half); + const int table_row = static_cast(row % table_rows); + const float c = cos[table_row * half + pair]; + const float s = sin[table_row * half + pair] * sin_sign; + const int64_t base = row * (2LL * half); + const float low = static_cast(x[base + pair]); + const float high = static_cast(x[base + pair + half]); + + out[base + pair] = static_cast(low * c - high * s); + out[base + pair + half] = static_cast(high * c + low * s); +} + +} // namespace + +torch::Tensor deterministic_rope_apply_rocm( + torch::Tensor x, + torch::Tensor cos, + torch::Tensor sin, + double sin_sign) { + TORCH_CHECK(x.is_cuda(), "ROCm RoPE: x must be a GPU tensor"); + TORCH_CHECK(x.dim() == 2 && x.is_contiguous(), + "ROCm RoPE: x must be contiguous [rows, head_dim]"); + TORCH_CHECK(cos.is_cuda() && sin.is_cuda(), + "ROCm RoPE: cos and sin must be GPU tensors"); + TORCH_CHECK(cos.scalar_type() == torch::kFloat32 && + sin.scalar_type() == torch::kFloat32, + "ROCm RoPE: cos and sin must be FP32"); + TORCH_CHECK(cos.is_contiguous() && sin.is_contiguous(), + "ROCm RoPE: cos and sin must be contiguous"); + TORCH_CHECK(cos.dim() == 2 && sin.sizes() == cos.sizes(), + "ROCm RoPE: cos and sin must have shape [table_rows, head_dim/2]"); + TORCH_CHECK(x.size(1) % 2 == 0, "ROCm RoPE: head_dim must be even"); + TORCH_CHECK(cos.size(0) > 0 && cos.size(1) == x.size(1) / 2, + "ROCm RoPE: invalid cos/sin table shape"); + TORCH_CHECK(x.size(0) % cos.size(0) == 0, + "ROCm RoPE: row count must be divisible by the position table size"); + + const at::cuda::OptionalCUDAGuard guard(device_of(x)); + auto out = torch::empty_like(x); + const int64_t n_rows = x.size(0); + const int half = static_cast(x.size(1) / 2); + const int table_rows = static_cast(cos.size(0)); + const int64_t count = n_rows * static_cast(half); + constexpr int threads = 256; + const int64_t blocks = (count + threads - 1) / threads; + auto stream = at::cuda::getCurrentCUDAStream(); + + AT_DISPATCH_FLOATING_TYPES_AND2( + at::ScalarType::Half, + at::ScalarType::BFloat16, + x.scalar_type(), + "deterministic_rope_apply_rocm", + [&] { + deterministic_rope_kernel<<>>( + x.data_ptr(), + cos.data_ptr(), + sin.data_ptr(), + out.data_ptr(), + n_rows, + table_rows, + half, + static_cast(sin_sign)); + }); + return out; +} diff --git a/rl_engine/kernels/ops/cuda/attention/__init__.py b/rl_engine/kernels/ops/cuda/attention/__init__.py index b60ccb55..21850cdf 100644 --- a/rl_engine/kernels/ops/cuda/attention/__init__.py +++ b/rl_engine/kernels/ops/cuda/attention/__init__.py @@ -13,6 +13,7 @@ CPCommunicationStatus, CUDAAGRSAttentionCPCommunication, P2PNCCLAttentionCPCommunication, + RCCLAGRSAttentionCPCommunication, sort_attention_cp_partial_states, ) from .deterministic_attn import ( @@ -43,6 +44,7 @@ "CPCommunicationStatus", "CUDAAGRSAttentionCPCommunication", "P2PNCCLAttentionCPCommunication", + "RCCLAGRSAttentionCPCommunication", "DeterministicAttentionCoreResult", "DeterministicAttentionOp", "RLKernelDeterministicAttentionCore", diff --git a/rl_engine/kernels/ops/cuda/attention/cp_comm.py b/rl_engine/kernels/ops/cuda/attention/cp_comm.py index d1470bfd..20602ede 100644 --- a/rl_engine/kernels/ops/cuda/attention/cp_comm.py +++ b/rl_engine/kernels/ops/cuda/attention/cp_comm.py @@ -40,7 +40,12 @@ import torch -CPCommunicationBackend = Literal["cuda_ag_rs", "p2p_nccl_reference", "local_debug"] +CPCommunicationBackend = Literal[ + "cuda_ag_rs", + "rccl_ag_rs", + "p2p_nccl_reference", + "local_debug", +] CPCommunicationStatus = Literal["interface_only", "implemented"] @@ -198,12 +203,17 @@ class AttentionCPCommunicationPlan: def validate(self) -> None: self.parallel.validate() - if self.backend not in {"cuda_ag_rs", "p2p_nccl_reference", "local_debug"}: + if self.backend not in { + "cuda_ag_rs", + "rccl_ag_rs", + "p2p_nccl_reference", + "local_debug", + }: raise ValueError(f"unsupported CP communication backend: {self.backend}") if self.status not in {"interface_only", "implemented"}: raise ValueError(f"unsupported CP communication status: {self.status}") if self.pattern != "ag_rs": - raise ValueError("PR7 CP communication must use the custom CUDA AG/RS interface") + raise ValueError("PR7 CP communication must use the self-owned AG/RS interface") if self.compute_communication != "decoupled": raise ValueError("PR7 CP communication must keep compute and communication decoupled") if self.merge_order != "global_block_index": @@ -242,6 +252,12 @@ def provenance(self) -> dict[str, object]: "cp_comm_strict_kv_communication": "all_gather", "cp_comm_strict_position_communication": "all_gather", "cp_comm_strict_backward": "rs_out_backward_ag_then_ag_qkv_backward_rs", + "cp_comm_runtime": ( + "rccl" + if self.backend == "rccl_ag_rs" + else "nccl" if self.backend in {"cuda_ag_rs", "p2p_nccl_reference"} else "local" + ), + "cp_comm_attention_numeric_reduction": self.backend == "cuda_ag_rs", "cp_comm_expected_kv_token_range": ( None if self.expected_kv_token_range is None else list(self.expected_kv_token_range) ), @@ -335,15 +351,21 @@ def forward( ctx.sequence_dim = int(sequence_dim) ctx.rank = int(rank) ctx.root = int(root) + ctx.world_size = int(getattr(collective, "world_size", 1)) packed = full.movedim(ctx.sequence_dim, 0).contiguous() + ctx.full_shape = tuple(packed.shape) if ctx.rank != ctx.root: packed = torch.zeros_like(packed) - local = collective.reduce_scatter(packed) + scatter = getattr(collective, "scatter", None) + local = scatter(packed) if callable(scatter) else collective.reduce_scatter(packed) return local.movedim(0, ctx.sequence_dim).contiguous() @staticmethod def backward(ctx, grad_local: torch.Tensor) -> tuple[torch.Tensor, None, None, None, None]: packed = grad_local.movedim(ctx.sequence_dim, 0).contiguous() + # The forward scatter has one authoritative full input on root. Its + # backward is the dual gather of every rank's local output gradient; + # non-root full inputs were zeroed in forward and receive no gradient. grad_full = ctx.collective.all_gather(packed).movedim(0, ctx.sequence_dim).contiguous() if ctx.rank != ctx.root: grad_full.zero_() @@ -557,6 +579,142 @@ def _validate_cuda_plan(self, plan: AttentionCPCommunicationPlan) -> None: raise AttentionCPCommunicationUnavailable("self-owned CUDA AG/RS requires CUDA") +class _RCCLRankOrderedTransport: + """RCCL transport with rank-ordered AG and root-owned scatter. + + The scatter half deliberately performs no floating-point reduction. The + strict Attention arithmetic remains entirely in the deterministic core. + """ + + def __init__(self, *, process_group: Any, root: int) -> None: + import torch.distributed as dist + + if not dist.is_available() or not dist.is_initialized(): + raise AttentionCPCommunicationUnavailable( + "self-owned RCCL AG/RS requires initialized torch.distributed" + ) + backend = str(dist.get_backend(process_group)).lower() + if "nccl" not in backend or torch.version.hip is None: + raise AttentionCPCommunicationUnavailable( + "self-owned RCCL AG/RS requires the PyTorch NCCL API on ROCm" + ) + self.group = process_group + self.rank = int(dist.get_rank(process_group)) + self.world_size = int(dist.get_world_size(process_group)) + self.root = int(root) + if self.root < 0 or self.root >= self.world_size: + raise AttentionCPCommunicationUnavailable("RCCL scatter root is outside the group") + + def all_gather(self, local: torch.Tensor) -> torch.Tensor: + import torch.distributed as dist + + if not local.is_cuda or not local.is_contiguous(): + raise AttentionCPCommunicationUnavailable( + "RCCL AllGather requires a contiguous ROCm tensor" + ) + shape = (self.world_size * local.size(0), *local.shape[1:]) + gathered = torch.empty(shape, dtype=local.dtype, device=local.device) + dist.all_gather_into_tensor(gathered, local, group=self.group) + return gathered + + def scatter(self, full: torch.Tensor) -> torch.Tensor: + import torch.distributed as dist + + if not full.is_cuda or not full.is_contiguous(): + raise AttentionCPCommunicationUnavailable( + "RCCL ReduceScatter transport requires a contiguous ROCm tensor" + ) + if full.size(0) % self.world_size: + raise AttentionCPCommunicationUnavailable( + "RCCL scatter leading dimension must divide the CP world size" + ) + chunks = tuple(chunk.contiguous() for chunk in full.chunk(self.world_size, dim=0)) + local = torch.empty_like(chunks[self.rank]) + global_root = self.root + if self.group is not None: + get_global_rank = getattr(dist, "get_global_rank", None) + if callable(get_global_rank): + global_root = int(get_global_rank(self.group, self.root)) + else: + global_root = int(dist.get_process_group_ranks(self.group)[self.root]) + dist.scatter( + local, + scatter_list=list(chunks) if self.rank == self.root else None, + src=global_root, + group=self.group, + ) + return local + + def reduce_scatter(self, full: torch.Tensor) -> torch.Tensor: + """Deterministic rank-order sum followed by local scatter. + + RCCL is used for point-to-point transport. The floating-point sum is + performed locally in source-rank order, so collective reduction order + is not delegated to RCCL. + """ + import torch.distributed as dist + + if not full.is_cuda or not full.is_contiguous(): + raise AttentionCPCommunicationUnavailable( + "RCCL ReduceScatter requires a contiguous ROCm tensor" + ) + if full.size(0) % self.world_size: + raise AttentionCPCommunicationUnavailable( + "RCCL ReduceScatter leading dimension must divide the CP world size" + ) + chunks = tuple(chunk.contiguous() for chunk in full.chunk(self.world_size, dim=0)) + gathered = self.all_gather(full) + local = gathered[self.rank * chunks[self.rank].size(0) : + (self.rank + 1) * chunks[self.rank].size(0)].clone() + chunk_rows = chunks[self.rank].size(0) + # Start with source rank 0, then add the remaining source ranks in + # ascending order. This avoids counting source 0 twice. + for source in range(1, self.world_size): + source_full = gathered[source * full.size(0) : (source + 1) * full.size(0)] + local.add_(source_full[self.rank * chunk_rows : (self.rank + 1) * chunk_rows]) + return local + + +class RCCLAGRSAttentionCPCommunication(CUDAAGRSAttentionCPCommunication): + """ROCm AG/RS adapter using RCCL only as rank-ordered tensor transport.""" + + backend_id = "rccl_ag_rs" + supports_autograd = True + transport_only = True + + def _get_collective(self, plan: AttentionCPCommunicationPlan): + if self._collective is None: + self._collective = _RCCLRankOrderedTransport( + process_group=self._process_group, + root=plan.merge_root_cp_rank, + ) + if self._collective.world_size != plan.parallel.cp_world_size: + raise AttentionCPCommunicationUnavailable( + "self-owned RCCL world size does not match the CP plan" + ) + return self._collective + + def _dist(self): + import torch.distributed as dist + + if not dist.is_available() or not dist.is_initialized(): + raise AttentionCPCommunicationUnavailable( + "self-owned RCCL AG/RS requires initialized torch.distributed" + ) + return dist + + def _validate_cuda_plan(self, plan: AttentionCPCommunicationPlan) -> None: + plan.validate() + if plan.backend != "rccl_ag_rs" or plan.status != "implemented": + raise AttentionCPCommunicationUnavailable( + "self-owned RCCL AG/RS requires an implemented rccl_ag_rs plan" + ) + if torch.version.hip is None or not torch.cuda.is_available(): + raise AttentionCPCommunicationUnavailable( + "self-owned RCCL AG/RS requires an available ROCm device" + ) + + class P2PNCCLAttentionCPCommunication: """Correctness-first P2P NCCL implementation of the CP protocol. @@ -1288,6 +1446,7 @@ def _rank_in_world(rank: int, world_size: int, name: str) -> None: "CPCommunicationBackend", "CPCommunicationStatus", "CUDAAGRSAttentionCPCommunication", + "RCCLAGRSAttentionCPCommunication", "P2PNCCLAttentionCPCommunication", "sort_attention_cp_partial_states", ] diff --git a/rl_engine/kernels/ops/cuda/attention/deterministic_attn.py b/rl_engine/kernels/ops/cuda/attention/deterministic_attn.py index c9bc32f7..98823816 100644 --- a/rl_engine/kernels/ops/cuda/attention/deterministic_attn.py +++ b/rl_engine/kernels/ops/cuda/attention/deterministic_attn.py @@ -1,7 +1,7 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright (c) 2026 RL-Kernel Contributors -"""CUDA deterministic standard-softmax attention (issue #147). +"""Deterministic standard-softmax attention for CUDA and ROCm (issue #147). Forward: QK → masked softmax+LSE → PV (all FP32 intermediate). Backward: dP → softmax_bwd → dQ/dK/dV with §4.1 fixed GQA order. @@ -28,6 +28,8 @@ from rl_engine.utils.logger import logger _HEAD_DIM = 128 +_IS_ROCM = torch.version.hip is not None +_GPU_PLATFORM = "ROCm" if _IS_ROCM else "CUDA" @dataclass(frozen=True) @@ -92,7 +94,7 @@ def backward(ctx, grad_out: torch.Tensor, grad_lse: torch.Tensor): class DeterministicAttentionOp: - """Batch-invariant standard softmax attention on CUDA. + """Batch-invariant standard softmax attention on a CUDA or ROCm GPU. Materializes full FP32 scores/P. Public surface matches NativeAttentionOp so #108 harness can call forward(**inputs) with key_padding_mask. @@ -101,13 +103,13 @@ class DeterministicAttentionOp: def __init__(self) -> None: if not _EXT_AVAILABLE or not hasattr(_C, "deterministic_attention_forward"): raise RuntimeError( - "Deterministic CUDA attention kernel is unavailable. " - "Rebuild the extension with `pip install -e .` on a CUDA build." + f"Deterministic {_GPU_PLATFORM} attention kernel is unavailable. " + "Rebuild the native extension for the active GPU platform." ) if not hasattr(_C, "deterministic_attention_backward"): raise RuntimeError( - "Deterministic CUDA attention backward kernel is unavailable. " - "Rebuild the extension with `pip install -e .` on a CUDA build." + f"Deterministic {_GPU_PLATFORM} attention backward kernel is unavailable. " + "Rebuild the native extension for the active GPU platform." ) logger.info("Successfully linked to _C.deterministic_attention_forward/backward.") @@ -204,7 +206,7 @@ def _validate_inputs( if k.dtype != q.dtype or v.dtype != q.dtype: raise ValueError("q, k, v must share the same dtype") if not (q.is_cuda and k.is_cuda and v.is_cuda): - raise ValueError("q, k, v must be CUDA tensors") + raise ValueError("q, k, v must be GPU tensors") if key_padding_mask is not None: if key_padding_mask.dtype != torch.bool: raise ValueError("key_padding_mask must be bool") @@ -218,7 +220,7 @@ def _validate_inputs( class RLKernelDeterministicAttentionCore: - """Production CUDA Attention core shared by training and rollout. + """Production GPU Attention core shared by training and rollout. Strict execution exposes only the existing WS1 no-Split-K kernel. This is intentional: a different Split-K schedule changes the arithmetic graph and @@ -227,7 +229,11 @@ class RLKernelDeterministicAttentionCore: core_id = STRICT_ATTENTION_CORE_ID strict_schedule = STRICT_ATTENTION_SCHEDULE_ID - backend_id = "rlkernel.cuda.deterministic_attention" + backend_id = ( + "rlkernel.rocm.deterministic_attention" + if _IS_ROCM + else "rlkernel.cuda.deterministic_attention" + ) merge_order = "global_block_index" accum_dtype = "fp32" downcast_at = "final_write" @@ -243,7 +249,7 @@ def __init__( if not isinstance(requested, SplitKVSpec): raise TypeError("split_kv must be a SplitKVSpec") if requested.mode is not SplitKVMode.DISABLED: - raise ValueError("the strict CUDA Attention core requires Split-KV to be disabled") + raise ValueError("the strict GPU Attention core requires Split-KV to be disabled") self.split_kv = requested self._op = DeterministicAttentionOp() @@ -317,7 +323,7 @@ def _validate_positions( return if query_position_ids is None or key_position_ids is None: raise ValueError( - "strict CUDA Attention requires query_position_ids and " "key_position_ids" + "strict GPU Attention requires query_position_ids and " "key_position_ids" ) expected_q_shape = (q.size(0), q.size(2)) expected_k_shape = (k.size(0), k.size(2)) @@ -342,6 +348,6 @@ def _validate_positions( raise ValueError("key_position_ids must be contiguous and increasing") if not torch.equal(query_position_ids, key_position_ids[:, -q.size(2) :]): raise ValueError( - "strict CUDA Attention requires queries to be the trailing " + "strict GPU Attention requires queries to be the trailing " "contiguous positions of the logical KV sequence" ) diff --git a/rl_engine/kernels/ops/cuda/attention/flashinfer_paged_attention.py b/rl_engine/kernels/ops/cuda/attention/flashinfer_paged_attention.py index ac6eec00..0a6bdeed 100644 --- a/rl_engine/kernels/ops/cuda/attention/flashinfer_paged_attention.py +++ b/rl_engine/kernels/ops/cuda/attention/flashinfer_paged_attention.py @@ -775,7 +775,7 @@ def _run_strict_cp( communication, "supports_autograd", False ): raise FlashInferUnavailable( - "strict training requires the autograd-capable self-owned CUDA AG/RS backend" + "strict training requires an autograd-capable self-owned GPU AG/RS backend" ) query_start, query_end = plan.query_token_ranges[plan.parallel.cp_rank] key_start, key_end = _cp_owner_ranges(plan)[plan.parallel.cp_rank] @@ -1515,12 +1515,18 @@ def _resolve_strict_rope(cfg: FlashInferPagedAttentionConfig) -> Any: if cfg.strict_rope_op is not None: return cfg.strict_rope_op try: + if torch.version.hip is not None: + from rl_engine.kernels.ops.rocm.rotary_embedding.rope import ( + RocmDeterministicRoPEOp, + ) + + return RocmDeterministicRoPEOp() from rl_engine.kernels.ops.cuda.rotary_embedding.rope import RoPESM90Op return RoPESM90Op() except (ImportError, RuntimeError) as exc: raise FlashInferUnavailable( - "strict Attention requires the RL-Kernel WS1 RoPE CUDA operator" + "strict Attention requires the RL-Kernel deterministic RoPE operator" ) from exc @@ -1530,7 +1536,7 @@ def _apply_strict_rope( position_ids: torch.Tensor, theta: float, ) -> torch.Tensor: - """RoPESM90Op accepts shared 1-D positions, so execute one batch row at a time.""" + """Execute one batch row at a time to preserve the strict row schedule.""" if position_ids.shape != (x.size(0), x.size(2)): raise ValueError("strict RoPE position IDs must have shape [B,S]") @@ -1682,12 +1688,12 @@ def _strict_attention_provenance( "strict_schedule": core_provenance["strict_schedule"], "accum_dtype": core_provenance["accum_dtype"], "downcast_at": core_provenance["downcast_at"], - "arithmetic_plan_source": "rlkernel_deterministic_cuda_core", + "arithmetic_plan_source": "rlkernel_deterministic_gpu_core", "arithmetic_semantics_verified": True, "native_attention_arithmetic": False, "fallback": False, "fallback_reason": None, - "rope_backend": getattr(rope, "backend_id", "rlkernel.cuda.rope_sm90"), + "rope_backend": getattr(rope, "backend_id", "rlkernel.unknown.rope"), "rope_theta": float(cfg.rope.rope_theta), "rotary_dim": cfg.rope.rotary_dim, "rope_fusion": False, @@ -1697,12 +1703,17 @@ def _strict_attention_provenance( "batch_invariant_claim": "strict_runtime_verified", "cp_comm_required": cp_required, "communication_backend": ( - "self_owned_cuda_ag_rs" if cp_required else "none" + getattr(cfg.cp_communication, "backend_id", "unknown") if cp_required else "none" ), "production_ready": bool( cp_required and core_provenance.get("attention_backend") - == "rlkernel.cuda.deterministic_attention" + in { + "rlkernel.cuda.deterministic_attention", + "rlkernel.rocm.deterministic_attention", + } + and getattr(cfg.cp_communication, "backend_id", None) + in {"cuda_ag_rs", "rccl_ag_rs"} ), } diff --git a/rl_engine/kernels/ops/rocm/attention/__init__.py b/rl_engine/kernels/ops/rocm/attention/__init__.py index 150c937f..2445e2de 100644 --- a/rl_engine/kernels/ops/rocm/attention/__init__.py +++ b/rl_engine/kernels/ops/rocm/attention/__init__.py @@ -2,7 +2,13 @@ # Copyright (c) 2026 RL-Kernel Contributors from .flash_attn import RocmFlashAttentionOp +from .deterministic_attn import ( + DeterministicAttentionOp, + RLKernelDeterministicAttentionCore, +) __all__ = [ "RocmFlashAttentionOp", + "DeterministicAttentionOp", + "RLKernelDeterministicAttentionCore", ] diff --git a/rl_engine/kernels/ops/rocm/attention/deterministic_attn.py b/rl_engine/kernels/ops/rocm/attention/deterministic_attn.py new file mode 100644 index 00000000..435a5272 --- /dev/null +++ b/rl_engine/kernels/ops/rocm/attention/deterministic_attn.py @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2026 RL-Kernel Contributors +"""ROCm public surface for the shared deterministic Attention core.""" + +from rl_engine.kernels.ops.cuda.attention.deterministic_attn import ( + DeterministicAttentionCoreResult, + DeterministicAttentionOp, + RLKernelDeterministicAttentionCore, +) + +__all__ = [ + "DeterministicAttentionCoreResult", + "DeterministicAttentionOp", + "RLKernelDeterministicAttentionCore", +] diff --git a/rl_engine/kernels/ops/rocm/rotary_embedding/__init__.py b/rl_engine/kernels/ops/rocm/rotary_embedding/__init__.py new file mode 100644 index 00000000..0a7285f8 --- /dev/null +++ b/rl_engine/kernels/ops/rocm/rotary_embedding/__init__.py @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2026 RL-Kernel Contributors + +from .rope import RocmDeterministicRoPEOp + +__all__ = ["RocmDeterministicRoPEOp"] diff --git a/rl_engine/kernels/ops/rocm/rotary_embedding/rope.py b/rl_engine/kernels/ops/rocm/rotary_embedding/rope.py new file mode 100644 index 00000000..3290a9c1 --- /dev/null +++ b/rl_engine/kernels/ops/rocm/rotary_embedding/rope.py @@ -0,0 +1,103 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2026 RL-Kernel Contributors +"""Deterministic GPT-NeoX RoPE for ROCm training and inference.""" + +from __future__ import annotations + +import torch +from torch import Tensor + +from rl_engine.kernels.ops.base import _C, _EXT_AVAILABLE + + +def _build_cos_sin( + positions: Tensor, + half: int, + theta: float, + device: torch.device, +) -> tuple[Tensor, Tensor]: + inv_freq = 1.0 / ( + theta ** (torch.arange(0, half, dtype=torch.float32, device=device) / half) + ) + frequency = positions.to(device=device, dtype=torch.float32).reshape(-1, 1) * inv_freq + return frequency.cos().contiguous(), frequency.sin().contiguous() + + +def _prepare(x: Tensor, positions: Tensor, theta: float) -> tuple[Tensor, Tensor, Tensor]: + dim = x.size(-1) + if dim % 2: + raise ValueError(f"RoPE head_dim must be even, got {dim}") + if positions.ndim == 1: + table_rows = positions.numel() + x_2d = x.contiguous().reshape(-1, dim) + elif positions.ndim == 2: + batch, sequence = positions.shape + if x.ndim not in (3, 4) or x.size(0) != batch or x.size(-2) != sequence: + raise ValueError("[B,S] positions require x [B,S,D] or [B,H,S,D]") + x_2d = ( + x.permute(1, 0, 2, 3).contiguous().reshape(-1, dim) + if x.ndim == 4 + else x.contiguous().reshape(-1, dim) + ) + table_rows = batch * sequence + else: + raise ValueError("positions must have shape [S] or [B,S]") + if table_rows < 1 or x_2d.size(0) % table_rows: + raise ValueError("RoPE rows must be divisible by the position table size") + cos, sin = _build_cos_sin(positions, dim // 2, float(theta), x.device) + return x_2d, cos, sin + + +def _restore(out: Tensor, shape: torch.Size, positions_dim: int) -> Tensor: + if positions_dim == 2 and len(shape) == 4: + batch, heads, sequence, dim = shape + return out.reshape(heads, batch, sequence, dim).permute(1, 0, 2, 3).contiguous() + return out.reshape(shape) + + +class _RocmRoPEFunction(torch.autograd.Function): + @staticmethod + def forward(ctx, x: Tensor, positions: Tensor, theta: float) -> Tensor: + x_2d, cos, sin = _prepare(x, positions, theta) + ctx.save_for_backward(cos, sin) + ctx.shape = x.shape + ctx.positions_dim = positions.ndim + out = _C.deterministic_rope_apply_rocm(x_2d, cos, sin, 1.0) + return _restore(out, x.shape, positions.ndim) + + @staticmethod + def backward(ctx, grad_out: Tensor): + cos, sin = ctx.saved_tensors + shape = ctx.shape + if ctx.positions_dim == 2 and len(shape) == 4: + grad_2d = grad_out.permute(1, 0, 2, 3).contiguous().reshape(-1, shape[-1]) + else: + grad_2d = grad_out.contiguous().reshape(-1, shape[-1]) + grad_x = _C.deterministic_rope_apply_rocm(grad_2d, cos, sin, -1.0) + return _restore(grad_x, shape, ctx.positions_dim), None, None + + +class RocmDeterministicRoPEOp: + """One precompiled HIP RoPE arithmetic path shared by train and rollout.""" + + backend_id = "rlkernel.rocm.deterministic_rope" + op_class = "elementwise" + fallback = False + + def __init__(self) -> None: + if torch.version.hip is None: + raise RuntimeError("RocmDeterministicRoPEOp requires a ROCm PyTorch build") + if not _EXT_AVAILABLE or not hasattr(_C, "deterministic_rope_apply_rocm"): + raise RuntimeError( + "ROCm deterministic RoPE is unavailable; rebuild rl_engine._C for ROCm" + ) + + def __call__(self, x: Tensor, positions: Tensor, *, theta: float = 1_000_000.0) -> Tensor: + if not x.is_cuda: + raise RuntimeError("ROCm deterministic RoPE requires a GPU tensor") + if x.dtype not in (torch.float16, torch.bfloat16): + raise ValueError("ROCm deterministic RoPE requires FP16 or BF16") + return _RocmRoPEFunction.apply(x, positions, float(theta)) + + +__all__ = ["RocmDeterministicRoPEOp"] diff --git a/scripts/rocm_attention_acceptance.py b/scripts/rocm_attention_acceptance.py new file mode 100644 index 00000000..9ef1440d --- /dev/null +++ b/scripts/rocm_attention_acceptance.py @@ -0,0 +1,84 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2026 RL-Kernel Contributors +"""Run the ROCm strict Attention acceptance matrix and retain evidence.""" + +from __future__ import annotations + +import argparse +import json +import subprocess +import sys +from pathlib import Path + +import torch + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output-dir", type=Path, default=Path("results/rocm-attention")) + args = parser.parse_args() + if torch.version.hip is None or torch.cuda.device_count() < 8: + raise RuntimeError("the formal acceptance matrix requires 8 visible ROCm GPUs") + + repo = Path(__file__).resolve().parents[1] + output = (repo / args.output_dir).resolve() if not args.output_dir.is_absolute() else args.output_dir + output.mkdir(parents=True, exist_ok=True) + steps: list[dict[str, object]] = [] + + commands = [ + ( + "single_gpu", + [sys.executable, "-m", "pytest", "-q", "tests/test_deterministic_attention_rocm.py"], + ) + ] + for transport, strict in (("p2p_nccl_reference", False), ("rccl_ag_rs", True)): + for ranks in (2, 4, 8): + name = f"{transport}_{ranks}r" + report = output / f"{name}.json" + command = [ + "torchrun", + "--standalone", + f"--nproc-per-node={ranks}", + "scripts/ws2_p2p_nccl_attention_reference_check.py", + "--transport", + transport, + "--output", + str(report), + ] + if strict: + command.append("--strict-shared-core") + commands.append((name, command)) + + for name, command in commands: + completed = subprocess.run( + command, + cwd=repo, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + check=False, + ) + (output / f"{name}.log").write_text(completed.stdout, encoding="utf-8") + steps.append({"name": name, "command": command, "returncode": completed.returncode}) + + summary = { + "schema_version": "ws2_rocm_attention_acceptance/v1", + "platform": "rocm", + "torch": str(torch.__version__), + "hip": str(torch.version.hip), + "collective": list(torch.cuda.nccl.version()), + "device_count": torch.cuda.device_count(), + "device_name": torch.cuda.get_device_name(0), + "steps": steps, + "passed": all(step["returncode"] == 0 for step in steps), + } + (output / "summary.json").write_text( + json.dumps(summary, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + print(json.dumps(summary, indent=2, sort_keys=True)) + return 0 if summary["passed"] else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/ws2_p2p_nccl_attention_reference_check.py b/scripts/ws2_p2p_nccl_attention_reference_check.py index 47a70676..963e5020 100644 --- a/scripts/ws2_p2p_nccl_attention_reference_check.py +++ b/scripts/ws2_p2p_nccl_attention_reference_check.py @@ -39,6 +39,7 @@ AttentionParallelSpec, CUDAAGRSAttentionCPCommunication, P2PNCCLAttentionCPCommunication, + RCCLAGRSAttentionCPCommunication, ) from rl_engine.kernels.ops.cuda.attention.deterministic_attn import ( # noqa: E402 RLKernelDeterministicAttentionCore, @@ -48,7 +49,6 @@ FlashInferQwen3PagedAttentionOp, _apply_strict_rope, ) -from rl_engine.kernels.ops.cuda.rotary_embedding.rope import RoPESM90Op # noqa: E402 from rl_engine.kernels.ops.pytorch.attention.cp_attention import ( # noqa: E402 AttentionPartialState, DeterministicCPAttentionReferenceOp, @@ -70,9 +70,9 @@ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: parser.add_argument("--final-write-atol", type=float, default=2.0e-2) parser.add_argument( "--transport", - choices=("p2p_nccl_reference", "cuda_ag_rs"), + choices=("p2p_nccl_reference", "cuda_ag_rs", "rccl_ag_rs"), default="p2p_nccl_reference", - help="P2P is the correctness reference; cuda_ag_rs selects PR311/PR312", + help="P2P is the reference; cuda_ag_rs and rccl_ag_rs are self-owned transports", ) parser.add_argument( "--strict-shared-core", @@ -81,21 +81,21 @@ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: ) parser.add_argument("--output", type=Path) args = parser.parse_args(argv) - if args.strict_shared_core and args.transport != "cuda_ag_rs": - parser.error("--strict-shared-core requires --transport cuda_ag_rs") + if args.strict_shared_core and args.transport not in {"cuda_ag_rs", "rccl_ag_rs"}: + parser.error("--strict-shared-core requires a self-owned AG/RS transport") return args def main(argv: Sequence[str] | None = None) -> int: args = parse_args(argv) if not torch.cuda.is_available() or torch.cuda.device_count() < 2: - raise RuntimeError("this check requires at least two visible CUDA devices") + raise RuntimeError("this check requires at least two visible CUDA/ROCm devices") dist.init_process_group("nccl", init_method="env://") try: world_size = dist.get_world_size() global_rank = dist.get_rank() if world_size not in {2, 4, 8}: - raise RuntimeError("this check requires 2, 4, or 8 NCCL ranks") + raise RuntimeError("this check requires 2, 4, or 8 NCCL/RCCL ranks") if torch.cuda.device_count() < world_size: raise RuntimeError("this single-node check requires one visible GPU per NCCL rank") local_rank = int(os.environ.get("LOCAL_RANK", str(global_rank))) @@ -131,12 +131,17 @@ def main(argv: Sequence[str] | None = None) -> int: dist.all_gather_object(reports, result) if global_rank == 0: report = { - "schema_version": ( - "ws2_p2p_nccl_attention_reference/v1" - if args.transport == "p2p_nccl_reference" - else "ws2_cuda_ag_rs_attention/v1" - ), + "schema_version": f"ws2_{args.transport}_attention/v2", "backend": str(dist.get_backend()), + "platform": "rocm" if torch.version.hip is not None else "cuda", + "torch_version": str(torch.__version__), + "runtime_version": ( + str(torch.version.hip) + if torch.version.hip is not None + else str(torch.version.cuda) + ), + "collective_version": list(torch.cuda.nccl.version()), + "device_name": torch.cuda.get_device_name(0), "transport": args.transport, "world_size": world_size, "tp_world_size": 1 if world_size == 2 else 2, @@ -212,11 +217,17 @@ def run_check( expected_kv_token_range=(0, args.seq_len), query_token_ranges=owner_ranges, ) - communication: P2PNCCLAttentionCPCommunication | CUDAAGRSAttentionCPCommunication + communication: ( + P2PNCCLAttentionCPCommunication + | CUDAAGRSAttentionCPCommunication + | RCCLAGRSAttentionCPCommunication + ) if args.transport == "p2p_nccl_reference": communication = P2PNCCLAttentionCPCommunication(process_group=cp_group) - else: + elif args.transport == "cuda_ag_rs": communication = CUDAAGRSAttentionCPCommunication(process_group=cp_group) + else: + communication = RCCLAGRSAttentionCPCommunication(process_group=cp_group) query_start, query_end = owner_ranges[cp_rank] q_local = q[:, :, query_start:query_end, :].contiguous() @@ -350,7 +361,11 @@ def _run_strict_shared_core_check( args: argparse.Namespace, *, plan: AttentionCPCommunicationPlan, - communication: CUDAAGRSAttentionCPCommunication | P2PNCCLAttentionCPCommunication, + communication: ( + CUDAAGRSAttentionCPCommunication + | RCCLAGRSAttentionCPCommunication + | P2PNCCLAttentionCPCommunication + ), q: torch.Tensor, k: torch.Tensor, v: torch.Tensor, @@ -383,7 +398,7 @@ def _run_strict_shared_core_check( q_ref = q.detach().clone().requires_grad_() k_ref = k.detach().clone().requires_grad_() v_ref = v.detach().clone().requires_grad_() - rope = RoPESM90Op() + rope = _strict_rope_op() q_ready = _apply_strict_rope(rope, q_ref, positions, config.rope.rope_theta) k_ready = _apply_strict_rope(rope, k_ref, positions, config.rope.rope_theta) reference = RLKernelDeterministicAttentionCore().forward_with_lse( @@ -445,6 +460,7 @@ def _run_strict_shared_core_check( and provenance.get("fallback") is False and provenance.get("strict_split_kv") == "disabled" and provenance.get("strict_comm_autograd") is True + and provenance.get("communication_backend") == args.transport ) return { "executed": True, @@ -468,5 +484,17 @@ def _run_strict_shared_core_check( } +def _strict_rope_op(): + if torch.version.hip is not None: + from rl_engine.kernels.ops.rocm.rotary_embedding.rope import ( + RocmDeterministicRoPEOp, + ) + + return RocmDeterministicRoPEOp() + from rl_engine.kernels.ops.cuda.rotary_embedding.rope import RoPESM90Op + + return RoPESM90Op() + + if __name__ == "__main__": raise SystemExit(main()) diff --git a/setup.py b/setup.py index 0cd24a0f..695f0419 100644 --- a/setup.py +++ b/setup.py @@ -140,6 +140,8 @@ def get_extensions(): "csrc/cuda/activation.cu", "csrc/cuda/attention/deterministic_attention.cu", ] + if is_rocm: + cuda_sources.append("csrc/rocm/deterministic_rope.cu") if not is_rocm: # This source contains NVIDIA PTX (cp.async, ldmatrix, and mma.sync). # The ROCm dispatcher falls back to PyTorch SDPA for this operator. diff --git a/tests/test_deterministic_attention_rocm.py b/tests/test_deterministic_attention_rocm.py new file mode 100644 index 00000000..519eee66 --- /dev/null +++ b/tests/test_deterministic_attention_rocm.py @@ -0,0 +1,131 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2026 RL-Kernel Contributors +"""ROCm strict Attention acceptance tests that never use a native fallback.""" + +import math + +import pytest +import torch + +pytestmark = pytest.mark.skipif( + torch.version.hip is None or not torch.cuda.is_available(), + reason="ROCm GPU is unavailable", +) + +from rl_engine.kernels.attention_contract import SplitKVSpec # noqa: E402 +from rl_engine.kernels.ops.rocm.attention.deterministic_attn import ( # noqa: E402 + RLKernelDeterministicAttentionCore, +) +from rl_engine.kernels.ops.rocm.rotary_embedding.rope import ( # noqa: E402 + RocmDeterministicRoPEOp, +) + + +def _qkv(*, batch: int = 2, sequence: int = 7): + generator = torch.Generator(device="cpu").manual_seed(942) + q = torch.randn(batch, 4, sequence, 128, dtype=torch.bfloat16, generator=generator).cuda() + k = torch.randn(batch, 1, sequence, 128, dtype=torch.bfloat16, generator=generator).cuda() + v = torch.randn(batch, 1, sequence, 128, dtype=torch.bfloat16, generator=generator).cuda() + return q, k, v + + +def test_rocm_attention_core_is_repeat_bitwise_and_no_fallback(): + q, k, v = _qkv() + positions = torch.arange(q.size(2), device=q.device).expand(q.size(0), -1) + core = RLKernelDeterministicAttentionCore() + first = core.forward_with_lse( + q, + k, + v, + query_position_ids=positions, + key_position_ids=positions, + ) + second = core.forward_with_lse( + q, + k, + v, + query_position_ids=positions, + key_position_ids=positions, + ) + assert torch.equal(first.out, second.out) + assert torch.equal(first.lse, second.lse) + assert first.provenance["attention_backend"] == "rlkernel.rocm.deterministic_attention" + assert first.provenance["fallback"] is False + assert first.provenance["split_kv"]["actual_split_kv_policy"] == "disabled" + + +def test_rocm_attention_forward_backward_train_rollout_bitwise(): + q, k, v = (tensor.requires_grad_() for tensor in _qkv(batch=1, sequence=5)) + positions = torch.arange(q.size(2), device=q.device).expand(q.size(0), -1) + core = RLKernelDeterministicAttentionCore() + train = core.forward_with_lse( + q, + k, + v, + query_position_ids=positions, + key_position_ids=positions, + ) + grad = torch.randn(train.out.shape, dtype=train.out.dtype, device="cpu").cuda() + (train.out.float() * grad.float()).sum().backward() + train_grads = tuple(tensor.grad.detach().clone() for tensor in (q, k, v)) + + q2, k2, v2 = (tensor.detach().clone().requires_grad_() for tensor in (q, k, v)) + rollout = core.forward_with_lse( + q2, + k2, + v2, + query_position_ids=positions, + key_position_ids=positions, + ) + (rollout.out.float() * grad.float()).sum().backward() + assert torch.equal(train.out, rollout.out) + assert torch.equal(train.lse, rollout.lse) + assert all( + torch.equal(expected, actual.grad) + for expected, actual in zip(train_grads, (q2, k2, v2), strict=True) + ) + + +def test_rocm_rope_is_batch_invariant_and_backward_repeat_bitwise(): + generator = torch.Generator(device="cpu").manual_seed(714) + x = torch.randn(3, 4, 6, 128, dtype=torch.bfloat16, generator=generator).cuda() + positions = torch.arange(6, device=x.device).expand(3, -1) + rope = RocmDeterministicRoPEOp() + together = rope(x, positions) + separate = torch.cat([rope(x[index : index + 1], positions[index]) for index in range(3)]) + assert torch.equal(together, separate) + assert torch.equal(together, rope(x, positions)) + + grad = torch.randn(together.shape, dtype=together.dtype, device="cpu").cuda() + x1 = x.detach().clone().requires_grad_() + x2 = x.detach().clone().requires_grad_() + (rope(x1, positions).float() * grad.float()).sum().backward() + (rope(x2, positions).float() * grad.float()).sum().backward() + assert torch.equal(x1.grad, x2.grad) + + +def test_rocm_rope_matches_fp32_rotate_half_reference(): + x = torch.randn(1, 2, 4, 128, dtype=torch.bfloat16, device="cuda") + positions = torch.arange(4, device=x.device).expand(1, -1) + actual = RocmDeterministicRoPEOp()(x, positions) + half = x.size(-1) // 2 + inv_freq = 1.0 / ( + 1_000_000.0 + ** (torch.arange(half, dtype=torch.float32, device=x.device) / half) + ) + frequency = positions.float().unsqueeze(-1) * inv_freq + cos = frequency.cos().unsqueeze(1) + sin = frequency.sin().unsqueeze(1) + reference = torch.cat( + ( + x[..., :half].float() * cos - x[..., half:].float() * sin, + x[..., half:].float() * cos + x[..., :half].float() * sin, + ), + dim=-1, + ).to(x.dtype) + torch.testing.assert_close(actual, reference, atol=0, rtol=0) + + +def test_rocm_strict_core_rejects_split_k(): + with pytest.raises(ValueError, match="Split-KV"): + RLKernelDeterministicAttentionCore(split_kv=SplitKVSpec.fixed(32)) From 21486d9fc971f8bcb3aad1ea05abe3fc914b033c Mon Sep 17 00:00:00 2001 From: root Date: Wed, 19 Aug 2026 09:29:56 +0000 Subject: [PATCH 02/35] test(attention): make ROCm acceptance imports explicit --- .../kernels/ops/cuda/attention/flashinfer_paged_attention.py | 3 ++- scripts/__init__.py | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 scripts/__init__.py diff --git a/rl_engine/kernels/ops/cuda/attention/flashinfer_paged_attention.py b/rl_engine/kernels/ops/cuda/attention/flashinfer_paged_attention.py index 0a6bdeed..deab2628 100644 --- a/rl_engine/kernels/ops/cuda/attention/flashinfer_paged_attention.py +++ b/rl_engine/kernels/ops/cuda/attention/flashinfer_paged_attention.py @@ -775,7 +775,8 @@ def _run_strict_cp( communication, "supports_autograd", False ): raise FlashInferUnavailable( - "strict training requires an autograd-capable self-owned GPU AG/RS backend" + "strict training requires an autograd-capable self-owned CUDA AG/RS " + "or ROCm RCCL AG/RS backend" ) query_start, query_end = plan.query_token_ranges[plan.parallel.cp_rank] key_start, key_end = _cp_owner_ranges(plan)[plan.parallel.cp_rank] diff --git a/scripts/__init__.py b/scripts/__init__.py new file mode 100644 index 00000000..5295edc3 --- /dev/null +++ b/scripts/__init__.py @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2026 RL-Kernel Contributors + +"""Repository-local validation scripts.""" From 674d7ba0bfa54fdb10b153a9013bb35eafd8875c Mon Sep 17 00:00:00 2001 From: root Date: Wed, 19 Aug 2026 10:03:53 +0000 Subject: [PATCH 03/35] style-rocm-attention-lint --- rl_engine/kernels/ops/cuda/attention/cp_comm.py | 7 +++---- .../cuda/attention/flashinfer_paged_attention.py | 10 +++------- rl_engine/kernels/ops/rocm/attention/__init__.py | 5 +---- .../kernels/ops/rocm/rotary_embedding/rope.py | 4 +--- scripts/rocm_attention_acceptance.py | 4 +++- .../ws2_p2p_nccl_attention_reference_check.py | 4 +--- scripts/ws2_pr7_flashinfer_attention_check.py | 16 +++++++--------- setup.py | 4 +--- tests/test_cp_attention.py | 2 +- tests/test_deterministic_attention_rocm.py | 9 ++------- 10 files changed, 23 insertions(+), 42 deletions(-) diff --git a/rl_engine/kernels/ops/cuda/attention/cp_comm.py b/rl_engine/kernels/ops/cuda/attention/cp_comm.py index 20602ede..798b1469 100644 --- a/rl_engine/kernels/ops/cuda/attention/cp_comm.py +++ b/rl_engine/kernels/ops/cuda/attention/cp_comm.py @@ -652,8 +652,6 @@ def reduce_scatter(self, full: torch.Tensor) -> torch.Tensor: performed locally in source-rank order, so collective reduction order is not delegated to RCCL. """ - import torch.distributed as dist - if not full.is_cuda or not full.is_contiguous(): raise AttentionCPCommunicationUnavailable( "RCCL ReduceScatter requires a contiguous ROCm tensor" @@ -664,8 +662,9 @@ def reduce_scatter(self, full: torch.Tensor) -> torch.Tensor: ) chunks = tuple(chunk.contiguous() for chunk in full.chunk(self.world_size, dim=0)) gathered = self.all_gather(full) - local = gathered[self.rank * chunks[self.rank].size(0) : - (self.rank + 1) * chunks[self.rank].size(0)].clone() + local = gathered[ + self.rank * chunks[self.rank].size(0) : (self.rank + 1) * chunks[self.rank].size(0) + ].clone() chunk_rows = chunks[self.rank].size(0) # Start with source rank 0, then add the remaining source ranks in # ascending order. This avoids counting source 0 twice. diff --git a/rl_engine/kernels/ops/cuda/attention/flashinfer_paged_attention.py b/rl_engine/kernels/ops/cuda/attention/flashinfer_paged_attention.py index deab2628..c242d119 100644 --- a/rl_engine/kernels/ops/cuda/attention/flashinfer_paged_attention.py +++ b/rl_engine/kernels/ops/cuda/attention/flashinfer_paged_attention.py @@ -1458,8 +1458,7 @@ def _validate_strict_core(core: Any) -> None: raise ValueError("strict deterministic core ID must be " f"{STRICT_ATTENTION_CORE_ID!r}") if getattr(core, "strict_schedule", None) != STRICT_ATTENTION_SCHEDULE_ID: raise ValueError( - "strict deterministic core schedule must be " - f"{STRICT_ATTENTION_SCHEDULE_ID!r}" + "strict deterministic core schedule must be " f"{STRICT_ATTENTION_SCHEDULE_ID!r}" ) required = { "merge_order": "global_block_index", @@ -1517,9 +1516,7 @@ def _resolve_strict_rope(cfg: FlashInferPagedAttentionConfig) -> Any: return cfg.strict_rope_op try: if torch.version.hip is not None: - from rl_engine.kernels.ops.rocm.rotary_embedding.rope import ( - RocmDeterministicRoPEOp, - ) + from rl_engine.kernels.ops.rocm.rotary_embedding.rope import RocmDeterministicRoPEOp return RocmDeterministicRoPEOp() from rl_engine.kernels.ops.cuda.rotary_embedding.rope import RoPESM90Op @@ -1713,8 +1710,7 @@ def _strict_attention_provenance( "rlkernel.cuda.deterministic_attention", "rlkernel.rocm.deterministic_attention", } - and getattr(cfg.cp_communication, "backend_id", None) - in {"cuda_ag_rs", "rccl_ag_rs"} + and getattr(cfg.cp_communication, "backend_id", None) in {"cuda_ag_rs", "rccl_ag_rs"} ), } diff --git a/rl_engine/kernels/ops/rocm/attention/__init__.py b/rl_engine/kernels/ops/rocm/attention/__init__.py index 2445e2de..b03448f5 100644 --- a/rl_engine/kernels/ops/rocm/attention/__init__.py +++ b/rl_engine/kernels/ops/rocm/attention/__init__.py @@ -1,11 +1,8 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright (c) 2026 RL-Kernel Contributors +from .deterministic_attn import DeterministicAttentionOp, RLKernelDeterministicAttentionCore from .flash_attn import RocmFlashAttentionOp -from .deterministic_attn import ( - DeterministicAttentionOp, - RLKernelDeterministicAttentionCore, -) __all__ = [ "RocmFlashAttentionOp", diff --git a/rl_engine/kernels/ops/rocm/rotary_embedding/rope.py b/rl_engine/kernels/ops/rocm/rotary_embedding/rope.py index 3290a9c1..7b0f385e 100644 --- a/rl_engine/kernels/ops/rocm/rotary_embedding/rope.py +++ b/rl_engine/kernels/ops/rocm/rotary_embedding/rope.py @@ -16,9 +16,7 @@ def _build_cos_sin( theta: float, device: torch.device, ) -> tuple[Tensor, Tensor]: - inv_freq = 1.0 / ( - theta ** (torch.arange(0, half, dtype=torch.float32, device=device) / half) - ) + inv_freq = 1.0 / (theta ** (torch.arange(0, half, dtype=torch.float32, device=device) / half)) frequency = positions.to(device=device, dtype=torch.float32).reshape(-1, 1) * inv_freq return frequency.cos().contiguous(), frequency.sin().contiguous() diff --git a/scripts/rocm_attention_acceptance.py b/scripts/rocm_attention_acceptance.py index 9ef1440d..bdb4adcb 100644 --- a/scripts/rocm_attention_acceptance.py +++ b/scripts/rocm_attention_acceptance.py @@ -21,7 +21,9 @@ def main() -> int: raise RuntimeError("the formal acceptance matrix requires 8 visible ROCm GPUs") repo = Path(__file__).resolve().parents[1] - output = (repo / args.output_dir).resolve() if not args.output_dir.is_absolute() else args.output_dir + output = ( + (repo / args.output_dir).resolve() if not args.output_dir.is_absolute() else args.output_dir + ) output.mkdir(parents=True, exist_ok=True) steps: list[dict[str, object]] = [] diff --git a/scripts/ws2_p2p_nccl_attention_reference_check.py b/scripts/ws2_p2p_nccl_attention_reference_check.py index 963e5020..1f12b0ca 100644 --- a/scripts/ws2_p2p_nccl_attention_reference_check.py +++ b/scripts/ws2_p2p_nccl_attention_reference_check.py @@ -486,9 +486,7 @@ def _run_strict_shared_core_check( def _strict_rope_op(): if torch.version.hip is not None: - from rl_engine.kernels.ops.rocm.rotary_embedding.rope import ( - RocmDeterministicRoPEOp, - ) + from rl_engine.kernels.ops.rocm.rotary_embedding.rope import RocmDeterministicRoPEOp return RocmDeterministicRoPEOp() from rl_engine.kernels.ops.cuda.rotary_embedding.rope import RoPESM90Op diff --git a/scripts/ws2_pr7_flashinfer_attention_check.py b/scripts/ws2_pr7_flashinfer_attention_check.py index 4e0b9895..8fc95800 100644 --- a/scripts/ws2_pr7_flashinfer_attention_check.py +++ b/scripts/ws2_pr7_flashinfer_attention_check.py @@ -25,13 +25,16 @@ if str(REPO_ROOT) not in sys.path: sys.path.insert(0, str(REPO_ROOT)) +from rl_engine.kernels.attention_contract import ( # noqa: E402 + STRICT_ATTENTION_CORE_ID, + STRICT_ATTENTION_SCHEDULE_ID, +) from rl_engine.kernels.ops.cuda.attention.cp_comm import ( # noqa: E402 AttentionCPCommunicationPlan, AttentionParallelSpec, ) -from rl_engine.kernels.attention_contract import ( # noqa: E402 - STRICT_ATTENTION_CORE_ID, - STRICT_ATTENTION_SCHEDULE_ID, +from rl_engine.kernels.ops.cuda.attention.deterministic_attn import ( # noqa: E402 + RLKernelDeterministicAttentionCore, ) from rl_engine.kernels.ops.cuda.attention.flashinfer_paged_attention import ( # noqa: E402 FlashInferPagedAttentionConfig, @@ -43,9 +46,6 @@ _materialize_strict_logical_kv, build_flashinfer_paged_kv_plan, ) -from rl_engine.kernels.ops.cuda.attention.deterministic_attn import ( # noqa: E402 - RLKernelDeterministicAttentionCore, -) from rl_engine.kernels.ops.cuda.rotary_embedding.rope import RoPESM90Op # noqa: E402 from rl_engine.testing.attention_comparison import ( # noqa: E402 AttentionPathResult, @@ -400,9 +400,7 @@ def _run_strict_cuda_reference( ) -> AttentionPathResult: """Call the shared CUDA core directly on the logical KV sequence.""" - core = config.deterministic_core or RLKernelDeterministicAttentionCore( - split_kv=config.split_kv - ) + core = config.deterministic_core or RLKernelDeterministicAttentionCore(split_kv=config.split_kv) rope = config.strict_rope_op or RoPESM90Op() logical_k, logical_v, key_positions = _materialize_strict_logical_kv( inputs.k_cache, diff --git a/setup.py b/setup.py index 695f0419..297d269d 100644 --- a/setup.py +++ b/setup.py @@ -217,9 +217,7 @@ def get_extensions(): nvcc_flags.append("-allow-unsupported-compiler") nvcc_flags.append("-D_ALLOW_COMPILER_AND_STL_VERSION_MISMATCH") - platform_define = ( - "-DKERNEL_ALIGN_WITH_ROCM" if is_rocm else "-DKERNEL_ALIGN_WITH_CUDA" - ) + platform_define = "-DKERNEL_ALIGN_WITH_ROCM" if is_rocm else "-DKERNEL_ALIGN_WITH_CUDA" cxx_flags = ["-O3", "-std=c++17", platform_define] extra_link_args = list(torch_rpath) diff --git a/tests/test_cp_attention.py b/tests/test_cp_attention.py index 4ad82ea8..73953090 100644 --- a/tests/test_cp_attention.py +++ b/tests/test_cp_attention.py @@ -18,10 +18,10 @@ STRICT_ATTENTION_CORE_ID, STRICT_ATTENTION_SCHEDULE_ID, ) +from rl_engine.kernels.ops.cuda.attention import deterministic_attn as deterministic_attn_module from rl_engine.kernels.ops.cuda.attention.deterministic_attn import ( RLKernelDeterministicAttentionCore, ) -from rl_engine.kernels.ops.cuda.attention import deterministic_attn as deterministic_attn_module from rl_engine.kernels.ops.pytorch.attention.cp_attention import ( AttentionPartialState, DeterministicCPAttentionReferenceOp, diff --git a/tests/test_deterministic_attention_rocm.py b/tests/test_deterministic_attention_rocm.py index 519eee66..a2f95121 100644 --- a/tests/test_deterministic_attention_rocm.py +++ b/tests/test_deterministic_attention_rocm.py @@ -2,8 +2,6 @@ # Copyright (c) 2026 RL-Kernel Contributors """ROCm strict Attention acceptance tests that never use a native fallback.""" -import math - import pytest import torch @@ -16,9 +14,7 @@ from rl_engine.kernels.ops.rocm.attention.deterministic_attn import ( # noqa: E402 RLKernelDeterministicAttentionCore, ) -from rl_engine.kernels.ops.rocm.rotary_embedding.rope import ( # noqa: E402 - RocmDeterministicRoPEOp, -) +from rl_engine.kernels.ops.rocm.rotary_embedding.rope import RocmDeterministicRoPEOp # noqa: E402 def _qkv(*, batch: int = 2, sequence: int = 7): @@ -110,8 +106,7 @@ def test_rocm_rope_matches_fp32_rotate_half_reference(): actual = RocmDeterministicRoPEOp()(x, positions) half = x.size(-1) // 2 inv_freq = 1.0 / ( - 1_000_000.0 - ** (torch.arange(half, dtype=torch.float32, device=x.device) / half) + 1_000_000.0 ** (torch.arange(half, dtype=torch.float32, device=x.device) / half) ) frequency = positions.float().unsqueeze(-1) * inv_freq cos = frequency.cos().unsqueeze(1) From 0934ca4002ee295c6d0451eaf55e0683d3cdd624 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 19 Aug 2026 13:38:48 +0000 Subject: [PATCH 04/35] test-unify-cuda-rocm-attention --- docs/operators/attention.md | 2 +- scripts/rocm_attention_acceptance.py | 2 +- ...uda.py => test_deterministic_attention.py} | 284 +++++++++++++----- tests/test_deterministic_attention_rocm.py | 126 -------- 4 files changed, 214 insertions(+), 200 deletions(-) rename tests/{test_deterministic_attention_cuda.py => test_deterministic_attention.py} (69%) delete mode 100644 tests/test_deterministic_attention_rocm.py diff --git a/docs/operators/attention.md b/docs/operators/attention.md index ebff9a58..66682ced 100644 --- a/docs/operators/attention.md +++ b/docs/operators/attention.md @@ -153,7 +153,7 @@ GPU-only LARGE Qwen3-8B real-shape smoke test. - `csrc/cuda/attention/deterministic_attention.cu` — CUDA kernels - `rl_engine/kernels/registry.py` - `tests/test_attention.py` -- `tests/test_deterministic_attention_cuda.py` +- `tests/test_deterministic_attention.py` ## Fixed Reduction Order (CUDA Deterministic Backend) diff --git a/scripts/rocm_attention_acceptance.py b/scripts/rocm_attention_acceptance.py index bdb4adcb..1423ab2b 100644 --- a/scripts/rocm_attention_acceptance.py +++ b/scripts/rocm_attention_acceptance.py @@ -30,7 +30,7 @@ def main() -> int: commands = [ ( "single_gpu", - [sys.executable, "-m", "pytest", "-q", "tests/test_deterministic_attention_rocm.py"], + [sys.executable, "-m", "pytest", "-q", "tests/test_deterministic_attention.py"], ) ] for transport, strict in (("p2p_nccl_reference", False), ("rccl_ag_rs", True)): diff --git a/tests/test_deterministic_attention_cuda.py b/tests/test_deterministic_attention.py similarity index 69% rename from tests/test_deterministic_attention_cuda.py rename to tests/test_deterministic_attention.py index f08315a8..d616ff4a 100644 --- a/tests/test_deterministic_attention_cuda.py +++ b/tests/test_deterministic_attention.py @@ -1,7 +1,7 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright (c) 2026 RL-Kernel Contributors -"""Deterministic standard-softmax attention CUDA tests (issue #147). +"""Deterministic standard-softmax Attention tests for CUDA and ROCm. Covers (per §7 and §8 of the implementation plan): - Forward correctness via #108 harness (run_operator_suite) @@ -23,28 +23,50 @@ import pytest import torch -pytestmark = pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA not available") +from rl_engine.platforms.device import device_ctx + +IS_ROCM = device_ctx.is_rocm +IS_CUDA = device_ctx.device_type == "cuda" +IS_GPU = IS_CUDA or IS_ROCM +BACKEND = "rocm" if IS_ROCM else "cuda" + +pytestmark = pytest.mark.skipif(not IS_GPU, reason="CUDA/ROCm GPU not available") +ROCM_ONLY = pytest.mark.skipif(not IS_ROCM, reason="ROCm-only acceptance check") try: + from rl_engine.kernels.attention_contract import SplitKVSpec from rl_engine.kernels.gtest.op_checks import CandidateSpec, OperatorCase, run_operator_suite - from rl_engine.kernels.ops.cuda.attention.deterministic_attn import DeterministicAttentionOp from rl_engine.kernels.ops.pytorch.attention.standard_attn import NativeAttentionOp + if IS_ROCM: + from rl_engine.kernels.ops.rocm.attention.deterministic_attn import ( + DeterministicAttentionOp, + RLKernelDeterministicAttentionCore, + ) + else: + from rl_engine.kernels.ops.cuda.attention.deterministic_attn import ( + DeterministicAttentionOp, + RLKernelDeterministicAttentionCore, + ) + _OP_AVAILABLE = True except (ImportError, RuntimeError): _OP_AVAILABLE = False +if IS_ROCM: + from rl_engine.kernels.ops.rocm.rotary_embedding.rope import RocmDeterministicRoPEOp + pytestmark = [ pytestmark, - pytest.mark.skipif(not _OP_AVAILABLE, reason="CUDA attention op not built"), + pytest.mark.skipif(not _OP_AVAILABLE, reason=f"{BACKEND} attention op not built"), ] -DEVICE = "cuda" +DEVICE = device_ctx.device D = 128 @pytest.fixture -def cuda_op(): +def attention_op(): return DeterministicAttentionOp() @@ -109,8 +131,8 @@ def _build_harness_cases(): def test_harness_forward(): """§8.4: run_operator_suite forward — candidate vs gold (accuracy tolerance).""" - cuda = DeterministicAttentionOp() - candidate = CandidateSpec(name="cuda-attention", fn=cuda, backend="cuda") + attention = DeterministicAttentionOp() + candidate = CandidateSpec(name=f"{BACKEND}-attention", fn=attention, backend=BACKEND) report = run_operator_suite("attention", candidates=[candidate], cases=_build_harness_cases()) for cr in report.candidates: for case in cr.cases: @@ -126,8 +148,8 @@ def test_harness_forward(): def test_harness_backward(): """§8.4: run_operator_suite backward — grad comparison vs gold fp32 autograd.""" - cuda = DeterministicAttentionOp() - candidate = CandidateSpec(name="cuda-attention", fn=cuda, backend="cuda") + attention = DeterministicAttentionOp() + candidate = CandidateSpec(name=f"{BACKEND}-attention", fn=attention, backend=BACKEND) report = run_operator_suite( "attention", candidates=[candidate], @@ -163,18 +185,18 @@ def test_harness_backward(): @pytest.mark.parametrize("dtype,hq,hkv,sq,skv,causal", SWEEP_CONFIGS) -def test_forward_sweep(cuda_op, gold_op, dtype, hq, hkv, sq, skv, causal): +def test_forward_sweep(attention_op, gold_op, dtype, hq, hkv, sq, skv, causal): B = 2 torch.manual_seed(42) q = torch.randn(B, hq, sq, D, device=DEVICE, dtype=dtype) k = torch.randn(B, hkv, skv, D, device=DEVICE, dtype=dtype) v = torch.randn(B, hkv, skv, D, device=DEVICE, dtype=dtype) - out_cuda = cuda_op.forward(q, k, v, causal=causal) + out_actual = attention_op.forward(q, k, v, causal=causal) out_gold = gold_op.forward_fp32(q, k, v, causal=causal) atol, rtol = _tol(dtype) - torch.testing.assert_close(out_cuda.float(), out_gold.float(), atol=atol, rtol=rtol) + torch.testing.assert_close(out_actual.float(), out_gold.float(), atol=atol, rtol=rtol) # ============================================================================= @@ -183,18 +205,18 @@ def test_forward_sweep(cuda_op, gold_op, dtype, hq, hkv, sq, skv, causal): @pytest.mark.parametrize("scale", [None, 0.0, 0.05]) -def test_scale(cuda_op, gold_op, scale): +def test_scale(attention_op, gold_op, scale): B, hq, hkv, sq, skv = 2, 4, 1, 8, 16 torch.manual_seed(42) q = torch.randn(B, hq, sq, D, device=DEVICE, dtype=torch.bfloat16) k = torch.randn(B, hkv, skv, D, device=DEVICE, dtype=torch.bfloat16) v = torch.randn(B, hkv, skv, D, device=DEVICE, dtype=torch.bfloat16) - out_cuda = cuda_op.forward(q, k, v, causal=True, scale=scale) + out_actual = attention_op.forward(q, k, v, causal=True, scale=scale) out_gold = gold_op.forward_fp32(q, k, v, causal=True, scale=scale) atol, rtol = _tol(torch.bfloat16) - torch.testing.assert_close(out_cuda.float(), out_gold.float(), atol=atol, rtol=rtol) + torch.testing.assert_close(out_actual.float(), out_gold.float(), atol=atol, rtol=rtol) # ============================================================================= @@ -203,7 +225,7 @@ def test_scale(cuda_op, gold_op, scale): @pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16]) -def test_forward_with_padding(cuda_op, gold_op, dtype): +def test_forward_with_padding(attention_op, gold_op, dtype): B, hq, hkv, sq, skv = 2, 4, 1, 8, 16 torch.manual_seed(7) q = torch.randn(B, hq, sq, D, device=DEVICE, dtype=dtype) @@ -213,21 +235,21 @@ def test_forward_with_padding(cuda_op, gold_op, dtype): mask[0, 10:] = False mask[1, 12:] = False - out_cuda = cuda_op.forward(q, k, v, causal=True, key_padding_mask=mask) + out_actual = attention_op.forward(q, k, v, causal=True, key_padding_mask=mask) out_gold = gold_op.forward_fp32(q, k, v, causal=True, key_padding_mask=mask) atol, rtol = _tol(dtype) - torch.testing.assert_close(out_cuda.float(), out_gold.float(), atol=atol, rtol=rtol) + torch.testing.assert_close(out_actual.float(), out_gold.float(), atol=atol, rtol=rtol) -def test_fully_masked_row(cuda_op): +def test_fully_masked_row(attention_op): B, hq, hkv, sq, skv = 1, 1, 1, 2, 4 q = torch.randn(B, hq, sq, D, device=DEVICE, dtype=torch.bfloat16) k = torch.randn(B, hkv, skv, D, device=DEVICE, dtype=torch.bfloat16) v = torch.randn(B, hkv, skv, D, device=DEVICE, dtype=torch.bfloat16) mask = torch.zeros(B, skv, device=DEVICE, dtype=torch.bool) - out, lse = cuda_op.forward_with_lse(q, k, v, causal=False, key_padding_mask=mask) + out, lse = attention_op.forward_with_lse(q, k, v, causal=False, key_padding_mask=mask) assert (out == 0).all() assert (lse == float("-inf")).all() @@ -237,14 +259,14 @@ def test_fully_masked_row(cuda_op): # ============================================================================= -def test_lse_correctness(cuda_op): +def test_lse_correctness(attention_op): B, hq, hkv, sq, skv = 2, 4, 1, 8, 16 torch.manual_seed(99) q = torch.randn(B, hq, sq, D, device=DEVICE, dtype=torch.bfloat16) k = torch.randn(B, hkv, skv, D, device=DEVICE, dtype=torch.bfloat16) v = torch.randn(B, hkv, skv, D, device=DEVICE, dtype=torch.bfloat16) - _, lse_cuda = cuda_op.forward_with_lse(q, k, v, causal=True) + _, lse_actual = attention_op.forward_with_lse(q, k, v, causal=True) scale = 1.0 / math.sqrt(D) g = hq // hkv @@ -256,7 +278,7 @@ def test_lse_correctness(cuda_op): scores = scores.masked_fill(~causal_mask.unsqueeze(0).unsqueeze(0), float("-inf")) lse_gold = torch.logsumexp(scores, dim=-1) - torch.testing.assert_close(lse_cuda, lse_gold, atol=5e-2, rtol=2e-2) + torch.testing.assert_close(lse_actual, lse_gold, atol=5e-2, rtol=2e-2) # ============================================================================= @@ -264,7 +286,7 @@ def test_lse_correctness(cuda_op): # ============================================================================= -def test_valid_only_vs_padded_accuracy(cuda_op): +def test_valid_only_vs_padded_accuracy(attention_op): """Padding changes reduction width; result is near-equal, not bitwise. We use causal=False here so that all valid keys are equally visible @@ -295,8 +317,8 @@ def test_valid_only_vs_padded_accuracy(cuda_op): mask = torch.ones(B, skv_padded, device=DEVICE, dtype=torch.bool) mask[:, skv_valid:] = False - out_valid = cuda_op.forward(q, k_valid, v_valid, causal=False) - out_padded = cuda_op.forward(q, k_padded, v_padded, causal=False, key_padding_mask=mask) + out_valid = attention_op.forward(q, k_valid, v_valid, causal=False) + out_padded = attention_op.forward(q, k_padded, v_padded, causal=False, key_padding_mask=mask) atol, rtol = _tol(torch.bfloat16) torch.testing.assert_close(out_valid.float(), out_padded.float(), atol=atol, rtol=rtol) @@ -307,7 +329,7 @@ def test_valid_only_vs_padded_accuracy(cuda_op): # ============================================================================= -def test_batch_invariance_single(cuda_op): +def test_batch_invariance_single(attention_op): """Same sample in full batch vs extracted single — bitwise.""" B, hq, hkv, sq, skv = 4, 4, 1, 8, 16 torch.manual_seed(11) @@ -315,17 +337,17 @@ def test_batch_invariance_single(cuda_op): k = torch.randn(B, hkv, skv, D, device=DEVICE, dtype=torch.bfloat16) v = torch.randn(B, hkv, skv, D, device=DEVICE, dtype=torch.bfloat16) - out_full, lse_full = cuda_op.forward_with_lse(q, k, v, causal=True) + out_full, lse_full = attention_op.forward_with_lse(q, k, v, causal=True) for i in range(B): - out_single, lse_single = cuda_op.forward_with_lse( + out_single, lse_single = attention_op.forward_with_lse( q[i : i + 1], k[i : i + 1], v[i : i + 1], causal=True ) assert torch.equal(out_full[i : i + 1], out_single), f"Output batch invariance failed i={i}" assert torch.equal(lse_full[i : i + 1], lse_single), f"LSE batch invariance failed i={i}" -def test_batch_invariance_position_permutation(cuda_op): +def test_batch_invariance_position_permutation(attention_op): """Same sample at different batch positions — bitwise.""" B, hq, hkv, sq, skv = 4, 32, 8, 8, 16 torch.manual_seed(12) @@ -333,13 +355,13 @@ def test_batch_invariance_position_permutation(cuda_op): k = torch.randn(B, hkv, skv, D, device=DEVICE, dtype=torch.bfloat16) v = torch.randn(B, hkv, skv, D, device=DEVICE, dtype=torch.bfloat16) - out_full = cuda_op.forward(q, k, v, causal=True) + out_full = attention_op.forward(q, k, v, causal=True) perm = [2, 0, 3, 1] q_perm = q[perm] k_perm = k[perm] v_perm = v[perm] - out_perm = cuda_op.forward(q_perm, k_perm, v_perm, causal=True) + out_perm = attention_op.forward(q_perm, k_perm, v_perm, causal=True) for new_pos, orig_pos in enumerate(perm): assert torch.equal( @@ -347,7 +369,7 @@ def test_batch_invariance_position_permutation(cuda_op): ), f"Position permutation invariance failed: orig={orig_pos} new={new_pos}" -def test_batch_invariance_chunk(cuda_op): +def test_batch_invariance_chunk(attention_op): """Batch-dim chunking — bitwise.""" B, hq, hkv, sq, skv = 4, 4, 1, 8, 16 torch.manual_seed(13) @@ -355,11 +377,11 @@ def test_batch_invariance_chunk(cuda_op): k = torch.randn(B, hkv, skv, D, device=DEVICE, dtype=torch.bfloat16) v = torch.randn(B, hkv, skv, D, device=DEVICE, dtype=torch.bfloat16) - out_full = cuda_op.forward(q, k, v, causal=True) + out_full = attention_op.forward(q, k, v, causal=True) out_chunk = torch.cat( [ - cuda_op.forward(q[:2], k[:2], v[:2], causal=True), - cuda_op.forward(q[2:], k[2:], v[2:], causal=True), + attention_op.forward(q[:2], k[:2], v[:2], causal=True), + attention_op.forward(q[2:], k[2:], v[2:], causal=True), ], dim=0, ) @@ -381,20 +403,22 @@ def test_batch_invariance_chunk(cuda_op): (3, 32, 8), ], ) -def test_chunked_prefill(cuda_op, chunk_size, hq, hkv): +def test_chunked_prefill(attention_op, chunk_size, hq, hkv): B, T = 1, 16 torch.manual_seed(22) q = torch.randn(B, hq, T, D, device=DEVICE, dtype=torch.bfloat16) k = torch.randn(B, hkv, T, D, device=DEVICE, dtype=torch.bfloat16) v = torch.randn(B, hkv, T, D, device=DEVICE, dtype=torch.bfloat16) - out_full = cuda_op.forward(q, k, v, causal=True) + out_full = attention_op.forward(q, k, v, causal=True) outs = [] for t in range(0, T, chunk_size): c = min(chunk_size, T - t) outs.append( - cuda_op.forward(q[:, :, t : t + c], k[:, :, : t + c], v[:, :, : t + c], causal=True) + attention_op.forward( + q[:, :, t : t + c], k[:, :, : t + c], v[:, :, : t + c], causal=True + ) ) out_chunked = torch.cat(outs, dim=2) assert torch.equal( @@ -403,7 +427,7 @@ def test_chunked_prefill(cuda_op, chunk_size, hq, hkv): @pytest.mark.parametrize("chunk_size", [1, 3, 8]) -def test_chunked_prefill_with_padding(cuda_op, chunk_size): +def test_chunked_prefill_with_padding(attention_op, chunk_size): """§7.4: chunked-prefill with key_padding_mask (mask sliced with Skv).""" B, hq, hkv, T = 1, 4, 1, 16 torch.manual_seed(23) @@ -413,13 +437,13 @@ def test_chunked_prefill_with_padding(cuda_op, chunk_size): mask = torch.ones(B, T, device=DEVICE, dtype=torch.bool) mask[0, 12:] = False - out_full = cuda_op.forward(q, k, v, causal=True, key_padding_mask=mask) + out_full = attention_op.forward(q, k, v, causal=True, key_padding_mask=mask) outs = [] for t in range(0, T, chunk_size): c = min(chunk_size, T - t) outs.append( - cuda_op.forward( + attention_op.forward( q[:, :, t : t + c], k[:, :, : t + c], v[:, :, : t + c], @@ -434,7 +458,7 @@ def test_chunked_prefill_with_padding(cuda_op, chunk_size): @pytest.mark.parametrize("chunk_size", [1, 3]) -def test_chunked_prefill_lse(cuda_op, chunk_size): +def test_chunked_prefill_lse(attention_op, chunk_size): """§7.4: LSE chunked-prefill invariance.""" B, hq, hkv, T = 1, 4, 1, 12 torch.manual_seed(24) @@ -442,12 +466,12 @@ def test_chunked_prefill_lse(cuda_op, chunk_size): k = torch.randn(B, hkv, T, D, device=DEVICE, dtype=torch.bfloat16) v = torch.randn(B, hkv, T, D, device=DEVICE, dtype=torch.bfloat16) - _, lse_full = cuda_op.forward_with_lse(q, k, v, causal=True) + _, lse_full = attention_op.forward_with_lse(q, k, v, causal=True) lses = [] for t in range(0, T, chunk_size): c = min(chunk_size, T - t) - _, lse_chunk = cuda_op.forward_with_lse( + _, lse_chunk = attention_op.forward_with_lse( q[:, :, t : t + c], k[:, :, : t + c], v[:, :, : t + c], causal=True ) lses.append(lse_chunk) @@ -460,7 +484,7 @@ def test_chunked_prefill_lse(cuda_op, chunk_size): # ============================================================================= -def test_prefill_decode_slice(cuda_op): +def test_prefill_decode_slice(attention_op): """§7.5.1: prefill[:, :, -1:] == decode(q[-1:], k_full, v_full).""" B, hq, hkv, sq, skv = 1, 4, 1, 8, 16 torch.manual_seed(33) @@ -468,8 +492,8 @@ def test_prefill_decode_slice(cuda_op): k = torch.randn(B, hkv, skv, D, device=DEVICE, dtype=torch.bfloat16) v = torch.randn(B, hkv, skv, D, device=DEVICE, dtype=torch.bfloat16) - prefill = cuda_op.forward(q, k, v, causal=True) - decode = cuda_op.forward(q[:, :, -1:], k, v, causal=True) + prefill = attention_op.forward(q, k, v, causal=True) + decode = attention_op.forward(q[:, :, -1:], k, v, causal=True) assert torch.equal(prefill[:, :, -1:], decode) @@ -482,7 +506,7 @@ def test_prefill_decode_slice(cuda_op): (3, 32, 8), ], ) -def test_kv_cache_handoff(cuda_op, S_new, hq, hkv): +def test_kv_cache_handoff(attention_op, S_new, hq, hkv): """§7.5.2: cat(k_cache, k_new) handoff == prefill tail.""" B, S_past = 1, 12 torch.manual_seed(44) @@ -491,12 +515,12 @@ def test_kv_cache_handoff(cuda_op, S_new, hq, hkv): v_full = torch.randn(B, hkv, S_past + S_new, D, device=DEVICE, dtype=torch.bfloat16) q_new = q_full[:, :, -S_new:] - prefill_tail = cuda_op.forward(q_full, k_full, v_full, causal=True)[:, :, -S_new:] - decode_path = cuda_op.forward(q_new, k_full, v_full, causal=True) + prefill_tail = attention_op.forward(q_full, k_full, v_full, causal=True)[:, :, -S_new:] + decode_path = attention_op.forward(q_new, k_full, v_full, causal=True) assert torch.equal(decode_path, prefill_tail) -def test_kv_cache_handoff_with_padding(cuda_op): +def test_kv_cache_handoff_with_padding(attention_op): """§7.5.2: cat handoff with padding mask.""" B, hq, hkv, S_past, S_new = 1, 4, 1, 12, 1 Skv = S_past + S_new @@ -508,10 +532,10 @@ def test_kv_cache_handoff_with_padding(cuda_op): mask[0, 8:10] = False q_new = q_full[:, :, -S_new:] - prefill_tail = cuda_op.forward(q_full, k_full, v_full, causal=True, key_padding_mask=mask)[ + prefill_tail = attention_op.forward(q_full, k_full, v_full, causal=True, key_padding_mask=mask)[ :, :, -S_new: ] - decode_path = cuda_op.forward(q_new, k_full, v_full, causal=True, key_padding_mask=mask) + decode_path = attention_op.forward(q_new, k_full, v_full, causal=True, key_padding_mask=mask) assert torch.equal(decode_path, prefill_tail) @@ -520,7 +544,7 @@ def test_kv_cache_handoff_with_padding(cuda_op): # ============================================================================= -def test_backward_smoke(cuda_op): +def test_backward_smoke(attention_op): """Backward runs and produces gradients with correct shapes.""" B, hq, hkv, sq, skv = 1, 4, 1, 4, 8 torch.manual_seed(55) @@ -528,7 +552,7 @@ def test_backward_smoke(cuda_op): k = torch.randn(B, hkv, skv, D, device=DEVICE, dtype=torch.bfloat16, requires_grad=True) v = torch.randn(B, hkv, skv, D, device=DEVICE, dtype=torch.bfloat16, requires_grad=True) - out = cuda_op.forward(q, k, v, causal=True) + out = attention_op.forward(q, k, v, causal=True) loss = out.sum() loss.backward() @@ -537,7 +561,7 @@ def test_backward_smoke(cuda_op): assert v.grad is not None and v.grad.shape == v.shape -def test_backward_fp64_reference(cuda_op): +def test_backward_fp64_reference(attention_op): """§7.6: FP64 high-precision gradient comparison.""" B, hq, hkv, sq, skv = 1, 4, 1, 4, 8 torch.manual_seed(56) @@ -547,11 +571,11 @@ def test_backward_fp64_reference(cuda_op): grad_out = torch.randn(B, hq, sq, D, device=DEVICE, dtype=torch.float32) - out_cuda = cuda_op.forward(q_bf16, k_bf16, v_bf16, causal=True) - out_cuda.backward(grad_out.to(out_cuda.dtype)) - dq_cuda = q_bf16.grad.float() - dk_cuda = k_bf16.grad.float() - dv_cuda = v_bf16.grad.float() + out_actual = attention_op.forward(q_bf16, k_bf16, v_bf16, causal=True) + out_actual.backward(grad_out.to(out_actual.dtype)) + dq_actual = q_bf16.grad.float() + dk_actual = k_bf16.grad.float() + dv_actual = v_bf16.grad.float() scale = 1.0 / math.sqrt(D) g = hq // hkv @@ -572,12 +596,12 @@ def test_backward_fp64_reference(cuda_op): dk_gold = k64.grad.float() dv_gold = v64.grad.float() - torch.testing.assert_close(dq_cuda, dq_gold, atol=5e-2, rtol=2e-2) - torch.testing.assert_close(dk_cuda, dk_gold, atol=5e-2, rtol=2e-2) - torch.testing.assert_close(dv_cuda, dv_gold, atol=5e-2, rtol=2e-2) + torch.testing.assert_close(dq_actual, dq_gold, atol=5e-2, rtol=2e-2) + torch.testing.assert_close(dk_actual, dk_gold, atol=5e-2, rtol=2e-2) + torch.testing.assert_close(dv_actual, dv_gold, atol=5e-2, rtol=2e-2) -def test_gradient_batch_invariance(cuda_op): +def test_gradient_batch_invariance(attention_op): """§7.6: dQ/dK/dV bitwise identical for same sample at different batch positions.""" B, hq, hkv, sq, skv = 3, 4, 1, 4, 8 torch.manual_seed(57) @@ -589,21 +613,21 @@ def test_gradient_batch_invariance(cuda_op): q_full = q.clone().requires_grad_(True) k_full = k.clone().requires_grad_(True) v_full = v.clone().requires_grad_(True) - out_full = cuda_op.forward(q_full, k_full, v_full, causal=True) + out_full = attention_op.forward(q_full, k_full, v_full, causal=True) out_full.backward(grad_out) for i in range(B): qi = q[i : i + 1].clone().requires_grad_(True) ki = k[i : i + 1].clone().requires_grad_(True) vi = v[i : i + 1].clone().requires_grad_(True) - out_i = cuda_op.forward(qi, ki, vi, causal=True) + out_i = attention_op.forward(qi, ki, vi, causal=True) out_i.backward(grad_out[i : i + 1]) assert torch.equal(q_full.grad[i : i + 1], qi.grad), f"dQ batch invariance failed i={i}" assert torch.equal(k_full.grad[i : i + 1], ki.grad), f"dK batch invariance failed i={i}" assert torch.equal(v_full.grad[i : i + 1], vi.grad), f"dV batch invariance failed i={i}" -def test_gqa_dk_dv_order(cuda_op): +def test_gqa_dk_dv_order(attention_op): """§7.6/§4.1: GQA dK/dV must follow fixed (hq_local, query_index) order. Two checks: @@ -623,13 +647,13 @@ def test_gqa_dk_dv_order(cuda_op): q1 = q_data.clone().requires_grad_(True) k1 = k_data.clone().requires_grad_(True) v1 = v_data.clone().requires_grad_(True) - cuda_op.forward(q1, k1, v1, causal=True).backward(grad_data) + attention_op.forward(q1, k1, v1, causal=True).backward(grad_data) q_batch = q_data.expand(4, -1, -1, -1).contiguous().clone().requires_grad_(True) k_batch = k_data.expand(4, -1, -1, -1).contiguous().clone().requires_grad_(True) v_batch = v_data.expand(4, -1, -1, -1).contiguous().clone().requires_grad_(True) grad_batch = grad_data.expand(4, -1, -1, -1).contiguous() - cuda_op.forward(q_batch, k_batch, v_batch, causal=True).backward(grad_batch) + attention_op.forward(q_batch, k_batch, v_batch, causal=True).backward(grad_batch) assert torch.equal(k1.grad, k_batch.grad[0:1]), "dK GQA order depends on batch size" assert torch.equal(v1.grad, v_batch.grad[0:1]), "dV GQA order depends on batch size" @@ -655,3 +679,119 @@ def test_gqa_dk_dv_order(cuda_op): torch.testing.assert_close(k1.grad.float(), dk_gold, atol=5e-2, rtol=2e-2) torch.testing.assert_close(v1.grad.float(), dv_gold, atol=5e-2, rtol=2e-2) + + +# ============================================================================= +# Strict core acceptance and ROCm HIP RoPE acceptance +# ============================================================================= + + +def _strict_qkv(*, batch: int = 2, sequence: int = 7): + generator = torch.Generator(device="cpu").manual_seed(942) + q = torch.randn(batch, 4, sequence, D, dtype=torch.bfloat16, generator=generator).to(DEVICE) + k = torch.randn(batch, 1, sequence, D, dtype=torch.bfloat16, generator=generator).to(DEVICE) + v = torch.randn(batch, 1, sequence, D, dtype=torch.bfloat16, generator=generator).to(DEVICE) + return q, k, v + + +def test_strict_attention_core_is_repeat_bitwise_and_no_fallback(): + q, k, v = _strict_qkv() + positions = torch.arange(q.size(2), device=q.device).expand(q.size(0), -1) + core = RLKernelDeterministicAttentionCore() + first = core.forward_with_lse( + q, + k, + v, + query_position_ids=positions, + key_position_ids=positions, + ) + second = core.forward_with_lse( + q, + k, + v, + query_position_ids=positions, + key_position_ids=positions, + ) + assert torch.equal(first.out, second.out) + assert torch.equal(first.lse, second.lse) + assert first.provenance["attention_backend"] == f"rlkernel.{BACKEND}.deterministic_attention" + assert first.provenance["fallback"] is False + assert first.provenance["split_kv"]["actual_split_kv_policy"] == "disabled" + + +def test_strict_attention_forward_backward_train_rollout_bitwise(): + q, k, v = (tensor.requires_grad_() for tensor in _strict_qkv(batch=1, sequence=5)) + positions = torch.arange(q.size(2), device=q.device).expand(q.size(0), -1) + core = RLKernelDeterministicAttentionCore() + train = core.forward_with_lse( + q, + k, + v, + query_position_ids=positions, + key_position_ids=positions, + ) + grad = torch.randn(train.out.shape, dtype=train.out.dtype, device="cpu").to(DEVICE) + (train.out.float() * grad.float()).sum().backward() + train_grads = tuple(tensor.grad.detach().clone() for tensor in (q, k, v)) + + q2, k2, v2 = (tensor.detach().clone().requires_grad_() for tensor in (q, k, v)) + rollout = core.forward_with_lse( + q2, + k2, + v2, + query_position_ids=positions, + key_position_ids=positions, + ) + (rollout.out.float() * grad.float()).sum().backward() + assert torch.equal(train.out, rollout.out) + assert torch.equal(train.lse, rollout.lse) + assert all( + torch.equal(expected, actual.grad) + for expected, actual in zip(train_grads, (q2, k2, v2), strict=True) + ) + + +@ROCM_ONLY +def test_rocm_rope_is_batch_invariant_and_backward_repeat_bitwise(): + generator = torch.Generator(device="cpu").manual_seed(714) + x = torch.randn(3, 4, 6, D, dtype=torch.bfloat16, generator=generator).to(DEVICE) + positions = torch.arange(6, device=x.device).expand(3, -1) + rope = RocmDeterministicRoPEOp() + together = rope(x, positions) + separate = torch.cat([rope(x[index : index + 1], positions[index]) for index in range(3)]) + assert torch.equal(together, separate) + assert torch.equal(together, rope(x, positions)) + + grad = torch.randn(together.shape, dtype=together.dtype, device="cpu").to(DEVICE) + x1 = x.detach().clone().requires_grad_() + x2 = x.detach().clone().requires_grad_() + (rope(x1, positions).float() * grad.float()).sum().backward() + (rope(x2, positions).float() * grad.float()).sum().backward() + assert torch.equal(x1.grad, x2.grad) + + +@ROCM_ONLY +def test_rocm_rope_matches_fp32_rotate_half_reference(): + x = torch.randn(1, 2, 4, D, dtype=torch.bfloat16, device=DEVICE) + positions = torch.arange(4, device=x.device).expand(1, -1) + actual = RocmDeterministicRoPEOp()(x, positions) + half = x.size(-1) // 2 + inv_freq = 1.0 / ( + 1_000_000.0 ** (torch.arange(half, dtype=torch.float32, device=x.device) / half) + ) + frequency = positions.float().unsqueeze(-1) * inv_freq + cos = frequency.cos().unsqueeze(1) + sin = frequency.sin().unsqueeze(1) + reference = torch.cat( + ( + x[..., :half].float() * cos - x[..., half:].float() * sin, + x[..., half:].float() * cos + x[..., :half].float() * sin, + ), + dim=-1, + ).to(x.dtype) + torch.testing.assert_close(actual, reference, atol=0, rtol=0) + + +def test_strict_core_rejects_split_k(): + with pytest.raises(ValueError, match="Split-KV"): + RLKernelDeterministicAttentionCore(split_kv=SplitKVSpec.fixed(32)) diff --git a/tests/test_deterministic_attention_rocm.py b/tests/test_deterministic_attention_rocm.py deleted file mode 100644 index a2f95121..00000000 --- a/tests/test_deterministic_attention_rocm.py +++ /dev/null @@ -1,126 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 -# Copyright (c) 2026 RL-Kernel Contributors -"""ROCm strict Attention acceptance tests that never use a native fallback.""" - -import pytest -import torch - -pytestmark = pytest.mark.skipif( - torch.version.hip is None or not torch.cuda.is_available(), - reason="ROCm GPU is unavailable", -) - -from rl_engine.kernels.attention_contract import SplitKVSpec # noqa: E402 -from rl_engine.kernels.ops.rocm.attention.deterministic_attn import ( # noqa: E402 - RLKernelDeterministicAttentionCore, -) -from rl_engine.kernels.ops.rocm.rotary_embedding.rope import RocmDeterministicRoPEOp # noqa: E402 - - -def _qkv(*, batch: int = 2, sequence: int = 7): - generator = torch.Generator(device="cpu").manual_seed(942) - q = torch.randn(batch, 4, sequence, 128, dtype=torch.bfloat16, generator=generator).cuda() - k = torch.randn(batch, 1, sequence, 128, dtype=torch.bfloat16, generator=generator).cuda() - v = torch.randn(batch, 1, sequence, 128, dtype=torch.bfloat16, generator=generator).cuda() - return q, k, v - - -def test_rocm_attention_core_is_repeat_bitwise_and_no_fallback(): - q, k, v = _qkv() - positions = torch.arange(q.size(2), device=q.device).expand(q.size(0), -1) - core = RLKernelDeterministicAttentionCore() - first = core.forward_with_lse( - q, - k, - v, - query_position_ids=positions, - key_position_ids=positions, - ) - second = core.forward_with_lse( - q, - k, - v, - query_position_ids=positions, - key_position_ids=positions, - ) - assert torch.equal(first.out, second.out) - assert torch.equal(first.lse, second.lse) - assert first.provenance["attention_backend"] == "rlkernel.rocm.deterministic_attention" - assert first.provenance["fallback"] is False - assert first.provenance["split_kv"]["actual_split_kv_policy"] == "disabled" - - -def test_rocm_attention_forward_backward_train_rollout_bitwise(): - q, k, v = (tensor.requires_grad_() for tensor in _qkv(batch=1, sequence=5)) - positions = torch.arange(q.size(2), device=q.device).expand(q.size(0), -1) - core = RLKernelDeterministicAttentionCore() - train = core.forward_with_lse( - q, - k, - v, - query_position_ids=positions, - key_position_ids=positions, - ) - grad = torch.randn(train.out.shape, dtype=train.out.dtype, device="cpu").cuda() - (train.out.float() * grad.float()).sum().backward() - train_grads = tuple(tensor.grad.detach().clone() for tensor in (q, k, v)) - - q2, k2, v2 = (tensor.detach().clone().requires_grad_() for tensor in (q, k, v)) - rollout = core.forward_with_lse( - q2, - k2, - v2, - query_position_ids=positions, - key_position_ids=positions, - ) - (rollout.out.float() * grad.float()).sum().backward() - assert torch.equal(train.out, rollout.out) - assert torch.equal(train.lse, rollout.lse) - assert all( - torch.equal(expected, actual.grad) - for expected, actual in zip(train_grads, (q2, k2, v2), strict=True) - ) - - -def test_rocm_rope_is_batch_invariant_and_backward_repeat_bitwise(): - generator = torch.Generator(device="cpu").manual_seed(714) - x = torch.randn(3, 4, 6, 128, dtype=torch.bfloat16, generator=generator).cuda() - positions = torch.arange(6, device=x.device).expand(3, -1) - rope = RocmDeterministicRoPEOp() - together = rope(x, positions) - separate = torch.cat([rope(x[index : index + 1], positions[index]) for index in range(3)]) - assert torch.equal(together, separate) - assert torch.equal(together, rope(x, positions)) - - grad = torch.randn(together.shape, dtype=together.dtype, device="cpu").cuda() - x1 = x.detach().clone().requires_grad_() - x2 = x.detach().clone().requires_grad_() - (rope(x1, positions).float() * grad.float()).sum().backward() - (rope(x2, positions).float() * grad.float()).sum().backward() - assert torch.equal(x1.grad, x2.grad) - - -def test_rocm_rope_matches_fp32_rotate_half_reference(): - x = torch.randn(1, 2, 4, 128, dtype=torch.bfloat16, device="cuda") - positions = torch.arange(4, device=x.device).expand(1, -1) - actual = RocmDeterministicRoPEOp()(x, positions) - half = x.size(-1) // 2 - inv_freq = 1.0 / ( - 1_000_000.0 ** (torch.arange(half, dtype=torch.float32, device=x.device) / half) - ) - frequency = positions.float().unsqueeze(-1) * inv_freq - cos = frequency.cos().unsqueeze(1) - sin = frequency.sin().unsqueeze(1) - reference = torch.cat( - ( - x[..., :half].float() * cos - x[..., half:].float() * sin, - x[..., half:].float() * cos + x[..., :half].float() * sin, - ), - dim=-1, - ).to(x.dtype) - torch.testing.assert_close(actual, reference, atol=0, rtol=0) - - -def test_rocm_strict_core_rejects_split_k(): - with pytest.raises(ValueError, match="Split-KV"): - RLKernelDeterministicAttentionCore(split_kv=SplitKVSpec.fixed(32)) From 4c744b3222dbe33efe35ed6d1bfe47b486303848 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 19 Aug 2026 15:06:23 +0000 Subject: [PATCH 05/35] refactor(attention): consolidate ROCm implementation --- .../cuda/attention/deterministic_attention.cu | 87 +++++++++++++++ csrc/rocm/deterministic_rope.cu | 91 ---------------- docs/operators/attention.md | 2 +- .../attention/flashinfer_paged_attention.py | 9 +- .../kernels/ops/cuda/rotary_embedding/rope.py | 43 +++++++- .../kernels/ops/rocm/attention/__init__.py | 3 - .../ops/rocm/attention/deterministic_attn.py | 15 --- .../ops/rocm/rotary_embedding/__init__.py | 6 -- .../kernels/ops/rocm/rotary_embedding/rope.py | 101 ------------------ scripts/__init__.py | 4 - scripts/rocm_attention_acceptance.py | 86 --------------- .../ws2_p2p_nccl_attention_reference_check.py | 87 ++++++++++++++- setup.py | 2 - ...y => test_deterministic_attention_cuda.py} | 17 +-- tests/test_flashinfer_pr7_attention.py | 20 +++- 15 files changed, 238 insertions(+), 335 deletions(-) delete mode 100644 csrc/rocm/deterministic_rope.cu delete mode 100644 rl_engine/kernels/ops/rocm/attention/deterministic_attn.py delete mode 100644 rl_engine/kernels/ops/rocm/rotary_embedding/__init__.py delete mode 100644 rl_engine/kernels/ops/rocm/rotary_embedding/rope.py delete mode 100644 scripts/__init__.py delete mode 100644 scripts/rocm_attention_acceptance.py rename tests/{test_deterministic_attention.py => test_deterministic_attention_cuda.py} (98%) diff --git a/csrc/cuda/attention/deterministic_attention.cu b/csrc/cuda/attention/deterministic_attention.cu index aaa70b42..2c56d6c7 100644 --- a/csrc/cuda/attention/deterministic_attention.cu +++ b/csrc/cuda/attention/deterministic_attention.cu @@ -685,3 +685,90 @@ std::vector deterministic_attention_backward( return {dQ, dK, dV}; } + +#if defined(USE_ROCM) +namespace { + +template +__global__ void deterministic_rope_kernel( + const scalar_t* __restrict__ x, + const float* __restrict__ cos, + const float* __restrict__ sin, + scalar_t* __restrict__ out, + int64_t n_rows, + int table_rows, + int half, + float sin_sign) { + const int64_t index = blockIdx.x * static_cast(blockDim.x) + threadIdx.x; + const int64_t count = n_rows * static_cast(half); + if (index >= count) { + return; + } + + const int64_t row = index / half; + const int pair = static_cast(index % half); + const int table_row = static_cast(row % table_rows); + const float c = cos[table_row * half + pair]; + const float s = sin[table_row * half + pair] * sin_sign; + const int64_t base = row * (2LL * half); + const float low = static_cast(x[base + pair]); + const float high = static_cast(x[base + pair + half]); + + out[base + pair] = static_cast(low * c - high * s); + out[base + pair + half] = static_cast(high * c + low * s); +} + +} // namespace + +torch::Tensor deterministic_rope_apply_rocm( + torch::Tensor x, + torch::Tensor cos, + torch::Tensor sin, + double sin_sign) { + TORCH_CHECK(x.is_cuda(), "ROCm RoPE: x must be a GPU tensor"); + TORCH_CHECK(x.dim() == 2 && x.is_contiguous(), + "ROCm RoPE: x must be contiguous [rows, head_dim]"); + TORCH_CHECK(cos.is_cuda() && sin.is_cuda(), + "ROCm RoPE: cos and sin must be GPU tensors"); + TORCH_CHECK(cos.scalar_type() == torch::kFloat32 && + sin.scalar_type() == torch::kFloat32, + "ROCm RoPE: cos and sin must be FP32"); + TORCH_CHECK(cos.is_contiguous() && sin.is_contiguous(), + "ROCm RoPE: cos and sin must be contiguous"); + TORCH_CHECK(cos.dim() == 2 && sin.sizes() == cos.sizes(), + "ROCm RoPE: cos and sin must have shape [table_rows, head_dim/2]"); + TORCH_CHECK(x.size(1) % 2 == 0, "ROCm RoPE: head_dim must be even"); + TORCH_CHECK(cos.size(0) > 0 && cos.size(1) == x.size(1) / 2, + "ROCm RoPE: invalid cos/sin table shape"); + TORCH_CHECK(x.size(0) % cos.size(0) == 0, + "ROCm RoPE: row count must be divisible by the position table size"); + + const at::cuda::OptionalCUDAGuard guard(device_of(x)); + auto out = torch::empty_like(x); + const int64_t n_rows = x.size(0); + const int half = static_cast(x.size(1) / 2); + const int table_rows = static_cast(cos.size(0)); + const int64_t count = n_rows * static_cast(half); + constexpr int threads = 256; + const int64_t blocks = (count + threads - 1) / threads; + auto stream = at::cuda::getCurrentCUDAStream(); + + AT_DISPATCH_FLOATING_TYPES_AND2( + at::ScalarType::Half, + at::ScalarType::BFloat16, + x.scalar_type(), + "deterministic_rope_apply_rocm", + [&] { + deterministic_rope_kernel<<>>( + x.data_ptr(), + cos.data_ptr(), + sin.data_ptr(), + out.data_ptr(), + n_rows, + table_rows, + half, + static_cast(sin_sign)); + }); + return out; +} +#endif diff --git a/csrc/rocm/deterministic_rope.cu b/csrc/rocm/deterministic_rope.cu deleted file mode 100644 index 91f1f0ff..00000000 --- a/csrc/rocm/deterministic_rope.cu +++ /dev/null @@ -1,91 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (c) 2026 RL-Kernel Contributors - -#include -#include -#include - -namespace { - -template -__global__ void deterministic_rope_kernel( - const scalar_t* __restrict__ x, - const float* __restrict__ cos, - const float* __restrict__ sin, - scalar_t* __restrict__ out, - int64_t n_rows, - int table_rows, - int half, - float sin_sign) { - const int64_t index = blockIdx.x * static_cast(blockDim.x) + threadIdx.x; - const int64_t count = n_rows * static_cast(half); - if (index >= count) { - return; - } - - const int64_t row = index / half; - const int pair = static_cast(index % half); - const int table_row = static_cast(row % table_rows); - const float c = cos[table_row * half + pair]; - const float s = sin[table_row * half + pair] * sin_sign; - const int64_t base = row * (2LL * half); - const float low = static_cast(x[base + pair]); - const float high = static_cast(x[base + pair + half]); - - out[base + pair] = static_cast(low * c - high * s); - out[base + pair + half] = static_cast(high * c + low * s); -} - -} // namespace - -torch::Tensor deterministic_rope_apply_rocm( - torch::Tensor x, - torch::Tensor cos, - torch::Tensor sin, - double sin_sign) { - TORCH_CHECK(x.is_cuda(), "ROCm RoPE: x must be a GPU tensor"); - TORCH_CHECK(x.dim() == 2 && x.is_contiguous(), - "ROCm RoPE: x must be contiguous [rows, head_dim]"); - TORCH_CHECK(cos.is_cuda() && sin.is_cuda(), - "ROCm RoPE: cos and sin must be GPU tensors"); - TORCH_CHECK(cos.scalar_type() == torch::kFloat32 && - sin.scalar_type() == torch::kFloat32, - "ROCm RoPE: cos and sin must be FP32"); - TORCH_CHECK(cos.is_contiguous() && sin.is_contiguous(), - "ROCm RoPE: cos and sin must be contiguous"); - TORCH_CHECK(cos.dim() == 2 && sin.sizes() == cos.sizes(), - "ROCm RoPE: cos and sin must have shape [table_rows, head_dim/2]"); - TORCH_CHECK(x.size(1) % 2 == 0, "ROCm RoPE: head_dim must be even"); - TORCH_CHECK(cos.size(0) > 0 && cos.size(1) == x.size(1) / 2, - "ROCm RoPE: invalid cos/sin table shape"); - TORCH_CHECK(x.size(0) % cos.size(0) == 0, - "ROCm RoPE: row count must be divisible by the position table size"); - - const at::cuda::OptionalCUDAGuard guard(device_of(x)); - auto out = torch::empty_like(x); - const int64_t n_rows = x.size(0); - const int half = static_cast(x.size(1) / 2); - const int table_rows = static_cast(cos.size(0)); - const int64_t count = n_rows * static_cast(half); - constexpr int threads = 256; - const int64_t blocks = (count + threads - 1) / threads; - auto stream = at::cuda::getCurrentCUDAStream(); - - AT_DISPATCH_FLOATING_TYPES_AND2( - at::ScalarType::Half, - at::ScalarType::BFloat16, - x.scalar_type(), - "deterministic_rope_apply_rocm", - [&] { - deterministic_rope_kernel<<>>( - x.data_ptr(), - cos.data_ptr(), - sin.data_ptr(), - out.data_ptr(), - n_rows, - table_rows, - half, - static_cast(sin_sign)); - }); - return out; -} diff --git a/docs/operators/attention.md b/docs/operators/attention.md index 66682ced..ebff9a58 100644 --- a/docs/operators/attention.md +++ b/docs/operators/attention.md @@ -153,7 +153,7 @@ GPU-only LARGE Qwen3-8B real-shape smoke test. - `csrc/cuda/attention/deterministic_attention.cu` — CUDA kernels - `rl_engine/kernels/registry.py` - `tests/test_attention.py` -- `tests/test_deterministic_attention.py` +- `tests/test_deterministic_attention_cuda.py` ## Fixed Reduction Order (CUDA Deterministic Backend) diff --git a/rl_engine/kernels/ops/cuda/attention/flashinfer_paged_attention.py b/rl_engine/kernels/ops/cuda/attention/flashinfer_paged_attention.py index c242d119..cf6cda11 100644 --- a/rl_engine/kernels/ops/cuda/attention/flashinfer_paged_attention.py +++ b/rl_engine/kernels/ops/cuda/attention/flashinfer_paged_attention.py @@ -1515,12 +1515,13 @@ def _resolve_strict_rope(cfg: FlashInferPagedAttentionConfig) -> Any: if cfg.strict_rope_op is not None: return cfg.strict_rope_op try: - if torch.version.hip is not None: - from rl_engine.kernels.ops.rocm.rotary_embedding.rope import RocmDeterministicRoPEOp + from rl_engine.kernels.ops.cuda.rotary_embedding.rope import ( + RocmDeterministicRoPEOp, + RoPESM90Op, + ) + if torch.version.hip is not None: return RocmDeterministicRoPEOp() - from rl_engine.kernels.ops.cuda.rotary_embedding.rope import RoPESM90Op - return RoPESM90Op() except (ImportError, RuntimeError) as exc: raise FlashInferUnavailable( diff --git a/rl_engine/kernels/ops/cuda/rotary_embedding/rope.py b/rl_engine/kernels/ops/cuda/rotary_embedding/rope.py index 9a764012..928c3033 100644 --- a/rl_engine/kernels/ops/cuda/rotary_embedding/rope.py +++ b/rl_engine/kernels/ops/cuda/rotary_embedding/rope.py @@ -1,6 +1,6 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright (c) 2026 RL-Kernel Contributors -"""Custom CUDA RoPE op for SM90 (GPT-NeoX rotate-half), matching NativeRoPEOp. +"""Deterministic GPU RoPE ops (GPT-NeoX rotate-half), matching NativeRoPEOp. cos/sin are built in fp32 with the exact reference math and passed to a small CUDA kernel (``_C.rope_apply_sm90``) that does the per-position rotation. Backward @@ -77,7 +77,7 @@ def forward(ctx, x: Tensor, positions: Tensor, theta: float) -> Tensor: ctx.save_for_backward(cos, sin) ctx.x_shape = tuple(x.shape) ctx.pos_dim = positions.dim() - out_2d = _C.rope_apply_sm90(x_2d, cos, sin, 1.0) + out_2d = _rope_apply(x_2d, cos, sin, 1.0) return _restore_rope(out_2d, x, positions) @staticmethod @@ -87,7 +87,7 @@ def backward(ctx, grad_out: Tensor): if ctx.needs_input_grad[0]: if ctx.pos_dim == 2 and len(ctx.x_shape) == 4: g_2d = grad_out.permute(1, 0, 2, 3).contiguous().reshape(-1, ctx.x_shape[-1]) - out_2d = _C.rope_apply_sm90(g_2d, cos, sin, -1.0) + out_2d = _rope_apply(g_2d, cos, sin, -1.0) heads, batch, seq, dim = ( ctx.x_shape[1], ctx.x_shape[0], @@ -97,10 +97,16 @@ def backward(ctx, grad_out: Tensor): grad_x = out_2d.reshape(heads, batch, seq, dim).permute(1, 0, 2, 3).contiguous() else: g_2d = grad_out.contiguous().reshape(-1, grad_out.shape[-1]) - grad_x = _C.rope_apply_sm90(g_2d, cos, sin, -1.0).reshape(grad_out.shape) + grad_x = _rope_apply(g_2d, cos, sin, -1.0).reshape(grad_out.shape) return grad_x, None, None +def _rope_apply(x: Tensor, cos: Tensor, sin: Tensor, sin_sign: float) -> Tensor: + if torch.version.hip is not None: + return _C.deterministic_rope_apply_rocm(x, cos, sin, sin_sign) + return _C.rope_apply_sm90(x, cos, sin, sin_sign) + + def _is_hopper(device: torch.device) -> bool: try: return torch.cuda.get_device_capability(device)[0] == 9 @@ -138,3 +144,32 @@ def forward(self, x: Tensor, positions: Tensor, *, theta: float = 1_000_000.0) - f"got compute capability {torch.cuda.get_device_capability(x.device)}" ) return _RoPEFunction.apply(x, positions, theta) + + +class RocmDeterministicRoPEOp: + """Precompiled HIP RoPE path shared by ROCm training and rollout.""" + + backend_id = "rlkernel.rocm.deterministic_rope" + op_class = "elementwise" + fallback = False + + def __init__(self) -> None: + if torch.version.hip is None: + raise RuntimeError("RocmDeterministicRoPEOp requires a ROCm PyTorch build") + if not _EXT_AVAILABLE or not hasattr(_C, "deterministic_rope_apply_rocm"): + raise RuntimeError( + "ROCm deterministic RoPE is unavailable; rebuild rl_engine._C for ROCm" + ) + + def __call__(self, x: Tensor, positions: Tensor, *, theta: float = 1_000_000.0) -> Tensor: + return self.forward(x, positions, theta=theta) + + def forward(self, x: Tensor, positions: Tensor, *, theta: float = 1_000_000.0) -> Tensor: + if not x.is_cuda: + raise RuntimeError("ROCm deterministic RoPE requires a GPU tensor") + if x.dtype not in (torch.float16, torch.bfloat16): + raise ValueError("ROCm deterministic RoPE requires FP16 or BF16") + return _RoPEFunction.apply(x, positions, theta) + + +__all__ = ["RoPESM90Op", "RocmDeterministicRoPEOp"] diff --git a/rl_engine/kernels/ops/rocm/attention/__init__.py b/rl_engine/kernels/ops/rocm/attention/__init__.py index b03448f5..150c937f 100644 --- a/rl_engine/kernels/ops/rocm/attention/__init__.py +++ b/rl_engine/kernels/ops/rocm/attention/__init__.py @@ -1,11 +1,8 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright (c) 2026 RL-Kernel Contributors -from .deterministic_attn import DeterministicAttentionOp, RLKernelDeterministicAttentionCore from .flash_attn import RocmFlashAttentionOp __all__ = [ "RocmFlashAttentionOp", - "DeterministicAttentionOp", - "RLKernelDeterministicAttentionCore", ] diff --git a/rl_engine/kernels/ops/rocm/attention/deterministic_attn.py b/rl_engine/kernels/ops/rocm/attention/deterministic_attn.py deleted file mode 100644 index 435a5272..00000000 --- a/rl_engine/kernels/ops/rocm/attention/deterministic_attn.py +++ /dev/null @@ -1,15 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 -# Copyright (c) 2026 RL-Kernel Contributors -"""ROCm public surface for the shared deterministic Attention core.""" - -from rl_engine.kernels.ops.cuda.attention.deterministic_attn import ( - DeterministicAttentionCoreResult, - DeterministicAttentionOp, - RLKernelDeterministicAttentionCore, -) - -__all__ = [ - "DeterministicAttentionCoreResult", - "DeterministicAttentionOp", - "RLKernelDeterministicAttentionCore", -] diff --git a/rl_engine/kernels/ops/rocm/rotary_embedding/__init__.py b/rl_engine/kernels/ops/rocm/rotary_embedding/__init__.py deleted file mode 100644 index 0a7285f8..00000000 --- a/rl_engine/kernels/ops/rocm/rotary_embedding/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 -# Copyright (c) 2026 RL-Kernel Contributors - -from .rope import RocmDeterministicRoPEOp - -__all__ = ["RocmDeterministicRoPEOp"] diff --git a/rl_engine/kernels/ops/rocm/rotary_embedding/rope.py b/rl_engine/kernels/ops/rocm/rotary_embedding/rope.py deleted file mode 100644 index 7b0f385e..00000000 --- a/rl_engine/kernels/ops/rocm/rotary_embedding/rope.py +++ /dev/null @@ -1,101 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 -# Copyright (c) 2026 RL-Kernel Contributors -"""Deterministic GPT-NeoX RoPE for ROCm training and inference.""" - -from __future__ import annotations - -import torch -from torch import Tensor - -from rl_engine.kernels.ops.base import _C, _EXT_AVAILABLE - - -def _build_cos_sin( - positions: Tensor, - half: int, - theta: float, - device: torch.device, -) -> tuple[Tensor, Tensor]: - inv_freq = 1.0 / (theta ** (torch.arange(0, half, dtype=torch.float32, device=device) / half)) - frequency = positions.to(device=device, dtype=torch.float32).reshape(-1, 1) * inv_freq - return frequency.cos().contiguous(), frequency.sin().contiguous() - - -def _prepare(x: Tensor, positions: Tensor, theta: float) -> tuple[Tensor, Tensor, Tensor]: - dim = x.size(-1) - if dim % 2: - raise ValueError(f"RoPE head_dim must be even, got {dim}") - if positions.ndim == 1: - table_rows = positions.numel() - x_2d = x.contiguous().reshape(-1, dim) - elif positions.ndim == 2: - batch, sequence = positions.shape - if x.ndim not in (3, 4) or x.size(0) != batch or x.size(-2) != sequence: - raise ValueError("[B,S] positions require x [B,S,D] or [B,H,S,D]") - x_2d = ( - x.permute(1, 0, 2, 3).contiguous().reshape(-1, dim) - if x.ndim == 4 - else x.contiguous().reshape(-1, dim) - ) - table_rows = batch * sequence - else: - raise ValueError("positions must have shape [S] or [B,S]") - if table_rows < 1 or x_2d.size(0) % table_rows: - raise ValueError("RoPE rows must be divisible by the position table size") - cos, sin = _build_cos_sin(positions, dim // 2, float(theta), x.device) - return x_2d, cos, sin - - -def _restore(out: Tensor, shape: torch.Size, positions_dim: int) -> Tensor: - if positions_dim == 2 and len(shape) == 4: - batch, heads, sequence, dim = shape - return out.reshape(heads, batch, sequence, dim).permute(1, 0, 2, 3).contiguous() - return out.reshape(shape) - - -class _RocmRoPEFunction(torch.autograd.Function): - @staticmethod - def forward(ctx, x: Tensor, positions: Tensor, theta: float) -> Tensor: - x_2d, cos, sin = _prepare(x, positions, theta) - ctx.save_for_backward(cos, sin) - ctx.shape = x.shape - ctx.positions_dim = positions.ndim - out = _C.deterministic_rope_apply_rocm(x_2d, cos, sin, 1.0) - return _restore(out, x.shape, positions.ndim) - - @staticmethod - def backward(ctx, grad_out: Tensor): - cos, sin = ctx.saved_tensors - shape = ctx.shape - if ctx.positions_dim == 2 and len(shape) == 4: - grad_2d = grad_out.permute(1, 0, 2, 3).contiguous().reshape(-1, shape[-1]) - else: - grad_2d = grad_out.contiguous().reshape(-1, shape[-1]) - grad_x = _C.deterministic_rope_apply_rocm(grad_2d, cos, sin, -1.0) - return _restore(grad_x, shape, ctx.positions_dim), None, None - - -class RocmDeterministicRoPEOp: - """One precompiled HIP RoPE arithmetic path shared by train and rollout.""" - - backend_id = "rlkernel.rocm.deterministic_rope" - op_class = "elementwise" - fallback = False - - def __init__(self) -> None: - if torch.version.hip is None: - raise RuntimeError("RocmDeterministicRoPEOp requires a ROCm PyTorch build") - if not _EXT_AVAILABLE or not hasattr(_C, "deterministic_rope_apply_rocm"): - raise RuntimeError( - "ROCm deterministic RoPE is unavailable; rebuild rl_engine._C for ROCm" - ) - - def __call__(self, x: Tensor, positions: Tensor, *, theta: float = 1_000_000.0) -> Tensor: - if not x.is_cuda: - raise RuntimeError("ROCm deterministic RoPE requires a GPU tensor") - if x.dtype not in (torch.float16, torch.bfloat16): - raise ValueError("ROCm deterministic RoPE requires FP16 or BF16") - return _RocmRoPEFunction.apply(x, positions, float(theta)) - - -__all__ = ["RocmDeterministicRoPEOp"] diff --git a/scripts/__init__.py b/scripts/__init__.py deleted file mode 100644 index 5295edc3..00000000 --- a/scripts/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 -# Copyright (c) 2026 RL-Kernel Contributors - -"""Repository-local validation scripts.""" diff --git a/scripts/rocm_attention_acceptance.py b/scripts/rocm_attention_acceptance.py deleted file mode 100644 index 1423ab2b..00000000 --- a/scripts/rocm_attention_acceptance.py +++ /dev/null @@ -1,86 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 -# Copyright (c) 2026 RL-Kernel Contributors -"""Run the ROCm strict Attention acceptance matrix and retain evidence.""" - -from __future__ import annotations - -import argparse -import json -import subprocess -import sys -from pathlib import Path - -import torch - - -def main() -> int: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--output-dir", type=Path, default=Path("results/rocm-attention")) - args = parser.parse_args() - if torch.version.hip is None or torch.cuda.device_count() < 8: - raise RuntimeError("the formal acceptance matrix requires 8 visible ROCm GPUs") - - repo = Path(__file__).resolve().parents[1] - output = ( - (repo / args.output_dir).resolve() if not args.output_dir.is_absolute() else args.output_dir - ) - output.mkdir(parents=True, exist_ok=True) - steps: list[dict[str, object]] = [] - - commands = [ - ( - "single_gpu", - [sys.executable, "-m", "pytest", "-q", "tests/test_deterministic_attention.py"], - ) - ] - for transport, strict in (("p2p_nccl_reference", False), ("rccl_ag_rs", True)): - for ranks in (2, 4, 8): - name = f"{transport}_{ranks}r" - report = output / f"{name}.json" - command = [ - "torchrun", - "--standalone", - f"--nproc-per-node={ranks}", - "scripts/ws2_p2p_nccl_attention_reference_check.py", - "--transport", - transport, - "--output", - str(report), - ] - if strict: - command.append("--strict-shared-core") - commands.append((name, command)) - - for name, command in commands: - completed = subprocess.run( - command, - cwd=repo, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - check=False, - ) - (output / f"{name}.log").write_text(completed.stdout, encoding="utf-8") - steps.append({"name": name, "command": command, "returncode": completed.returncode}) - - summary = { - "schema_version": "ws2_rocm_attention_acceptance/v1", - "platform": "rocm", - "torch": str(torch.__version__), - "hip": str(torch.version.hip), - "collective": list(torch.cuda.nccl.version()), - "device_count": torch.cuda.device_count(), - "device_name": torch.cuda.get_device_name(0), - "steps": steps, - "passed": all(step["returncode"] == 0 for step in steps), - } - (output / "summary.json").write_text( - json.dumps(summary, indent=2, sort_keys=True) + "\n", - encoding="utf-8", - ) - print(json.dumps(summary, indent=2, sort_keys=True)) - return 0 if summary["passed"] else 1 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/ws2_p2p_nccl_attention_reference_check.py b/scripts/ws2_p2p_nccl_attention_reference_check.py index 1f12b0ca..40bef37b 100644 --- a/scripts/ws2_p2p_nccl_attention_reference_check.py +++ b/scripts/ws2_p2p_nccl_attention_reference_check.py @@ -15,6 +15,7 @@ import json import math import os +import subprocess import sys from pathlib import Path from types import SimpleNamespace @@ -80,14 +81,94 @@ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: help="run AG(Q/K/V/positions) -> shared CUDA core -> RS(Out/LSE) with backward", ) parser.add_argument("--output", type=Path) + parser.add_argument( + "--run-rocm-matrix", + action="store_true", + help="run the complete 1/2/4/8-GPU ROCm acceptance matrix", + ) + parser.add_argument( + "--output-dir", + type=Path, + default=Path("results/rocm-attention"), + help="directory for --run-rocm-matrix logs and JSON reports", + ) args = parser.parse_args(argv) if args.strict_shared_core and args.transport not in {"cuda_ag_rs", "rccl_ag_rs"}: parser.error("--strict-shared-core requires a self-owned AG/RS transport") + if args.run_rocm_matrix and (args.strict_shared_core or args.output is not None): + parser.error("--run-rocm-matrix cannot be combined with single-run output options") return args +def _run_rocm_matrix(output_dir: Path) -> int: + if torch.version.hip is None or torch.cuda.device_count() < 8: + raise RuntimeError("the formal acceptance matrix requires 8 visible ROCm GPUs") + + repo = Path(__file__).resolve().parents[1] + output = (repo / output_dir).resolve() if not output_dir.is_absolute() else output_dir + output.mkdir(parents=True, exist_ok=True) + script = Path(__file__).resolve() + commands: list[tuple[str, list[str]]] = [ + ( + "single_gpu", + [sys.executable, "-m", "pytest", "-q", "tests/test_deterministic_attention_cuda.py"], + ) + ] + for transport, strict in (("p2p_nccl_reference", False), ("rccl_ag_rs", True)): + for ranks in (2, 4, 8): + name = f"{transport}_{ranks}r" + command = [ + sys.executable, + "-m", + "torch.distributed.run", + "--standalone", + f"--nproc-per-node={ranks}", + str(script), + "--transport", + transport, + "--output", + str(output / f"{name}.json"), + ] + if strict: + command.append("--strict-shared-core") + commands.append((name, command)) + + steps: list[dict[str, object]] = [] + for name, command in commands: + completed = subprocess.run( + command, + cwd=repo, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + check=False, + ) + (output / f"{name}.log").write_text(completed.stdout, encoding="utf-8") + steps.append({"name": name, "command": command, "returncode": completed.returncode}) + + summary = { + "schema_version": "ws2_rocm_attention_acceptance/v1", + "platform": "rocm", + "torch": str(torch.__version__), + "hip": str(torch.version.hip), + "collective": list(torch.cuda.nccl.version()), + "device_count": torch.cuda.device_count(), + "device_name": torch.cuda.get_device_name(0), + "steps": steps, + "passed": all(step["returncode"] == 0 for step in steps), + } + (output / "summary.json").write_text( + json.dumps(summary, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + print(json.dumps(summary, indent=2, sort_keys=True)) + return 0 if summary["passed"] else 1 + + def main(argv: Sequence[str] | None = None) -> int: args = parse_args(argv) + if args.run_rocm_matrix: + return _run_rocm_matrix(args.output_dir) if not torch.cuda.is_available() or torch.cuda.device_count() < 2: raise RuntimeError("this check requires at least two visible CUDA/ROCm devices") dist.init_process_group("nccl", init_method="env://") @@ -485,12 +566,10 @@ def _run_strict_shared_core_check( def _strict_rope_op(): - if torch.version.hip is not None: - from rl_engine.kernels.ops.rocm.rotary_embedding.rope import RocmDeterministicRoPEOp + from rl_engine.kernels.ops.cuda.rotary_embedding.rope import RocmDeterministicRoPEOp, RoPESM90Op + if torch.version.hip is not None: return RocmDeterministicRoPEOp() - from rl_engine.kernels.ops.cuda.rotary_embedding.rope import RoPESM90Op - return RoPESM90Op() diff --git a/setup.py b/setup.py index 297d269d..57b3ebd9 100644 --- a/setup.py +++ b/setup.py @@ -140,8 +140,6 @@ def get_extensions(): "csrc/cuda/activation.cu", "csrc/cuda/attention/deterministic_attention.cu", ] - if is_rocm: - cuda_sources.append("csrc/rocm/deterministic_rope.cu") if not is_rocm: # This source contains NVIDIA PTX (cp.async, ldmatrix, and mma.sync). # The ROCm dispatcher falls back to PyTorch SDPA for this operator. diff --git a/tests/test_deterministic_attention.py b/tests/test_deterministic_attention_cuda.py similarity index 98% rename from tests/test_deterministic_attention.py rename to tests/test_deterministic_attention_cuda.py index d616ff4a..f981e840 100644 --- a/tests/test_deterministic_attention.py +++ b/tests/test_deterministic_attention_cuda.py @@ -36,25 +36,18 @@ try: from rl_engine.kernels.attention_contract import SplitKVSpec from rl_engine.kernels.gtest.op_checks import CandidateSpec, OperatorCase, run_operator_suite + from rl_engine.kernels.ops.cuda.attention.deterministic_attn import ( + DeterministicAttentionOp, + RLKernelDeterministicAttentionCore, + ) from rl_engine.kernels.ops.pytorch.attention.standard_attn import NativeAttentionOp - if IS_ROCM: - from rl_engine.kernels.ops.rocm.attention.deterministic_attn import ( - DeterministicAttentionOp, - RLKernelDeterministicAttentionCore, - ) - else: - from rl_engine.kernels.ops.cuda.attention.deterministic_attn import ( - DeterministicAttentionOp, - RLKernelDeterministicAttentionCore, - ) - _OP_AVAILABLE = True except (ImportError, RuntimeError): _OP_AVAILABLE = False if IS_ROCM: - from rl_engine.kernels.ops.rocm.rotary_embedding.rope import RocmDeterministicRoPEOp + from rl_engine.kernels.ops.cuda.rotary_embedding.rope import RocmDeterministicRoPEOp pytestmark = [ pytestmark, diff --git a/tests/test_flashinfer_pr7_attention.py b/tests/test_flashinfer_pr7_attention.py index b3ed329e..ebd012fe 100644 --- a/tests/test_flashinfer_pr7_attention.py +++ b/tests/test_flashinfer_pr7_attention.py @@ -3,9 +3,12 @@ from __future__ import annotations +import importlib.util import json +import sys import types from dataclasses import replace +from pathlib import Path import pytest import torch @@ -39,8 +42,21 @@ materialize_flashinfer_paged_kv_cache, ) from rl_engine.testing.attention_comparison import DecodeKVCacheMetadata -from scripts import ws2_p2p_nccl_attention_reference_check as p2p_check_script -from scripts import ws2_pr7_flashinfer_attention_check as check_script + + +def _load_repo_script(name: str): + path = Path(__file__).resolve().parents[1] / "scripts" / f"{name}.py" + spec = importlib.util.spec_from_file_location(f"rl_kernel_{name}", path) + if spec is None or spec.loader is None: + raise ImportError(f"cannot load repository script {path}") + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + +p2p_check_script = _load_repo_script("ws2_p2p_nccl_attention_reference_check") +check_script = _load_repo_script("ws2_pr7_flashinfer_attention_check") class _FakeFlashInferWrapper: From 22d3780730f8040c469fd4ba9effebfc52f7b152 Mon Sep 17 00:00:00 2001 From: Codex H100 Validation Date: Wed, 19 Aug 2026 17:24:30 +0000 Subject: [PATCH 06/35] feat(attention): default strict runtime to decoupled ring schedule --- .../attention/flashinfer_paged_attention.py | 9 ++ .../ops/pytorch/attention/cp_attention.py | 123 ++++++++++++++++++ tests/test_cp_attention.py | 12 ++ tests/test_flashinfer_pr7_attention.py | 5 + 4 files changed, 149 insertions(+) diff --git a/rl_engine/kernels/ops/cuda/attention/flashinfer_paged_attention.py b/rl_engine/kernels/ops/cuda/attention/flashinfer_paged_attention.py index cf6cda11..fcd4f778 100644 --- a/rl_engine/kernels/ops/cuda/attention/flashinfer_paged_attention.py +++ b/rl_engine/kernels/ops/cuda/attention/flashinfer_paged_attention.py @@ -49,6 +49,7 @@ ) from rl_engine.kernels.ops.pytorch.attention.cp_attention import ( AttentionPartialState, + AttentionRingSchedule, DeterministicCPAttentionReferenceOp, build_reference_split_kv_runtime_plan_set, merge_attention_partial_states, @@ -805,6 +806,11 @@ def _run_strict_cp( if expected_kv_range is None: raise FlashInferUnavailable("strict CP Attention requires an expected KV range") expected_k_tokens = expected_kv_range[1] - expected_kv_range[0] + ring_schedule = AttentionRingSchedule.build( + expected_k_tokens, + cp_world_size=plan.parallel.cp_world_size, + kv_chunk_size=None, + ) if global_q.size(2) != expected_q_tokens: raise FlashInferUnavailable("strict AG(Q) returned the wrong global width") if global_k.size(2) != expected_k_tokens or global_v.shape != global_k.shape: @@ -855,6 +861,9 @@ def _run_strict_cp( "strict_comm_autograd": bool(getattr(communication, "supports_autograd", False)), "strict_local_query_range": [query_start, query_end], "strict_local_kv_range": [key_start, key_end], + **ring_schedule.provenance(), + "ring_schedule_default": True, + "ring_partial_arithmetic": False, } ) return FlashInferAttentionResult( diff --git a/rl_engine/kernels/ops/pytorch/attention/cp_attention.py b/rl_engine/kernels/ops/pytorch/attention/cp_attention.py index 2dabb840..70d6e2e3 100644 --- a/rl_engine/kernels/ops/pytorch/attention/cp_attention.py +++ b/rl_engine/kernels/ops/pytorch/attention/cp_attention.py @@ -56,6 +56,104 @@ def __post_init__(self) -> None: raise ValueError("block_end must be >= block_start") +@dataclass(frozen=True) +class AttentionRingBlock: + """One logical KV block in the decoupled Ring Attention schedule.""" + + global_block_index: int + block_start: int + block_end: int + owner_cp_rank: int + + +@dataclass(frozen=True) +class AttentionRingSchedule: + """Static compute order separated from the fixed arithmetic merge order.""" + + schedule_id: str + total_kv_tokens: int + cp_world_size: int + kv_chunk_size: Optional[int] + blocks: tuple[AttentionRingBlock, ...] + compute_order: tuple[int, ...] + merge_order: tuple[int, ...] + compute_communication: str = "decoupled" + overlap: str = "disabled" + + @classmethod + def build( + cls, + total_kv_tokens: int, + *, + cp_world_size: int = 1, + kv_chunk_size: Optional[int] = None, + ) -> "AttentionRingSchedule": + if ( + isinstance(cp_world_size, bool) + or not isinstance(cp_world_size, int) + or cp_world_size < 1 + ): + raise ValueError("cp_world_size must be >= 1") + if kv_chunk_size is not None and ( + isinstance(kv_chunk_size, bool) + or not isinstance(kv_chunk_size, int) + or kv_chunk_size < 1 + ): + raise ValueError("kv_chunk_size must be >= 1 when provided") + if ( + isinstance(total_kv_tokens, bool) + or not isinstance(total_kv_tokens, int) + or total_kv_tokens < 1 + ): + raise ValueError("total_kv_tokens must be an integer >= 1") + if total_kv_tokens < cp_world_size: + raise ValueError("Ring Attention requires at least one KV token per CP rank") + blocks: list[AttentionRingBlock] = [] + for owner_cp_rank, (owner_start, owner_end) in enumerate( + _split_bounds(total_kv_tokens, cp_world_size) + ): + cursor = owner_start + while cursor < owner_end: + block_end = ( + owner_end if kv_chunk_size is None else min(cursor + kv_chunk_size, owner_end) + ) + blocks.append( + AttentionRingBlock( + global_block_index=len(blocks), + block_start=cursor, + block_end=block_end, + owner_cp_rank=owner_cp_rank, + ) + ) + cursor = block_end + compute_order: list[int] = [] + left, right = 0, len(blocks) - 1 + while left <= right: + compute_order.append(left) + if left != right: + compute_order.append(right) + left += 1 + right -= 1 + return cls( + schedule_id="rlkernel.attention.strict_ring_state.v1", + total_kv_tokens=total_kv_tokens, + cp_world_size=cp_world_size, + kv_chunk_size=kv_chunk_size, + blocks=tuple(blocks), + compute_order=tuple(compute_order), + merge_order=tuple(range(len(blocks))), + ) + + def provenance(self) -> dict[str, object]: + return { + "compute_communication": self.compute_communication, + "compute_schedule": self.schedule_id, + "compute_order": list(self.compute_order), + "merge_order_indices": list(self.merge_order), + "communication_overlap": self.overlap, + } + + @dataclass(frozen=True) class AttentionBackwardGradients: """Training-side gradients emitted by the CP attention backward reference.""" @@ -237,6 +335,21 @@ def split_kv_execution_plans( backend="deterministic_cp_reference", ) + @staticmethod + def ring_schedule( + total_kv_tokens: int, + *, + cp_world_size: int = 1, + kv_chunk_size: Optional[int] = None, + ) -> AttentionRingSchedule: + """Build the production-default pre-overlap Ring schedule.""" + + return AttentionRingSchedule.build( + total_kv_tokens, + cp_world_size=cp_world_size, + kv_chunk_size=kv_chunk_size, + ) + def __call__( self, q: torch.Tensor, @@ -551,6 +664,11 @@ def backward_reference( torch.autograd.backward(out, dout.to(dtype=out.dtype)) if q_leaf.grad is None or k_leaf.grad is None or v_leaf.grad is None: raise RuntimeError("CP attention backward did not produce dq/dk/dv") + ring_schedule = self.ring_schedule( + k.size(2), + cp_world_size=cp_world_size, + kv_chunk_size=kv_chunk_size, + ) return AttentionBackwardPathResult( name=name @@ -589,6 +707,9 @@ def backward_reference( "merge_order": "global_block_index", "accum_dtype": "fp32", "downcast_at": "final_write", + **ring_schedule.provenance(), + "ring_schedule_default": True, + "ring_partial_arithmetic": False, "output_dtype": str(resolved_output_dtype).replace("torch.", ""), "q_dtype": str(q.dtype).replace("torch.", ""), "k_dtype": str(k.dtype).replace("torch.", ""), @@ -1182,6 +1303,8 @@ def build_reference_split_kv_runtime_plan_set( "AttentionBackwardPathResult", "AttentionBackwardRankDrift", "AttentionPartialState", + "AttentionRingBlock", + "AttentionRingSchedule", "build_reference_split_kv_runtime_plan_set", "CPAttentionReferenceOp", "DeterministicCPAttentionReferenceOp", diff --git a/tests/test_cp_attention.py b/tests/test_cp_attention.py index 73953090..2ce2d7ce 100644 --- a/tests/test_cp_attention.py +++ b/tests/test_cp_attention.py @@ -24,6 +24,7 @@ ) from rl_engine.kernels.ops.pytorch.attention.cp_attention import ( AttentionPartialState, + AttentionRingSchedule, DeterministicCPAttentionReferenceOp, compare_cp_attention_backward, merge_attention_partial_states, @@ -40,6 +41,17 @@ _GRAD_ATOL = 1.0e-5 +def test_ring_schedule_separates_compute_and_merge_order(): + schedule = AttentionRingSchedule.build(12, cp_world_size=2, kv_chunk_size=2) + + assert schedule.schedule_id == "rlkernel.attention.strict_ring_state.v1" + assert schedule.compute_communication == "decoupled" + assert schedule.overlap == "disabled" + assert schedule.merge_order == tuple(range(6)) + assert schedule.compute_order == (0, 5, 1, 4, 2, 3) + assert [block.owner_cp_rank for block in schedule.blocks] == [0, 0, 0, 1, 1, 1] + + @contextlib.contextmanager def _single_thread(): prev = torch.get_num_threads() diff --git a/tests/test_flashinfer_pr7_attention.py b/tests/test_flashinfer_pr7_attention.py index ebd012fe..592b4256 100644 --- a/tests/test_flashinfer_pr7_attention.py +++ b/tests/test_flashinfer_pr7_attention.py @@ -670,6 +670,11 @@ def test_strict_cp_path_gathers_qkv_and_real_position_ids_before_shared_core(): assert result.provenance["materialization"] == ("ag_qkv_positions_shared_core_rs") assert result.provenance["strict_full_qkv_all_gather"] is True assert result.provenance["strict_position_ids_all_gather"] is True + assert result.provenance["compute_communication"] == "decoupled" + assert result.provenance["compute_schedule"] == ("rlkernel.attention.strict_ring_state.v1") + assert result.provenance["communication_overlap"] == "disabled" + assert result.provenance["ring_schedule_default"] is True + assert result.provenance["ring_partial_arithmetic"] is False assert result.provenance["fallback"] is False From aac0fb08755cc23461da456e6ca25b015dbd96b6 Mon Sep 17 00:00:00 2001 From: lamentropetion <3051000145@qq.com> Date: Thu, 20 Aug 2026 20:04:14 +0800 Subject: [PATCH 07/35] fix(attention): gate ROCm strict provenance --- rl_engine/kernels/attention_contract.py | 4 + .../ops/pytorch/attention/cp_attention.py | 3 +- .../ws2_p2p_nccl_attention_reference_check.py | 95 ++++++++++++++++--- tests/test_flashinfer_pr7_attention.py | 67 +++++++++++++ 4 files changed, 157 insertions(+), 12 deletions(-) diff --git a/rl_engine/kernels/attention_contract.py b/rl_engine/kernels/attention_contract.py index 1b5abfbc..d686131a 100644 --- a/rl_engine/kernels/attention_contract.py +++ b/rl_engine/kernels/attention_contract.py @@ -22,6 +22,9 @@ # rollout. Backend adapters may differ, but strict runtime evidence must not. STRICT_ATTENTION_CORE_ID = "rlkernel.attention.deterministic_core.v1" STRICT_ATTENTION_SCHEDULE_ID = "single_batch_single_query_global_kv_blocks" +# The distributed strict path executes the full-KV core above. This identifies +# the fixed, pre-overlap communication schedule around that core. +STRICT_ATTENTION_RING_SCHEDULE_ID = "rlkernel.attention.strict_ring_state.v1" class AttentionContractError(ValueError): @@ -1454,6 +1457,7 @@ class AttentionDispatchResult: "SplitKVRuntimePlanSet", "SplitKVSpec", "STRICT_ATTENTION_CORE_ID", + "STRICT_ATTENTION_RING_SCHEDULE_ID", "STRICT_ATTENTION_SCHEDULE_ID", "validate_split_kv_alignment", "validate_split_kv_plan_set_alignment", diff --git a/rl_engine/kernels/ops/pytorch/attention/cp_attention.py b/rl_engine/kernels/ops/pytorch/attention/cp_attention.py index 70d6e2e3..0729a464 100644 --- a/rl_engine/kernels/ops/pytorch/attention/cp_attention.py +++ b/rl_engine/kernels/ops/pytorch/attention/cp_attention.py @@ -17,6 +17,7 @@ import torch from rl_engine.kernels.attention_contract import ( + STRICT_ATTENTION_RING_SCHEDULE_ID, SplitKVExecutionPlan, SplitKVMode, SplitKVRuntimeCoordinate, @@ -135,7 +136,7 @@ def build( left += 1 right -= 1 return cls( - schedule_id="rlkernel.attention.strict_ring_state.v1", + schedule_id=STRICT_ATTENTION_RING_SCHEDULE_ID, total_kv_tokens=total_kv_tokens, cp_world_size=cp_world_size, kv_chunk_size=kv_chunk_size, diff --git a/scripts/ws2_p2p_nccl_attention_reference_check.py b/scripts/ws2_p2p_nccl_attention_reference_check.py index 40bef37b..18248164 100644 --- a/scripts/ws2_p2p_nccl_attention_reference_check.py +++ b/scripts/ws2_p2p_nccl_attention_reference_check.py @@ -30,6 +30,7 @@ from rl_engine.kernels.attention_contract import ( # noqa: E402 STRICT_ATTENTION_CORE_ID, + STRICT_ATTENTION_RING_SCHEDULE_ID, STRICT_ATTENTION_SCHEDULE_ID, ) from rl_engine.kernels.ops.cuda.attention.cp_comm import ( # noqa: E402 @@ -112,7 +113,19 @@ def _run_rocm_matrix(output_dir: Path) -> int: ( "single_gpu", [sys.executable, "-m", "pytest", "-q", "tests/test_deterministic_attention_cuda.py"], - ) + ), + ( + "adapter_cp_contracts", + [ + sys.executable, + "-m", + "pytest", + "-q", + "tests/test_flashinfer_pr7_attention.py", + "tests/test_cp_attention.py", + "tests/test_attention_comparison.py", + ], + ), ] for transport, strict in (("p2p_nccl_reference", False), ("rccl_ag_rs", True)): for ranks in (2, 4, 8): @@ -148,6 +161,7 @@ def _run_rocm_matrix(output_dir: Path) -> int: summary = { "schema_version": "ws2_rocm_attention_acceptance/v1", + "git_commit": _current_git_commit(repo), "platform": "rocm", "torch": str(torch.__version__), "hip": str(torch.version.hip), @@ -165,6 +179,19 @@ def _run_rocm_matrix(output_dir: Path) -> int: return 0 if summary["passed"] else 1 +def _current_git_commit(repo: Path) -> str: + """Bind an acceptance artifact to the checkout that generated it.""" + + try: + return subprocess.check_output( + ["git", "-C", str(repo), "rev-parse", "HEAD"], + text=True, + stderr=subprocess.STDOUT, + ).strip() + except (OSError, subprocess.CalledProcessError) as exc: + raise RuntimeError("ROCm Attention acceptance requires a Git checkout") from exc + + def main(argv: Sequence[str] | None = None) -> int: args = parse_args(argv) if args.run_rocm_matrix: @@ -213,6 +240,7 @@ def main(argv: Sequence[str] | None = None) -> int: if global_rank == 0: report = { "schema_version": f"ws2_{args.transport}_attention/v2", + "git_commit": _current_git_commit(REPO_ROOT), "backend": str(dist.get_backend()), "platform": "rocm" if torch.version.hip is not None else "cuda", "torch_version": str(torch.__version__), @@ -533,20 +561,18 @@ def _run_strict_shared_core_check( repeat_lse_bitwise = repeat_lse_bitwise and torch.equal(repeated.lse, distributed.lse) provenance = distributed.provenance - identity_valid = ( - provenance.get("strict_core_id") == STRICT_ATTENTION_CORE_ID - and provenance.get("strict_schedule") == STRICT_ATTENTION_SCHEDULE_ID - and provenance.get("strict_mode") is True - and provenance.get("native_attention_arithmetic") is False - and provenance.get("fallback") is False - and provenance.get("strict_split_kv") == "disabled" - and provenance.get("strict_comm_autograd") is True - and provenance.get("communication_backend") == args.transport + identity_errors = _strict_shared_core_identity_errors( + provenance, + transport=args.transport, + is_rocm=torch.version.hip is not None, ) return { "executed": True, "passed": ( - all(bitwise.values()) and repeat_out_bitwise and repeat_lse_bitwise and identity_valid + all(bitwise.values()) + and repeat_out_bitwise + and repeat_lse_bitwise + and not identity_errors ), "strict_core_id": provenance.get("strict_core_id"), "strict_schedule": provenance.get("strict_schedule"), @@ -558,6 +584,8 @@ def _run_strict_shared_core_check( "fallback": provenance.get("fallback"), "split_kv_policy": provenance.get("strict_split_kv"), "communication_autograd": provenance.get("strict_comm_autograd"), + "strict_provenance": provenance, + "identity_errors": identity_errors, "bitwise": bitwise, "max_abs": max_abs, "repeat_out_bitwise": repeat_out_bitwise, @@ -565,6 +593,51 @@ def _run_strict_shared_core_check( } +def _strict_shared_core_identity_errors( + provenance: dict[str, object], + *, + transport: str, + is_rocm: bool, +) -> list[str]: + """Return every strict-contract provenance mismatch for the rank report.""" + + expected_backend = ( + "rlkernel.rocm.deterministic_attention" + if is_rocm + else "rlkernel.cuda.deterministic_attention" + ) + expected_rope = "rlkernel.rocm.deterministic_rope" if is_rocm else "rlkernel.cuda.rope_sm90" + required = { + "strict_core_id": STRICT_ATTENTION_CORE_ID, + "strict_schedule": STRICT_ATTENTION_SCHEDULE_ID, + "attention_backend": expected_backend, + "actual_backend": expected_backend, + "rope_backend": expected_rope, + "strict_mode": True, + "native_attention_arithmetic": False, + "fallback": False, + "strict_split_kv": "disabled", + "strict_comm_autograd": True, + "communication_backend": transport, + "production_ready": True, + "strict_full_qkv_all_gather": True, + "strict_position_ids_all_gather": True, + "compute_communication": "decoupled", + "compute_schedule": STRICT_ATTENTION_RING_SCHEDULE_ID, + "communication_overlap": "disabled", + "ring_schedule_default": True, + "ring_partial_arithmetic": False, + "rope_fusion": False, + "q_rope_state": "post_rope", + "k_cache_rope_state": "post_rope", + } + return [ + f"{name}={provenance.get(name)!r}, expected {expected!r}" + for name, expected in required.items() + if provenance.get(name) != expected + ] + + def _strict_rope_op(): from rl_engine.kernels.ops.cuda.rotary_embedding.rope import RocmDeterministicRoPEOp, RoPESM90Op diff --git a/tests/test_flashinfer_pr7_attention.py b/tests/test_flashinfer_pr7_attention.py index 592b4256..7d43bd9b 100644 --- a/tests/test_flashinfer_pr7_attention.py +++ b/tests/test_flashinfer_pr7_attention.py @@ -15,6 +15,7 @@ from rl_engine.kernels.attention_contract import ( STRICT_ATTENTION_CORE_ID, + STRICT_ATTENTION_RING_SCHEDULE_ID, STRICT_ATTENTION_SCHEDULE_ID, SplitKVSpec, ) @@ -1813,6 +1814,72 @@ def test_strict_shared_core_entrypoint_requires_self_owned_ag_rs(): assert args.strict_shared_core is True +def _strict_acceptance_provenance(**overrides): + provenance = { + "strict_core_id": STRICT_ATTENTION_CORE_ID, + "strict_schedule": STRICT_ATTENTION_SCHEDULE_ID, + "attention_backend": "rlkernel.cuda.deterministic_attention", + "actual_backend": "rlkernel.cuda.deterministic_attention", + "rope_backend": "rlkernel.cuda.rope_sm90", + "strict_mode": True, + "native_attention_arithmetic": False, + "fallback": False, + "strict_split_kv": "disabled", + "strict_comm_autograd": True, + "communication_backend": "cuda_ag_rs", + "production_ready": True, + "strict_full_qkv_all_gather": True, + "strict_position_ids_all_gather": True, + "compute_communication": "decoupled", + "compute_schedule": STRICT_ATTENTION_RING_SCHEDULE_ID, + "communication_overlap": "disabled", + "ring_schedule_default": True, + "ring_partial_arithmetic": False, + "rope_fusion": False, + "q_rope_state": "post_rope", + "k_cache_rope_state": "post_rope", + } + provenance.update(overrides) + return provenance + + +@pytest.mark.parametrize( + ("field", "invalid"), + [ + ("compute_schedule", "dynamic_ring"), + ("communication_overlap", "enabled"), + ("ring_schedule_default", False), + ("ring_partial_arithmetic", True), + ("actual_backend", "flashinfer"), + ("rope_backend", "native_rope"), + ("strict_comm_autograd", False), + ], +) +def test_strict_shared_core_acceptance_rejects_provenance_drift(field, invalid): + errors = p2p_check_script._strict_shared_core_identity_errors( + _strict_acceptance_provenance(**{field: invalid}), + transport="cuda_ag_rs", + is_rocm=False, + ) + + assert any(error.startswith(f"{field}=") for error in errors) + + +def test_strict_shared_core_acceptance_requires_rocm_backend_and_rope(): + provenance = _strict_acceptance_provenance( + attention_backend="rlkernel.rocm.deterministic_attention", + actual_backend="rlkernel.rocm.deterministic_attention", + rope_backend="rlkernel.rocm.deterministic_rope", + communication_backend="rccl_ag_rs", + ) + + assert not p2p_check_script._strict_shared_core_identity_errors( + provenance, + transport="rccl_ag_rs", + is_rocm=True, + ) + + @pytest.mark.parametrize( ("argv", "message"), [ From d146d432ba3dfe280d90a9f4a5f41eb8a481b9a4 Mon Sep 17 00:00:00 2001 From: lamentropetion <3051000145@qq.com> Date: Thu, 20 Aug 2026 21:27:58 +0800 Subject: [PATCH 08/35] feat(attention): use strict vendor cores by platform --- rl_engine/kernels/attention_contract.py | 16 +- .../kernels/ops/cuda/attention/__init__.py | 4 +- .../ops/cuda/attention/deterministic_attn.py | 11 +- .../kernels/ops/cuda/attention/flash_attn.py | 224 +++++++++++++++ .../attention/flashinfer_paged_attention.py | 116 +++++--- .../kernels/ops/rocm/attention/__init__.py | 8 +- .../kernels/ops/rocm/attention/flash_attn.py | 265 ++++++++++++++++++ .../ws2_p2p_nccl_attention_reference_check.py | 55 +++- scripts/ws2_pr7_flashinfer_attention_check.py | 72 +++-- tests/test_attention_correctness.py | 149 ++++++++++ tests/test_flashinfer_pr7_attention.py | 42 ++- 11 files changed, 878 insertions(+), 84 deletions(-) diff --git a/rl_engine/kernels/attention_contract.py b/rl_engine/kernels/attention_contract.py index d686131a..549857c1 100644 --- a/rl_engine/kernels/attention_contract.py +++ b/rl_engine/kernels/attention_contract.py @@ -18,9 +18,14 @@ _EnumT = TypeVar("_EnumT", bound=Enum) -# Stable identity for the CUDA Attention arithmetic shared by training and -# rollout. Backend adapters may differ, but strict runtime evidence must not. -STRICT_ATTENTION_CORE_ID = "rlkernel.attention.deterministic_core.v1" +# Stable identities for Attention arithmetic shared by training and rollout. +STRICT_ATTENTION_PRODUCTION_CORE_ID = "rlkernel.attention.flash_attention4.num_splits1.v1" +STRICT_ATTENTION_ROCM_PRODUCTION_CORE_ID = "rlkernel.attention.rocm.aiter_ck_dense_mha.v1" +STRICT_ATTENTION_REFERENCE_CORE_ID = "rlkernel.attention.deterministic_core.v1" +# Compatibility alias for callers that explicitly select the original core. +STRICT_ATTENTION_CORE_ID = STRICT_ATTENTION_REFERENCE_CORE_ID +STRICT_ATTENTION_FA4_SCHEDULE_ID = "single_batch_flash_attention4_num_splits1" +STRICT_ATTENTION_ROCM_SCHEDULE_ID = "single_batch_aiter_ck_dense_mha_no_splitkv" STRICT_ATTENTION_SCHEDULE_ID = "single_batch_single_query_global_kv_blocks" # The distributed strict path executes the full-KV core above. This identifies # the fixed, pre-overlap communication schedule around that core. @@ -1457,7 +1462,12 @@ class AttentionDispatchResult: "SplitKVRuntimePlanSet", "SplitKVSpec", "STRICT_ATTENTION_CORE_ID", + "STRICT_ATTENTION_FA4_SCHEDULE_ID", + "STRICT_ATTENTION_PRODUCTION_CORE_ID", + "STRICT_ATTENTION_REFERENCE_CORE_ID", "STRICT_ATTENTION_RING_SCHEDULE_ID", + "STRICT_ATTENTION_ROCM_PRODUCTION_CORE_ID", + "STRICT_ATTENTION_ROCM_SCHEDULE_ID", "STRICT_ATTENTION_SCHEDULE_ID", "validate_split_kv_alignment", "validate_split_kv_plan_set_alignment", diff --git a/rl_engine/kernels/ops/cuda/attention/__init__.py b/rl_engine/kernels/ops/cuda/attention/__init__.py index 21850cdf..814424c7 100644 --- a/rl_engine/kernels/ops/cuda/attention/__init__.py +++ b/rl_engine/kernels/ops/cuda/attention/__init__.py @@ -21,7 +21,7 @@ DeterministicAttentionOp, RLKernelDeterministicAttentionCore, ) -from .flash_attn import FlashAttentionOp +from .flash_attn import FlashAttentionOp, StrictFlashAttention4Core, StrictFlashAttentionUnavailable from .flashinfer_paged_attention import ( FlashInferPagedAttentionConfig, FlashInferQwen3PagedAttentionOp, @@ -49,6 +49,8 @@ "DeterministicAttentionOp", "RLKernelDeterministicAttentionCore", "FlashAttentionOp", + "StrictFlashAttention4Core", + "StrictFlashAttentionUnavailable", "FlashInferPagedAttentionConfig", "FlashInferQwen3PagedAttentionOp", "FlashInferRoPEFusionConfig", diff --git a/rl_engine/kernels/ops/cuda/attention/deterministic_attn.py b/rl_engine/kernels/ops/cuda/attention/deterministic_attn.py index 98823816..ba75a26a 100644 --- a/rl_engine/kernels/ops/cuda/attention/deterministic_attn.py +++ b/rl_engine/kernels/ops/cuda/attention/deterministic_attn.py @@ -220,11 +220,10 @@ def _validate_inputs( class RLKernelDeterministicAttentionCore: - """Production GPU Attention core shared by training and rollout. + """Materializing GPU reference core shared by training and rollout. - Strict execution exposes only the existing WS1 no-Split-K kernel. This is - intentional: a different Split-K schedule changes the arithmetic graph and - therefore cannot share this core identity. + Production uses FA4 CuTe on CUDA and AITER CK dense MHA on ROCm. This core + remains useful for correctness and capability-gap diagnosis. """ core_id = STRICT_ATTENTION_CORE_ID @@ -239,6 +238,8 @@ class RLKernelDeterministicAttentionCore: downcast_at = "final_write" fallback = False native_attention_arithmetic = False + production_ready = False + reference_only = True def __init__( self, @@ -306,6 +307,8 @@ def forward_with_lse( "fallback": self.fallback, "fallback_reason": None, "native_attention_arithmetic": self.native_attention_arithmetic, + "production_ready": self.production_ready, + "reference_only": self.reference_only, "strict_schedule": self.strict_schedule, }, ) diff --git a/rl_engine/kernels/ops/cuda/attention/flash_attn.py b/rl_engine/kernels/ops/cuda/attention/flash_attn.py index 0a068af6..24afdfc5 100644 --- a/rl_engine/kernels/ops/cuda/attention/flash_attn.py +++ b/rl_engine/kernels/ops/cuda/attention/flash_attn.py @@ -1,11 +1,235 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright (c) 2026 RL-Kernel Contributors +from __future__ import annotations + +import importlib +import importlib.metadata +import inspect +from typing import Any, Callable + import torch +from rl_engine.kernels.attention_contract import ( + STRICT_ATTENTION_FA4_SCHEDULE_ID, + STRICT_ATTENTION_PRODUCTION_CORE_ID, + SplitKVMode, + SplitKVSpec, +) from rl_engine.kernels.ops.base import _C, _EXT_AVAILABLE +from rl_engine.kernels.ops.cuda.attention.deterministic_attn import ( + DeterministicAttentionCoreResult, + RLKernelDeterministicAttentionCore, +) from rl_engine.utils.logger import logger +_FA4_API_SOURCE = "flash_attn.cute.interface" +_FA4_REQUIRED_PARAMETERS = frozenset( + {"softmax_scale", "causal", "num_splits", "pack_gqa", "deterministic", "return_lse"} +) + + +class StrictFlashAttentionUnavailable(RuntimeError): + """Raised when the exact FlashAttention production contract is unavailable.""" + + +def _load_fa4_cute_op() -> tuple[Callable[..., Any], str]: + try: + module = importlib.import_module(_FA4_API_SOURCE) + op = getattr(module, "flash_attn_func") + except (AttributeError, ImportError, OSError, RuntimeError) as exc: + raise StrictFlashAttentionUnavailable( + "strict CUDA Attention requires flash_attn.cute.interface.flash_attn_func" + ) from exc + try: + package_version = importlib.metadata.version("flash-attn") + except importlib.metadata.PackageNotFoundError: + package_version = "unknown" + return op, package_version + + +class StrictFlashAttention4Core: + """Shared CUDA production core with the reduction-affecting FA4 knobs fixed.""" + + core_id = STRICT_ATTENTION_PRODUCTION_CORE_ID + strict_schedule = STRICT_ATTENTION_FA4_SCHEDULE_ID + backend_id = "flash_attention_4.cute" + api_source = _FA4_API_SOURCE + merge_order = "global_block_index" + accum_dtype = "fp32" + downcast_at = "final_write" + fallback = False + native_attention_arithmetic = True + production_ready = True + reference_only = False + num_splits = 1 + deterministic_backward = True + + def __init__( + self, + *, + split_kv: SplitKVSpec | None = None, + _op: Callable[..., Any] | None = None, + _package_version: str | None = None, + ) -> None: + requested = SplitKVSpec.disabled() if split_kv is None else split_kv + if not isinstance(requested, SplitKVSpec): + raise TypeError("split_kv must be a SplitKVSpec") + if requested.mode is not SplitKVMode.DISABLED: + raise ValueError("strict FA4 Attention requires Split-KV to be disabled") + if _op is None: + op, package_version = _load_fa4_cute_op() + else: + op = _op + package_version = "test-double" if _package_version is None else _package_version + self._validate_api(op) + self.split_kv = requested + self.package_version = package_version + self._op = op + + @staticmethod + def _validate_api(op: Callable[..., Any]) -> None: + try: + parameters = inspect.signature(op).parameters + except (TypeError, ValueError) as exc: + raise StrictFlashAttentionUnavailable( + "cannot inspect the FlashAttention CuTe API signature" + ) from exc + missing = sorted(_FA4_REQUIRED_PARAMETERS.difference(parameters)) + if missing: + raise StrictFlashAttentionUnavailable( + "FlashAttention CuTe API is missing strict controls: " + ", ".join(missing) + ) + + def __call__( + self, + q: torch.Tensor, + k: torch.Tensor, + v: torch.Tensor, + **kwargs: Any, + ) -> DeterministicAttentionCoreResult: + return self.forward_with_lse(q, k, v, **kwargs) + + def forward_with_lse( + self, + q: torch.Tensor, + k: torch.Tensor, + v: torch.Tensor, + *, + causal: bool = True, + scale: float | None = None, + key_padding_mask: torch.Tensor | None = None, + query_position_ids: torch.Tensor | None = None, + key_position_ids: torch.Tensor | None = None, + output_dtype: torch.dtype | None = None, + ) -> DeterministicAttentionCoreResult: + self._validate_inputs(q, k, v, key_padding_mask) + RLKernelDeterministicAttentionCore._validate_positions( + q, + k, + causal=causal, + query_position_ids=query_position_ids, + key_position_ids=key_position_ids, + ) + resolved_dtype = q.dtype if output_dtype is None else output_dtype + if resolved_dtype != q.dtype: + raise ValueError("strict Attention output_dtype must match the Q/K/V input dtype") + + q_fa = q.transpose(1, 2).contiguous() + k_fa = k.transpose(1, 2).contiguous() + v_fa = v.transpose(1, 2).contiguous() + result = self._op( + q_fa, + k_fa, + v_fa, + softmax_scale=scale, + causal=causal, + num_splits=self.num_splits, + pack_gqa=q.size(1) > k.size(1), + deterministic=self.deterministic_backward, + return_lse=True, + ) + if not isinstance(result, tuple) or len(result) != 2: + raise StrictFlashAttentionUnavailable( + "FlashAttention CuTe must return exactly (out, lse) when return_lse=True" + ) + out_fa, lse = result + if not isinstance(out_fa, torch.Tensor) or not isinstance(lse, torch.Tensor): + raise StrictFlashAttentionUnavailable("FlashAttention CuTe returned non-tensor output") + expected_out_shape = q_fa.shape + expected_lse_shape = (q.size(0), q.size(1), q.size(2)) + if tuple(out_fa.shape) != tuple(expected_out_shape): + raise StrictFlashAttentionUnavailable( + f"FlashAttention output must have shape {tuple(expected_out_shape)}" + ) + if tuple(lse.shape) != expected_lse_shape: + raise StrictFlashAttentionUnavailable( + f"FlashAttention LSE must have shape {expected_lse_shape}" + ) + if out_fa.dtype != resolved_dtype: + raise StrictFlashAttentionUnavailable( + "FlashAttention output dtype does not match the requested output dtype" + ) + if lse.dtype != torch.float32: + raise StrictFlashAttentionUnavailable( + "FlashAttention attention-domain LSE must be FP32" + ) + + out = out_fa.transpose(1, 2).contiguous() + return DeterministicAttentionCoreResult( + out=out, + lse=lse.contiguous(), + provenance={ + "strict_core_id": self.core_id, + "strict_schedule": self.strict_schedule, + "attention_backend": self.backend_id, + "fa_api_source": self.api_source, + "fa_package_version": self.package_version, + "num_splits": self.num_splits, + "deterministic_backward": self.deterministic_backward, + "dropout_p": 0.0, + "split_kv": self.split_kv.resolve(k.size(2), backend=self.backend_id).to_dict(), + "merge_order": self.merge_order, + "accum_dtype": self.accum_dtype, + "downcast_at": self.downcast_at, + "fallback": self.fallback, + "fallback_reason": None, + "native_attention_arithmetic": self.native_attention_arithmetic, + "production_ready": self.production_ready, + "reference_only": self.reference_only, + }, + ) + + @staticmethod + def _validate_inputs( + q: torch.Tensor, + k: torch.Tensor, + v: torch.Tensor, + key_padding_mask: torch.Tensor | None, + ) -> None: + if key_padding_mask is not None: + raise ValueError( + "strict FA4 core does not accept padding masks; materialize each logical row" + ) + if q.ndim != 4 or k.ndim != 4 or v.ndim != 4: + raise ValueError("q/k/v must be 4-D [B, H, S, D]") + if q.size(0) != 1: + raise ValueError("strict FA4 core executes one logical batch row at a time") + if k.size(0) != 1 or v.size(0) != 1: + raise ValueError("q/k/v batch sizes must match") + if k.shape != v.shape or q.size(3) != k.size(3): + raise ValueError("k/v shapes and q/k/v head dimensions must match") + if q.size(1) % k.size(1) != 0: + raise ValueError("Q heads must be divisible by KV heads for GQA") + if q.dtype not in (torch.float16, torch.bfloat16): + raise ValueError("strict FA4 core supports FP16/BF16 only") + if k.dtype != q.dtype or v.dtype != q.dtype: + raise ValueError("q/k/v must share one dtype") + if not (q.is_cuda and k.is_cuda and v.is_cuda): + raise ValueError("strict FA4 core requires CUDA tensors") + if not (q.device == k.device == v.device): + raise ValueError("q/k/v must be on one CUDA device") + class FlashAttentionOp: """ diff --git a/rl_engine/kernels/ops/cuda/attention/flashinfer_paged_attention.py b/rl_engine/kernels/ops/cuda/attention/flashinfer_paged_attention.py index fcd4f778..48785e6c 100644 --- a/rl_engine/kernels/ops/cuda/attention/flashinfer_paged_attention.py +++ b/rl_engine/kernels/ops/cuda/attention/flashinfer_paged_attention.py @@ -26,6 +26,10 @@ from rl_engine.kernels.attention_contract import ( STRICT_ATTENTION_CORE_ID, + STRICT_ATTENTION_FA4_SCHEDULE_ID, + STRICT_ATTENTION_PRODUCTION_CORE_ID, + STRICT_ATTENTION_ROCM_PRODUCTION_CORE_ID, + STRICT_ATTENTION_ROCM_SCHEDULE_ID, STRICT_ATTENTION_SCHEDULE_ID, AttentionContractError, SplitKVExecutionPlan, @@ -43,10 +47,8 @@ AttentionCPPartialState, AttentionParallelSpec, ) -from rl_engine.kernels.ops.cuda.attention.deterministic_attn import ( - DeterministicAttentionCoreResult, - RLKernelDeterministicAttentionCore, -) +from rl_engine.kernels.ops.cuda.attention.deterministic_attn import DeterministicAttentionCoreResult +from rl_engine.kernels.ops.cuda.attention.flash_attn import StrictFlashAttention4Core from rl_engine.kernels.ops.pytorch.attention.cp_attention import ( AttentionPartialState, AttentionRingSchedule, @@ -704,7 +706,7 @@ def _run_strict_core( ) -> FlashInferAttentionResult: """Use FlashInfer only for paged-KV layout, never Attention arithmetic.""" - core = cfg.deterministic_core or RLKernelDeterministicAttentionCore(split_kv=cfg.split_kv) + core = _resolve_strict_core(cfg) _validate_strict_core(core) rope = _resolve_strict_rope(cfg) logical_k, logical_v, key_positions = _materialize_strict_logical_kv( @@ -819,7 +821,7 @@ def _run_strict_cp( rope = _resolve_strict_rope(cfg) q_ready = _apply_strict_rope(rope, global_q, global_q_positions, cfg.rope.rope_theta) k_ready = _apply_strict_rope(rope, global_k, global_k_positions, cfg.rope.rope_theta) - core = cfg.deterministic_core or RLKernelDeterministicAttentionCore(split_kv=cfg.split_kv) + core = _resolve_strict_core(cfg) _validate_strict_core(core) outputs: list[torch.Tensor] = [] @@ -1462,28 +1464,68 @@ def _validate_runtime_outputs( def _validate_strict_core(core: Any) -> None: if not callable(getattr(core, "forward_with_lse", None)): - raise ValueError("strict deterministic core must implement forward_with_lse") - if getattr(core, "core_id", None) != STRICT_ATTENTION_CORE_ID: - raise ValueError("strict deterministic core ID must be " f"{STRICT_ATTENTION_CORE_ID!r}") - if getattr(core, "strict_schedule", None) != STRICT_ATTENTION_SCHEDULE_ID: - raise ValueError( - "strict deterministic core schedule must be " f"{STRICT_ATTENTION_SCHEDULE_ID!r}" - ) + raise ValueError("strict Attention core must implement forward_with_lse") + expected_schedules = { + STRICT_ATTENTION_PRODUCTION_CORE_ID: STRICT_ATTENTION_FA4_SCHEDULE_ID, + STRICT_ATTENTION_ROCM_PRODUCTION_CORE_ID: STRICT_ATTENTION_ROCM_SCHEDULE_ID, + STRICT_ATTENTION_CORE_ID: STRICT_ATTENTION_SCHEDULE_ID, + } + core_id = getattr(core, "core_id", None) + if core_id not in expected_schedules: + raise ValueError("strict Attention core ID is not an exact supported identity") + if getattr(core, "strict_schedule", None) != expected_schedules[core_id]: + raise ValueError("strict Attention core schedule does not match its exact core identity") required = { "merge_order": "global_block_index", "accum_dtype": "fp32", "downcast_at": "final_write", "fallback": False, - "native_attention_arithmetic": False, } mismatches = [ name for name, expected in required.items() if getattr(core, name, None) != expected ] if mismatches: raise ValueError( - "strict deterministic core has incompatible arithmetic identity: " - + ", ".join(mismatches) + "strict Attention core has incompatible arithmetic identity: " + ", ".join(mismatches) ) + if core_id in {STRICT_ATTENTION_PRODUCTION_CORE_ID, STRICT_ATTENTION_ROCM_PRODUCTION_CORE_ID}: + production_required = { + "native_attention_arithmetic": True, + "num_splits": 1, + "deterministic_backward": True, + "production_ready": True, + "reference_only": False, + } + production_mismatches = [ + name + for name, expected in production_required.items() + if getattr(core, name, None) != expected + ] + if production_mismatches: + raise ValueError( + "strict production core has incompatible controls: " + + ", ".join(production_mismatches) + ) + if ( + core_id == STRICT_ATTENTION_PRODUCTION_CORE_ID + and getattr(core, "backend_id", None) != "flash_attention_4.cute" + ): + raise ValueError("strict CUDA production core must be FlashAttention-4 CuTe") + if core_id == STRICT_ATTENTION_ROCM_PRODUCTION_CORE_ID: + if getattr(core, "backend_id", None) != "aiter.rocm.ck_dense_mha": + raise ValueError("strict ROCm production core must be AITER CK dense MHA") + if getattr(core, "split_kv_control", None) != "dense_non_split_api": + raise ValueError("strict ROCm production core must use the non-Split-K CK API") + + +def _resolve_strict_core(cfg: FlashInferPagedAttentionConfig) -> Any: + if cfg.deterministic_core is not None: + return cfg.deterministic_core + if torch.version.hip is not None: + from rl_engine.kernels.ops.rocm.attention.flash_attn import StrictRocmAiterCKAttentionCore + + return StrictRocmAiterCKAttentionCore(split_kv=cfg.split_kv) + return StrictFlashAttention4Core(split_kv=cfg.split_kv) def _validate_strict_core_result( @@ -1492,12 +1534,12 @@ def _validate_strict_core_result( ) -> None: if not isinstance(result, DeterministicAttentionCoreResult): raise FlashInferUnavailable( - "strict deterministic core must return DeterministicAttentionCoreResult" + "strict Attention core must return DeterministicAttentionCoreResult" ) if result.out.dtype not in (torch.float16, torch.bfloat16): - raise FlashInferUnavailable("strict deterministic core output must be FP16/BF16") + raise FlashInferUnavailable("strict Attention core output must be FP16/BF16") if result.lse.dtype is not torch.float32: - raise FlashInferUnavailable("strict deterministic core LSE must be FP32") + raise FlashInferUnavailable("strict Attention core LSE must be FP32") expected = { "strict_core_id": core.core_id, "strict_schedule": core.strict_schedule, @@ -1506,7 +1548,7 @@ def _validate_strict_core_result( "accum_dtype": core.accum_dtype, "downcast_at": core.downcast_at, "fallback": False, - "native_attention_arithmetic": False, + "native_attention_arithmetic": core.native_attention_arithmetic, } mismatches = [name for name, value in expected.items() if result.provenance.get(name) != value] if mismatches: @@ -1679,6 +1721,11 @@ def _strict_attention_provenance( cp_required: bool, rope: Any, ) -> dict[str, Any]: + communication_id = getattr(cfg.cp_communication, "backend_id", "unknown") + communication_backend = ( + "self_owned_cuda_ag_rs" if communication_id == "cuda_ag_rs" else communication_id + ) + expected_communication = "rccl_ag_rs" if torch.version.hip is not None else "cuda_ag_rs" return { "attention_backend": core_provenance["attention_backend"], "requested_backend": "flashinfer_layout_adapter", @@ -1696,9 +1743,12 @@ def _strict_attention_provenance( "strict_schedule": core_provenance["strict_schedule"], "accum_dtype": core_provenance["accum_dtype"], "downcast_at": core_provenance["downcast_at"], - "arithmetic_plan_source": "rlkernel_deterministic_gpu_core", + "arithmetic_plan_source": core_provenance.get( + "fa_api_source", + core_provenance.get("aiter_api_source", "rlkernel_reference_core"), + ), "arithmetic_semantics_verified": True, - "native_attention_arithmetic": False, + "native_attention_arithmetic": core_provenance["native_attention_arithmetic"], "fallback": False, "fallback_reason": None, "rope_backend": getattr(rope, "backend_id", "rlkernel.unknown.rope"), @@ -1710,17 +1760,19 @@ def _strict_attention_provenance( "k_cache_rope_state": "post_rope", "batch_invariant_claim": "strict_runtime_verified", "cp_comm_required": cp_required, - "communication_backend": ( - getattr(cfg.cp_communication, "backend_id", "unknown") if cp_required else "none" - ), + "communication_backend": communication_backend if cp_required else "none", + "platform": core_provenance.get("platform", "cuda"), + "num_splits": core_provenance.get("num_splits"), + "split_kv_control": core_provenance.get("split_kv_control"), + "deterministic_backward": core_provenance.get("deterministic_backward"), + "fa_api_source": core_provenance.get("fa_api_source"), + "fa_package_version": core_provenance.get("fa_package_version"), + "aiter_api_source": core_provenance.get("aiter_api_source"), + "aiter_source_sha256": core_provenance.get("aiter_source_sha256"), + "reference_only": bool(core_provenance.get("reference_only", False)), "production_ready": bool( - cp_required - and core_provenance.get("attention_backend") - in { - "rlkernel.cuda.deterministic_attention", - "rlkernel.rocm.deterministic_attention", - } - and getattr(cfg.cp_communication, "backend_id", None) in {"cuda_ag_rs", "rccl_ag_rs"} + core_provenance.get("production_ready", False) + and (not cp_required or communication_id == expected_communication) ), } diff --git a/rl_engine/kernels/ops/rocm/attention/__init__.py b/rl_engine/kernels/ops/rocm/attention/__init__.py index 150c937f..926280c8 100644 --- a/rl_engine/kernels/ops/rocm/attention/__init__.py +++ b/rl_engine/kernels/ops/rocm/attention/__init__.py @@ -1,8 +1,14 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright (c) 2026 RL-Kernel Contributors -from .flash_attn import RocmFlashAttentionOp +from .flash_attn import ( + RocmFlashAttentionOp, + StrictRocmAiterCKAttentionCore, + StrictRocmAttentionUnavailable, +) __all__ = [ "RocmFlashAttentionOp", + "StrictRocmAiterCKAttentionCore", + "StrictRocmAttentionUnavailable", ] diff --git a/rl_engine/kernels/ops/rocm/attention/flash_attn.py b/rl_engine/kernels/ops/rocm/attention/flash_attn.py index a9781cfb..fff003a2 100644 --- a/rl_engine/kernels/ops/rocm/attention/flash_attn.py +++ b/rl_engine/kernels/ops/rocm/attention/flash_attn.py @@ -1,13 +1,278 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright (c) 2026 RL-Kernel Contributors +from __future__ import annotations + +import hashlib +import importlib +import inspect +import math import os +from pathlib import Path +from typing import Any, Callable import torch +from torch.autograd import Function +from torch.autograd.function import once_differentiable +from rl_engine.kernels.attention_contract import ( + STRICT_ATTENTION_ROCM_PRODUCTION_CORE_ID, + STRICT_ATTENTION_ROCM_SCHEDULE_ID, + SplitKVMode, + SplitKVSpec, +) +from rl_engine.kernels.ops.cuda.attention.deterministic_attn import ( + DeterministicAttentionCoreResult, + RLKernelDeterministicAttentionCore, +) from rl_engine.utils.logger import logger _MAX_TESTED_ROCM_TRITON_HEAD_DIM = 512 +_AITER_API_SOURCE = "aiter.ops.mha" + + +class StrictRocmAttentionUnavailable(RuntimeError): + """Raised when the exact AITER CK strict contract is unavailable.""" + + +def _load_aiter_ck_ops() -> tuple[Callable[..., Any], Callable[..., Any], str]: + try: + module = importlib.import_module(_AITER_API_SOURCE) + mha_fwd = getattr(module, "mha_fwd") + mha_bwd = getattr(module, "mha_bwd") + except (AttributeError, ImportError, OSError, RuntimeError) as exc: + raise StrictRocmAttentionUnavailable( + "strict ROCm Attention requires aiter.ops.mha.mha_fwd and mha_bwd" + ) from exc + if not callable(mha_fwd) or not callable(mha_bwd): + raise StrictRocmAttentionUnavailable("AITER CK MHA entry points are not callable") + module_file = inspect.getsourcefile(module) + if not module_file: + raise StrictRocmAttentionUnavailable("cannot fingerprint the AITER MHA source module") + source_sha256 = hashlib.sha256(Path(module_file).read_bytes()).hexdigest() + return mha_fwd, mha_bwd, source_sha256 + + +class _AiterCKAttentionFn(Function): + @staticmethod + def forward( + ctx, + q: torch.Tensor, + k: torch.Tensor, + v: torch.Tensor, + causal: bool, + scale: float, + mha_fwd: Callable[..., Any], + mha_bwd: Callable[..., Any], + ) -> tuple[torch.Tensor, torch.Tensor]: + q_fa = q.transpose(1, 2).contiguous() + k_fa = k.transpose(1, 2).contiguous() + v_fa = v.transpose(1, 2).contiguous() + result = mha_fwd( + q_fa, + k_fa, + v_fa, + 0.0, + float(scale), + bool(causal), + -1, + -1, + 0, + True, + False, + ) + if not isinstance(result, (tuple, list)) or len(result) != 4: + raise StrictRocmAttentionUnavailable( + "AITER mha_fwd must return (out, lse, dropout_mask, rng_state)" + ) + out_fa, lse, _dropout_mask, rng_state = result + if not all(isinstance(item, torch.Tensor) for item in (out_fa, lse, rng_state)): + raise StrictRocmAttentionUnavailable("AITER mha_fwd returned non-tensor state") + ctx.save_for_backward(q_fa, k_fa, v_fa, out_fa, lse, rng_state) + ctx.causal = bool(causal) + ctx.scale = float(scale) + ctx.mha_bwd = mha_bwd + ctx.mark_non_differentiable(lse) + return out_fa.transpose(1, 2).contiguous(), lse.contiguous() + + @staticmethod + @once_differentiable + def backward(ctx, grad_out: torch.Tensor, grad_lse: torch.Tensor): + q_fa, k_fa, v_fa, out_fa, lse, rng_state = ctx.saved_tensors + grad_out_fa = grad_out.transpose(1, 2).contiguous() + result = ctx.mha_bwd( + grad_out_fa, + q_fa, + k_fa, + v_fa, + out_fa, + lse, + 0.0, + ctx.scale, + ctx.causal, + -1, + -1, + True, + rng_state=rng_state, + ) + if not isinstance(result, (tuple, list)) or len(result) < 3: + raise StrictRocmAttentionUnavailable("AITER mha_bwd must return dQ/dK/dV") + dq, dk, dv = result[:3] + return ( + dq.transpose(1, 2).contiguous(), + dk.transpose(1, 2).contiguous(), + dv.transpose(1, 2).contiguous(), + None, + None, + None, + None, + ) + + +class StrictRocmAiterCKAttentionCore: + """Shared ROCm production core using the non-Split-K AITER CK dense MHA.""" + + core_id = STRICT_ATTENTION_ROCM_PRODUCTION_CORE_ID + strict_schedule = STRICT_ATTENTION_ROCM_SCHEDULE_ID + backend_id = "aiter.rocm.ck_dense_mha" + api_source = _AITER_API_SOURCE + merge_order = "global_block_index" + accum_dtype = "fp32" + downcast_at = "final_write" + fallback = False + native_attention_arithmetic = True + production_ready = True + reference_only = False + num_splits = 1 + split_kv_control = "dense_non_split_api" + deterministic_backward = True + + def __init__( + self, + *, + split_kv: SplitKVSpec | None = None, + _mha_fwd: Callable[..., Any] | None = None, + _mha_bwd: Callable[..., Any] | None = None, + _source_sha256: str | None = None, + ) -> None: + requested = SplitKVSpec.disabled() if split_kv is None else split_kv + if not isinstance(requested, SplitKVSpec): + raise TypeError("split_kv must be a SplitKVSpec") + if requested.mode is not SplitKVMode.DISABLED: + raise ValueError("strict AITER CK Attention requires Split-KV to be disabled") + if (_mha_fwd is None) != (_mha_bwd is None): + raise ValueError("test injection requires both AITER forward and backward callables") + if _mha_fwd is None: + mha_fwd, mha_bwd, source_sha256 = _load_aiter_ck_ops() + else: + mha_fwd, mha_bwd = _mha_fwd, _mha_bwd + source_sha256 = "test-double" if _source_sha256 is None else _source_sha256 + if not callable(mha_fwd) or not callable(mha_bwd): + raise StrictRocmAttentionUnavailable("AITER CK MHA entry points are not callable") + self.split_kv = requested + self.source_sha256 = source_sha256 + self._mha_fwd = mha_fwd + self._mha_bwd = mha_bwd + + def forward_with_lse( + self, + q: torch.Tensor, + k: torch.Tensor, + v: torch.Tensor, + *, + causal: bool = True, + scale: float | None = None, + key_padding_mask: torch.Tensor | None = None, + query_position_ids: torch.Tensor | None = None, + key_position_ids: torch.Tensor | None = None, + output_dtype: torch.dtype | None = None, + ) -> DeterministicAttentionCoreResult: + self._validate_inputs(q, k, v, key_padding_mask) + RLKernelDeterministicAttentionCore._validate_positions( + q, + k, + causal=causal, + query_position_ids=query_position_ids, + key_position_ids=key_position_ids, + ) + resolved_dtype = q.dtype if output_dtype is None else output_dtype + if resolved_dtype != q.dtype: + raise ValueError("strict Attention output_dtype must match the Q/K/V input dtype") + resolved_scale = 1.0 / math.sqrt(q.size(-1)) if scale is None else float(scale) + out, lse = _AiterCKAttentionFn.apply( + q, + k, + v, + bool(causal), + resolved_scale, + self._mha_fwd, + self._mha_bwd, + ) + expected_lse_shape = (q.size(0), q.size(1), q.size(2)) + if out.shape != q.shape or out.dtype != resolved_dtype: + raise StrictRocmAttentionUnavailable("AITER CK output shape/dtype changed") + if tuple(lse.shape) != expected_lse_shape or lse.dtype != torch.float32: + raise StrictRocmAttentionUnavailable("AITER CK must export [B,H,Sq] FP32 LSE") + device_properties = torch.cuda.get_device_properties(q.device) + return DeterministicAttentionCoreResult( + out=out, + lse=lse, + provenance={ + "strict_core_id": self.core_id, + "strict_schedule": self.strict_schedule, + "attention_backend": self.backend_id, + "platform": "rocm", + "torch_version": torch.__version__, + "rocm_version": torch.version.hip, + "gpu_name": device_properties.name, + "gpu_arch": getattr(device_properties, "gcnArchName", "unknown"), + "aiter_api_source": self.api_source, + "aiter_source_sha256": self.source_sha256, + "num_splits": self.num_splits, + "split_kv_control": self.split_kv_control, + "deterministic_backward": self.deterministic_backward, + "dropout_p": 0.0, + "split_kv": self.split_kv.resolve(k.size(2), backend=self.backend_id).to_dict(), + "merge_order": self.merge_order, + "accum_dtype": self.accum_dtype, + "downcast_at": self.downcast_at, + "fallback": self.fallback, + "fallback_reason": None, + "native_attention_arithmetic": self.native_attention_arithmetic, + "production_ready": self.production_ready, + "reference_only": self.reference_only, + }, + ) + + @staticmethod + def _validate_inputs( + q: torch.Tensor, + k: torch.Tensor, + v: torch.Tensor, + key_padding_mask: torch.Tensor | None, + ) -> None: + if torch.version.hip is None: + raise StrictRocmAttentionUnavailable("strict AITER CK core requires ROCm PyTorch") + if key_padding_mask is not None: + raise ValueError("strict AITER CK core materializes each unpadded logical row") + if q.ndim != 4 or k.ndim != 4 or v.ndim != 4: + raise ValueError("q/k/v must be 4-D [B,H,S,D]") + if q.size(0) != 1 or k.size(0) != 1 or v.size(0) != 1: + raise ValueError("strict AITER CK core executes one logical batch row at a time") + if k.shape != v.shape or q.size(-1) != k.size(-1): + raise ValueError("k/v shapes and q/k/v head dimensions must match") + if q.size(1) % k.size(1) != 0: + raise ValueError("Q heads must be divisible by KV heads for GQA") + if q.size(-1) > 256 or q.size(-1) % 8: + raise ValueError("AITER CK requires head_dim <= 256 and divisible by 8") + if q.dtype not in (torch.float16, torch.bfloat16): + raise ValueError("strict AITER CK core supports FP16/BF16 only") + if k.dtype != q.dtype or v.dtype != q.dtype: + raise ValueError("q/k/v must share one dtype") + if not (q.is_cuda and k.is_cuda and v.is_cuda): + raise ValueError("strict AITER CK core requires ROCm GPU tensors") + if not (q.device == k.device == v.device): + raise ValueError("q/k/v must be on one ROCm device") def _select_flash_attn_backend() -> str: diff --git a/scripts/ws2_p2p_nccl_attention_reference_check.py b/scripts/ws2_p2p_nccl_attention_reference_check.py index 18248164..abceb610 100644 --- a/scripts/ws2_p2p_nccl_attention_reference_check.py +++ b/scripts/ws2_p2p_nccl_attention_reference_check.py @@ -29,9 +29,11 @@ sys.path.insert(0, str(REPO_ROOT)) from rl_engine.kernels.attention_contract import ( # noqa: E402 - STRICT_ATTENTION_CORE_ID, + STRICT_ATTENTION_FA4_SCHEDULE_ID, + STRICT_ATTENTION_PRODUCTION_CORE_ID, STRICT_ATTENTION_RING_SCHEDULE_ID, - STRICT_ATTENTION_SCHEDULE_ID, + STRICT_ATTENTION_ROCM_PRODUCTION_CORE_ID, + STRICT_ATTENTION_ROCM_SCHEDULE_ID, ) from rl_engine.kernels.ops.cuda.attention.cp_comm import ( # noqa: E402 AttentionCPBlockMetadata, @@ -43,9 +45,7 @@ P2PNCCLAttentionCPCommunication, RCCLAGRSAttentionCPCommunication, ) -from rl_engine.kernels.ops.cuda.attention.deterministic_attn import ( # noqa: E402 - RLKernelDeterministicAttentionCore, -) +from rl_engine.kernels.ops.cuda.attention.flash_attn import StrictFlashAttention4Core # noqa: E402 from rl_engine.kernels.ops.cuda.attention.flashinfer_paged_attention import ( # noqa: E402 FlashInferPagedAttentionConfig, FlashInferQwen3PagedAttentionOp, @@ -510,7 +510,7 @@ def _run_strict_shared_core_check( rope = _strict_rope_op() q_ready = _apply_strict_rope(rope, q_ref, positions, config.rope.rope_theta) k_ready = _apply_strict_rope(rope, k_ref, positions, config.rope.rope_theta) - reference = RLKernelDeterministicAttentionCore().forward_with_lse( + reference = _strict_attention_core().forward_with_lse( q_ready, k_ready, v_ref, @@ -601,24 +601,30 @@ def _strict_shared_core_identity_errors( ) -> list[str]: """Return every strict-contract provenance mismatch for the rank report.""" - expected_backend = ( - "rlkernel.rocm.deterministic_attention" - if is_rocm - else "rlkernel.cuda.deterministic_attention" + expected_core = ( + STRICT_ATTENTION_ROCM_PRODUCTION_CORE_ID if is_rocm else STRICT_ATTENTION_PRODUCTION_CORE_ID + ) + expected_schedule = ( + STRICT_ATTENTION_ROCM_SCHEDULE_ID if is_rocm else STRICT_ATTENTION_FA4_SCHEDULE_ID ) + expected_backend = "aiter.rocm.ck_dense_mha" if is_rocm else "flash_attention_4.cute" expected_rope = "rlkernel.rocm.deterministic_rope" if is_rocm else "rlkernel.cuda.rope_sm90" + expected_communication = "rccl_ag_rs" if is_rocm else "self_owned_cuda_ag_rs" required = { - "strict_core_id": STRICT_ATTENTION_CORE_ID, - "strict_schedule": STRICT_ATTENTION_SCHEDULE_ID, + "strict_core_id": expected_core, + "strict_schedule": expected_schedule, "attention_backend": expected_backend, "actual_backend": expected_backend, "rope_backend": expected_rope, "strict_mode": True, - "native_attention_arithmetic": False, + "native_attention_arithmetic": True, + "num_splits": 1, + "deterministic_backward": True, + "reference_only": False, "fallback": False, "strict_split_kv": "disabled", "strict_comm_autograd": True, - "communication_backend": transport, + "communication_backend": expected_communication, "production_ready": True, "strict_full_qkv_all_gather": True, "strict_position_ids_all_gather": True, @@ -631,11 +637,30 @@ def _strict_shared_core_identity_errors( "q_rope_state": "post_rope", "k_cache_rope_state": "post_rope", } - return [ + errors = [ f"{name}={provenance.get(name)!r}, expected {expected!r}" for name, expected in required.items() if provenance.get(name) != expected ] + if is_rocm: + if provenance.get("split_kv_control") != "dense_non_split_api": + errors.append("split_kv_control must prove the AITER dense non-Split-K API") + if provenance.get("aiter_api_source") != "aiter.ops.mha": + errors.append("aiter_api_source must identify aiter.ops.mha") + if not provenance.get("aiter_source_sha256"): + errors.append("aiter_source_sha256 is missing") + else: + if provenance.get("fa_api_source") != "flash_attn.cute.interface": + errors.append("fa_api_source must identify the FA4 CuTe API") + return errors + + +def _strict_attention_core(): + if torch.version.hip is not None: + from rl_engine.kernels.ops.rocm.attention.flash_attn import StrictRocmAiterCKAttentionCore + + return StrictRocmAiterCKAttentionCore() + return StrictFlashAttention4Core() def _strict_rope_op(): diff --git a/scripts/ws2_pr7_flashinfer_attention_check.py b/scripts/ws2_pr7_flashinfer_attention_check.py index 8fc95800..cea6ca51 100644 --- a/scripts/ws2_pr7_flashinfer_attention_check.py +++ b/scripts/ws2_pr7_flashinfer_attention_check.py @@ -26,16 +26,16 @@ sys.path.insert(0, str(REPO_ROOT)) from rl_engine.kernels.attention_contract import ( # noqa: E402 - STRICT_ATTENTION_CORE_ID, - STRICT_ATTENTION_SCHEDULE_ID, + STRICT_ATTENTION_FA4_SCHEDULE_ID, + STRICT_ATTENTION_PRODUCTION_CORE_ID, + STRICT_ATTENTION_ROCM_PRODUCTION_CORE_ID, + STRICT_ATTENTION_ROCM_SCHEDULE_ID, ) from rl_engine.kernels.ops.cuda.attention.cp_comm import ( # noqa: E402 AttentionCPCommunicationPlan, AttentionParallelSpec, ) -from rl_engine.kernels.ops.cuda.attention.deterministic_attn import ( # noqa: E402 - RLKernelDeterministicAttentionCore, -) +from rl_engine.kernels.ops.cuda.attention.flash_attn import StrictFlashAttention4Core # noqa: E402 from rl_engine.kernels.ops.cuda.attention.flashinfer_paged_attention import ( # noqa: E402 FlashInferPagedAttentionConfig, FlashInferQwen3PagedAttentionOp, @@ -46,7 +46,6 @@ _materialize_strict_logical_kv, build_flashinfer_paged_kv_plan, ) -from rl_engine.kernels.ops.cuda.rotary_embedding.rope import RoPESM90Op # noqa: E402 from rl_engine.testing.attention_comparison import ( # noqa: E402 AttentionPathResult, DecodeAttentionInputs, @@ -398,10 +397,10 @@ def _run_strict_cuda_reference( config: FlashInferPagedAttentionConfig, paged_plan: Any, ) -> AttentionPathResult: - """Call the shared CUDA core directly on the logical KV sequence.""" + """Call the platform production core directly on the logical KV sequence.""" - core = config.deterministic_core or RLKernelDeterministicAttentionCore(split_kv=config.split_kv) - rope = config.strict_rope_op or RoPESM90Op() + core = config.deterministic_core or _strict_attention_core(config.split_kv) + rope = config.strict_rope_op or _strict_rope_op() logical_k, logical_v, key_positions = _materialize_strict_logical_kv( inputs.k_cache, inputs.v_cache, @@ -431,12 +430,12 @@ def _run_strict_cuda_reference( outputs.append(result.out) lses.append(result.lse) return AttentionPathResult( - name="direct_rlkernel_cuda_deterministic_attention", + name="direct_platform_strict_attention", out=torch.cat(outputs, dim=0), lse=torch.cat(lses, dim=0), provenance={ - "strict_core_id": STRICT_ATTENTION_CORE_ID, - "strict_schedule": STRICT_ATTENTION_SCHEDULE_ID, + "strict_core_id": core.core_id, + "strict_schedule": core.strict_schedule, "split_kv_policy": "disabled", }, ) @@ -647,14 +646,41 @@ def _acceptance_errors(report: dict[str, Any], args: argparse.Namespace) -> list if provenance.get("fallback") is not False: errors.append("FlashInfer execution used or omitted fallback provenance") if args.strict: + is_rocm = torch.version.hip is not None + expected_core = ( + STRICT_ATTENTION_ROCM_PRODUCTION_CORE_ID + if is_rocm + else STRICT_ATTENTION_PRODUCTION_CORE_ID + ) + expected_schedule = ( + STRICT_ATTENTION_ROCM_SCHEDULE_ID if is_rocm else STRICT_ATTENTION_FA4_SCHEDULE_ID + ) + expected_backend = "aiter.rocm.ck_dense_mha" if is_rocm else "flash_attention_4.cute" if provenance.get("strict_mode") is not True: errors.append("strict runtime did not execute the shared Attention core") - if provenance.get("strict_core_id") != STRICT_ATTENTION_CORE_ID: + if provenance.get("strict_core_id") != expected_core: errors.append("strict runtime core identity is invalid") - if provenance.get("strict_schedule") != STRICT_ATTENTION_SCHEDULE_ID: + if provenance.get("strict_schedule") != expected_schedule: errors.append("strict runtime arithmetic schedule is invalid") - if provenance.get("native_attention_arithmetic") is not False: - errors.append("strict runtime entered native FlashInfer Attention arithmetic") + if provenance.get("actual_backend") != expected_backend: + errors.append("strict runtime backend identity is invalid") + if provenance.get("native_attention_arithmetic") is not True: + errors.append("strict runtime did not execute the native production arithmetic") + if provenance.get("num_splits") != 1: + errors.append("strict runtime did not prove one reduction partition") + if provenance.get("deterministic_backward") is not True: + errors.append("strict runtime did not request deterministic backward") + if provenance.get("reference_only") is not False: + errors.append("strict runtime selected the reference core") + if is_rocm: + if provenance.get("split_kv_control") != "dense_non_split_api": + errors.append("strict ROCm runtime did not use AITER dense non-Split-K MHA") + if provenance.get("aiter_api_source") != "aiter.ops.mha": + errors.append("strict ROCm runtime did not prove the AITER API source") + if not provenance.get("aiter_source_sha256"): + errors.append("strict ROCm runtime did not fingerprint AITER MHA") + elif provenance.get("fa_api_source") != "flash_attn.cute.interface": + errors.append("strict CUDA runtime did not prove the FA4 CuTe API source") strict_plans = provenance.get("strict_core_row_plans") if not isinstance(strict_plans, list) or not strict_plans: errors.append("strict no-Split-K execution plans are missing") @@ -685,6 +711,20 @@ def _acceptance_errors(report: dict[str, Any], args: argparse.Namespace) -> list return errors +def _strict_attention_core(split_kv): + if torch.version.hip is not None: + from rl_engine.kernels.ops.rocm.attention.flash_attn import StrictRocmAiterCKAttentionCore + + return StrictRocmAiterCKAttentionCore(split_kv=split_kv) + return StrictFlashAttention4Core(split_kv=split_kv) + + +def _strict_rope_op(): + from rl_engine.kernels.ops.cuda.rotary_embedding.rope import RocmDeterministicRoPEOp, RoPESM90Op + + return RocmDeterministicRoPEOp() if torch.version.hip is not None else RoPESM90Op() + + def _select_batch_row(inputs: DecodeAttentionInputs, batch_index: int) -> DecodeAttentionInputs: metadata = inputs.metadata cp_block_owners = ( diff --git a/tests/test_attention_correctness.py b/tests/test_attention_correctness.py index d68ad3cb..a77e195c 100644 --- a/tests/test_attention_correctness.py +++ b/tests/test_attention_correctness.py @@ -8,6 +8,15 @@ import torch import torch.nn.functional as F +from rl_engine.kernels.attention_contract import ( + STRICT_ATTENTION_ROCM_PRODUCTION_CORE_ID, + STRICT_ATTENTION_ROCM_SCHEDULE_ID, +) +from rl_engine.kernels.ops.rocm.attention.flash_attn import ( + StrictRocmAiterCKAttentionCore, + StrictRocmAttentionUnavailable, +) + try: from torch.nn.attention import SDPBackend, sdpa_kernel except ImportError: @@ -440,3 +449,143 @@ def test_native_attention_rejects_invalid_gqa_head_ratio(): with pytest.raises(ValueError, match="q heads must be divisible"): NativeAttentionOp()(q, k, v) + + +def test_strict_rocm_aiter_ck_core_fixes_forward_and_backward_contract(monkeypatch): + calls = [] + + def fake_fwd( + q, + k, + v, + dropout_p, + softmax_scale, + causal, + window_left, + window_right, + sink_size, + return_lse, + return_dropout_mask, + ): + calls.append( + ( + "forward", + dropout_p, + softmax_scale, + causal, + window_left, + window_right, + sink_size, + return_lse, + return_dropout_mask, + ) + ) + return ( + q.clone(), + torch.zeros(q.size(0), q.size(2), q.size(1), dtype=torch.float32), + torch.empty(0), + torch.zeros(2, dtype=torch.int64), + ) + + def fake_bwd( + dout, + q, + k, + v, + out, + lse, + dropout_p, + softmax_scale, + causal, + window_left, + window_right, + deterministic, + **kwargs, + ): + calls.append( + ( + "backward", + dropout_p, + softmax_scale, + causal, + window_left, + window_right, + deterministic, + kwargs["rng_state"].shape, + ) + ) + return torch.ones_like(q), torch.ones_like(k), torch.ones_like(v), torch.empty(0) + + core = StrictRocmAiterCKAttentionCore( + _mha_fwd=fake_fwd, + _mha_bwd=fake_bwd, + _source_sha256="a" * 64, + ) + monkeypatch.setattr(core, "_validate_inputs", lambda *_args: None) + monkeypatch.setattr( + torch.cuda, + "get_device_properties", + lambda _device: type("Props", (), {"name": "test-gpu", "gcnArchName": "gfx-test"})(), + ) + q = torch.randn(1, 4, 2, 8, dtype=torch.bfloat16, requires_grad=True) + k = torch.randn(1, 2, 3, 8, dtype=torch.bfloat16, requires_grad=True) + v = torch.randn(1, 2, 3, 8, dtype=torch.bfloat16, requires_grad=True) + result = core.forward_with_lse( + q, + k, + v, + causal=True, + scale=0.125, + query_position_ids=torch.tensor([[1, 2]]), + key_position_ids=torch.tensor([[0, 1, 2]]), + ) + result.out.float().sum().backward() + + assert calls == [ + ("forward", 0.0, 0.125, True, -1, -1, 0, True, False), + ("backward", 0.0, 0.125, True, -1, -1, True, torch.Size([2])), + ] + assert result.out.shape == q.shape + assert result.lse.shape == q.shape[:3] + assert result.lse.dtype is torch.float32 + assert result.provenance["strict_core_id"] == STRICT_ATTENTION_ROCM_PRODUCTION_CORE_ID + assert result.provenance["strict_schedule"] == STRICT_ATTENTION_ROCM_SCHEDULE_ID + assert result.provenance["attention_backend"] == "aiter.rocm.ck_dense_mha" + assert result.provenance["split_kv_control"] == "dense_non_split_api" + assert result.provenance["num_splits"] == 1 + assert result.provenance["deterministic_backward"] is True + assert result.provenance["aiter_source_sha256"] == "a" * 64 + assert q.grad is not None and k.grad is not None and v.grad is not None + + +def test_strict_rocm_aiter_ck_core_rejects_non_fp32_lse(monkeypatch): + def fake_fwd(q, k, v, *_args): + return ( + q, + torch.zeros(q.size(0), q.size(2), q.size(1), dtype=q.dtype), + torch.empty(0), + torch.empty(2), + ) + + core = StrictRocmAiterCKAttentionCore( + _mha_fwd=fake_fwd, + _mha_bwd=lambda *_args, **_kwargs: None, + ) + monkeypatch.setattr(core, "_validate_inputs", lambda *_args: None) + monkeypatch.setattr( + torch.cuda, + "get_device_properties", + lambda _device: type("Props", (), {"name": "test-gpu", "gcnArchName": "gfx-test"})(), + ) + q = torch.randn(1, 4, 1, 8, dtype=torch.bfloat16) + k = torch.randn(1, 2, 1, 8, dtype=torch.bfloat16) + + with pytest.raises(StrictRocmAttentionUnavailable, match="FP32 LSE"): + core.forward_with_lse( + q, + k, + k, + causal=True, + query_position_ids=torch.tensor([[0]]), + key_position_ids=torch.tensor([[0]]), + ) diff --git a/tests/test_flashinfer_pr7_attention.py b/tests/test_flashinfer_pr7_attention.py index 7d43bd9b..22b67a35 100644 --- a/tests/test_flashinfer_pr7_attention.py +++ b/tests/test_flashinfer_pr7_attention.py @@ -15,7 +15,11 @@ from rl_engine.kernels.attention_contract import ( STRICT_ATTENTION_CORE_ID, + STRICT_ATTENTION_FA4_SCHEDULE_ID, + STRICT_ATTENTION_PRODUCTION_CORE_ID, STRICT_ATTENTION_RING_SCHEDULE_ID, + STRICT_ATTENTION_ROCM_PRODUCTION_CORE_ID, + STRICT_ATTENTION_ROCM_SCHEDULE_ID, STRICT_ATTENTION_SCHEDULE_ID, SplitKVSpec, ) @@ -1725,7 +1729,7 @@ def test_pr7_check_acceptance_errors_require_all_drift_and_invariance_fields(): assert check_script._acceptance_errors(report, args) == ["batch_invariant_sweep failed"] -def test_pr7_check_accepts_strict_shared_core_without_native_flashinfer_arithmetic(): +def test_pr7_check_accepts_strict_cuda_production_core(): args = check_script._parse_args(["--strict", "--device", "cuda"]) report = { "device": "cuda:0", @@ -1734,9 +1738,14 @@ def test_pr7_check_accepts_strict_shared_core_without_native_flashinfer_arithmet "attention_mode": "decode", "fallback": False, "strict_mode": True, - "strict_core_id": STRICT_ATTENTION_CORE_ID, - "strict_schedule": STRICT_ATTENTION_SCHEDULE_ID, - "native_attention_arithmetic": False, + "strict_core_id": STRICT_ATTENTION_PRODUCTION_CORE_ID, + "strict_schedule": STRICT_ATTENTION_FA4_SCHEDULE_ID, + "actual_backend": "flash_attention_4.cute", + "native_attention_arithmetic": True, + "num_splits": 1, + "deterministic_backward": True, + "fa_api_source": "flash_attn.cute.interface", + "reference_only": False, "strict_core_row_plans": [{"actual_split_kv_policy": "disabled"}], "rope_backend": "rlkernel.cuda.rope_sm90", "rope_theta": 1_000_000.0, @@ -1816,17 +1825,21 @@ def test_strict_shared_core_entrypoint_requires_self_owned_ag_rs(): def _strict_acceptance_provenance(**overrides): provenance = { - "strict_core_id": STRICT_ATTENTION_CORE_ID, - "strict_schedule": STRICT_ATTENTION_SCHEDULE_ID, - "attention_backend": "rlkernel.cuda.deterministic_attention", - "actual_backend": "rlkernel.cuda.deterministic_attention", + "strict_core_id": STRICT_ATTENTION_PRODUCTION_CORE_ID, + "strict_schedule": STRICT_ATTENTION_FA4_SCHEDULE_ID, + "attention_backend": "flash_attention_4.cute", + "actual_backend": "flash_attention_4.cute", "rope_backend": "rlkernel.cuda.rope_sm90", "strict_mode": True, - "native_attention_arithmetic": False, + "native_attention_arithmetic": True, + "num_splits": 1, + "deterministic_backward": True, + "reference_only": False, + "fa_api_source": "flash_attn.cute.interface", "fallback": False, "strict_split_kv": "disabled", "strict_comm_autograd": True, - "communication_backend": "cuda_ag_rs", + "communication_backend": "self_owned_cuda_ag_rs", "production_ready": True, "strict_full_qkv_all_gather": True, "strict_position_ids_all_gather": True, @@ -1867,10 +1880,15 @@ def test_strict_shared_core_acceptance_rejects_provenance_drift(field, invalid): def test_strict_shared_core_acceptance_requires_rocm_backend_and_rope(): provenance = _strict_acceptance_provenance( - attention_backend="rlkernel.rocm.deterministic_attention", - actual_backend="rlkernel.rocm.deterministic_attention", + strict_core_id=STRICT_ATTENTION_ROCM_PRODUCTION_CORE_ID, + strict_schedule=STRICT_ATTENTION_ROCM_SCHEDULE_ID, + attention_backend="aiter.rocm.ck_dense_mha", + actual_backend="aiter.rocm.ck_dense_mha", rope_backend="rlkernel.rocm.deterministic_rope", communication_backend="rccl_ag_rs", + split_kv_control="dense_non_split_api", + aiter_api_source="aiter.ops.mha", + aiter_source_sha256="a" * 64, ) assert not p2p_check_script._strict_shared_core_identity_errors( From 4418d18df67f55a5014ce82317ddebf39aa3ca49 Mon Sep 17 00:00:00 2001 From: lamentropetion <3051000145@qq.com> Date: Thu, 20 Aug 2026 21:41:29 +0800 Subject: [PATCH 09/35] test(attention): accept strict ROCm RoPE provenance --- scripts/ws2_pr7_flashinfer_attention_check.py | 13 +++++-- tests/test_flashinfer_pr7_attention.py | 38 +++++++++++++++++++ 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/scripts/ws2_pr7_flashinfer_attention_check.py b/scripts/ws2_pr7_flashinfer_attention_check.py index cea6ca51..f9e1d7de 100644 --- a/scripts/ws2_pr7_flashinfer_attention_check.py +++ b/scripts/ws2_pr7_flashinfer_attention_check.py @@ -686,10 +686,15 @@ def _acceptance_errors(report: dict[str, Any], args: argparse.Namespace) -> list errors.append("strict no-Split-K execution plans are missing") elif any(plan.get("actual_split_kv_policy") != "disabled" for plan in strict_plans): errors.append("strict runtime did not keep Split-KV disabled") - if provenance.get("rope_backend") not in { - "rlkernel.cuda.rope_sm90", - "rlkernel.cuda.rope_sm90_op", - }: + expected_rope_backends = ( + {"rlkernel.rocm.deterministic_rope"} + if is_rocm + else { + "rlkernel.cuda.rope_sm90", + "rlkernel.cuda.rope_sm90_op", + } + ) + if provenance.get("rope_backend") not in expected_rope_backends: errors.append("strict runtime did not use the RL-Kernel WS1 RoPE operator") elif provenance.get("pos_encoding_mode") != "ROPE_LLAMA": errors.append("FlashInfer runtime did not use ROPE_LLAMA") diff --git a/tests/test_flashinfer_pr7_attention.py b/tests/test_flashinfer_pr7_attention.py index 22b67a35..e11792bb 100644 --- a/tests/test_flashinfer_pr7_attention.py +++ b/tests/test_flashinfer_pr7_attention.py @@ -1764,6 +1764,44 @@ def test_pr7_check_accepts_strict_cuda_production_core(): assert check_script._acceptance_errors(report, args) == [] +def test_pr7_check_accepts_strict_rocm_production_core(monkeypatch): + monkeypatch.setattr(torch.version, "hip", "6.3.0") + args = check_script._parse_args(["--strict", "--device", "cuda"]) + report = { + "device": "cuda:0", + "shape": {"q_heads": 16, "kv_heads": 4, "head_dim": 128}, + "candidate_provenance": { + "attention_mode": "decode", + "fallback": False, + "strict_mode": True, + "strict_core_id": STRICT_ATTENTION_ROCM_PRODUCTION_CORE_ID, + "strict_schedule": STRICT_ATTENTION_ROCM_SCHEDULE_ID, + "actual_backend": "aiter.rocm.ck_dense_mha", + "native_attention_arithmetic": True, + "num_splits": 1, + "deterministic_backward": True, + "reference_only": False, + "split_kv_control": "dense_non_split_api", + "aiter_api_source": "aiter.ops.mha", + "aiter_source_sha256": "a" * 64, + "strict_core_row_plans": [{"actual_split_kv_policy": "disabled"}], + "rope_backend": "rlkernel.rocm.deterministic_rope", + "rope_theta": 1_000_000.0, + "rotary_dim": 128, + "arithmetic_semantics_verified": True, + }, + "drift": { + "out": {"max_abs": 0.0}, + "lse": {"max_abs": 0.0}, + "dlogp": {"max_abs": 0.0}, + }, + "batch_invariant_sweep": {"passed": True}, + "page_layout_invariant_sweep": {"passed": True}, + } + + assert check_script._acceptance_errors(report, args) == [] + + def test_pr7_check_rejects_nonfinite_drift_and_wrong_tp_local_shape(): args = check_script._parse_args([]) report = { From fe398240699fd230289c40588c1f5c10275cb049 Mon Sep 17 00:00:00 2001 From: lamentropetion <3051000145@qq.com> Date: Thu, 20 Aug 2026 22:18:56 +0800 Subject: [PATCH 10/35] test(attention): validate reported platform provenance --- scripts/ws2_pr7_flashinfer_attention_check.py | 5 ++++- tests/test_flashinfer_pr7_attention.py | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/scripts/ws2_pr7_flashinfer_attention_check.py b/scripts/ws2_pr7_flashinfer_attention_check.py index f9e1d7de..b9a42e90 100644 --- a/scripts/ws2_pr7_flashinfer_attention_check.py +++ b/scripts/ws2_pr7_flashinfer_attention_check.py @@ -646,7 +646,10 @@ def _acceptance_errors(report: dict[str, Any], args: argparse.Namespace) -> list if provenance.get("fallback") is not False: errors.append("FlashInfer execution used or omitted fallback provenance") if args.strict: - is_rocm = torch.version.hip is not None + platform = provenance.get("platform", "cuda") + if platform not in {"cuda", "rocm"}: + errors.append("strict runtime platform provenance is invalid") + is_rocm = platform == "rocm" expected_core = ( STRICT_ATTENTION_ROCM_PRODUCTION_CORE_ID if is_rocm diff --git a/tests/test_flashinfer_pr7_attention.py b/tests/test_flashinfer_pr7_attention.py index e11792bb..a18464ed 100644 --- a/tests/test_flashinfer_pr7_attention.py +++ b/tests/test_flashinfer_pr7_attention.py @@ -1764,8 +1764,7 @@ def test_pr7_check_accepts_strict_cuda_production_core(): assert check_script._acceptance_errors(report, args) == [] -def test_pr7_check_accepts_strict_rocm_production_core(monkeypatch): - monkeypatch.setattr(torch.version, "hip", "6.3.0") +def test_pr7_check_accepts_strict_rocm_production_core(): args = check_script._parse_args(["--strict", "--device", "cuda"]) report = { "device": "cuda:0", @@ -1777,6 +1776,7 @@ def test_pr7_check_accepts_strict_rocm_production_core(monkeypatch): "strict_core_id": STRICT_ATTENTION_ROCM_PRODUCTION_CORE_ID, "strict_schedule": STRICT_ATTENTION_ROCM_SCHEDULE_ID, "actual_backend": "aiter.rocm.ck_dense_mha", + "platform": "rocm", "native_attention_arithmetic": True, "num_splits": 1, "deterministic_backward": True, From f6ff0940ec9f5d18156a1014e4b2f2f93b38d635 Mon Sep 17 00:00:00 2001 From: lamentropetion <3051000145@qq.com> Date: Thu, 20 Aug 2026 22:31:44 +0800 Subject: [PATCH 11/35] test(attention): run strict references by logical row --- .../ws2_p2p_nccl_attention_reference_check.py | 35 ++++++++++++++++--- tests/test_flashinfer_pr7_attention.py | 35 +++++++++++++++++++ 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/scripts/ws2_p2p_nccl_attention_reference_check.py b/scripts/ws2_p2p_nccl_attention_reference_check.py index abceb610..e04c4c06 100644 --- a/scripts/ws2_p2p_nccl_attention_reference_check.py +++ b/scripts/ws2_p2p_nccl_attention_reference_check.py @@ -510,13 +510,11 @@ def _run_strict_shared_core_check( rope = _strict_rope_op() q_ready = _apply_strict_rope(rope, q_ref, positions, config.rope.rope_theta) k_ready = _apply_strict_rope(rope, k_ref, positions, config.rope.rope_theta) - reference = _strict_attention_core().forward_with_lse( + reference = _strict_attention_reference_rows( q_ready, k_ready, v_ref, - causal=True, - query_position_ids=positions, - key_position_ids=positions, + positions=positions, output_dtype=q.dtype, ) out_ref = reference.out[:, :, start:end, :] @@ -663,6 +661,35 @@ def _strict_attention_core(): return StrictFlashAttention4Core() +def _strict_attention_reference_rows( + q: torch.Tensor, + k: torch.Tensor, + v: torch.Tensor, + *, + positions: torch.Tensor, + output_dtype: torch.dtype, +) -> SimpleNamespace: + """Run the production core with its one-logical-row execution contract.""" + + core = _strict_attention_core() + rows = [ + core.forward_with_lse( + q[index : index + 1], + k[index : index + 1], + v[index : index + 1], + causal=True, + query_position_ids=positions[index : index + 1], + key_position_ids=positions[index : index + 1], + output_dtype=output_dtype, + ) + for index in range(q.size(0)) + ] + return SimpleNamespace( + out=torch.cat([row.out for row in rows], dim=0), + lse=torch.cat([row.lse for row in rows], dim=0), + ) + + def _strict_rope_op(): from rl_engine.kernels.ops.cuda.rotary_embedding.rope import RocmDeterministicRoPEOp, RoPESM90Op diff --git a/tests/test_flashinfer_pr7_attention.py b/tests/test_flashinfer_pr7_attention.py index a18464ed..8bbbdf2a 100644 --- a/tests/test_flashinfer_pr7_attention.py +++ b/tests/test_flashinfer_pr7_attention.py @@ -1861,6 +1861,41 @@ def test_strict_shared_core_entrypoint_requires_self_owned_ag_rs(): assert args.strict_shared_core is True +def test_strict_shared_core_reference_executes_one_logical_row(monkeypatch): + calls = [] + + class _RowCore: + def forward_with_lse(self, q, k, v, **kwargs): + calls.append((q.shape[0], kwargs["query_position_ids"].clone())) + return types.SimpleNamespace( + out=q + k + v, + lse=(q + k).sum(dim=-1), + ) + + monkeypatch.setattr(p2p_check_script, "_strict_attention_core", _RowCore) + q = torch.randn(2, 1, 3, 4, requires_grad=True) + k = torch.randn(2, 1, 3, 4, requires_grad=True) + v = torch.randn(2, 1, 3, 4, requires_grad=True) + positions = torch.arange(3).expand(2, -1) + + result = p2p_check_script._strict_attention_reference_rows( + q, + k, + v, + positions=positions, + output_dtype=q.dtype, + ) + + assert [batch for batch, _positions in calls] == [1, 1] + assert [item.tolist() for _batch, item in calls] == [[[0, 1, 2]], [[0, 1, 2]]] + assert torch.equal(result.out, q + k + v) + assert torch.equal(result.lse, (q + k).sum(dim=-1)) + (result.out.sum() + result.lse.sum()).backward() + assert q.grad is not None + assert k.grad is not None + assert v.grad is not None + + def _strict_acceptance_provenance(**overrides): provenance = { "strict_core_id": STRICT_ATTENTION_PRODUCTION_CORE_ID, From 9ff0e6dc5a82d348d755f155c5ccc7790840343b Mon Sep 17 00:00:00 2001 From: lamentropetion <3051000145@qq.com> Date: Thu, 20 Aug 2026 22:45:42 +0800 Subject: [PATCH 12/35] test(attention): report executed vendor backend --- scripts/ws2_pr7_flashinfer_attention_check.py | 48 +++++++++++++------ tests/test_flashinfer_pr7_attention.py | 34 +++++++++++++ 2 files changed, 67 insertions(+), 15 deletions(-) diff --git a/scripts/ws2_pr7_flashinfer_attention_check.py b/scripts/ws2_pr7_flashinfer_attention_check.py index b9a42e90..7a11863c 100644 --- a/scripts/ws2_pr7_flashinfer_attention_check.py +++ b/scripts/ws2_pr7_flashinfer_attention_check.py @@ -1,12 +1,11 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright (c) 2026 RL-Kernel Contributors -"""PR7 FlashInfer RoPE-fused paged attention validation entry point. +"""PR7 strict paged-layout Attention validation entry point. -The default dry-run mode is CI/local friendly: it builds the FlashInfer page plan -and provenance without importing FlashInfer or requiring CUDA. On a CUDA host -with FlashInfer installed, omit ``--dry-run`` to run the opt-in PR7 candidate and -compare it with the PR6 full logical KV reference. +The default dry-run mode is CI/local friendly: it builds the paged-KV plan and +provenance without requiring a GPU vendor backend. On a GPU host, omit +``--dry-run`` to run the platform production core and its strict reference. """ from __future__ import annotations @@ -70,7 +69,7 @@ def main(argv: Sequence[str] | None = None) -> int: report: dict[str, Any] = { "status": "dry_run" if args.dry_run else "executed", "pr": "PR7", - "target": "Qwen3-8B TP-local FlashInfer candidate; CP transport validated separately", + "target": "Qwen3-8B TP-local paged Attention; CP transport validated separately", "mode": config.mode, "device": str(device), "shape": { @@ -99,11 +98,11 @@ def main(argv: Sequence[str] | None = None) -> int: | {"cp_comm_required": config.require_cp_comm}, "paged_kv_plan": plan.provenance(), "tests_expected": [ - "FlashInfer ROPE_LLAMA vs NativeRoPEOp + full logical KV reference", + "platform production core vs direct same-core reference", "split-K disabled/fixed policy drift", "batch composition/position invariant sweep", "attention-domain LSE export drift", - "strict shared CUDA core with separate multi-rank AG/RS forward/backward evidence", + "strict platform core with separate multi-rank AG/RS forward/backward evidence", ], "thresholds": { "out_max_abs": args.out_atol, @@ -153,7 +152,7 @@ def main(argv: Sequence[str] | None = None) -> int: ) pytorch_reference = run_decode_full_prefill_reference(reference_inputs) reference = ( - _run_strict_cuda_reference(inputs, config, plan) + _run_strict_platform_reference(inputs, config, plan) if config.strict_mode else pytorch_reference ) @@ -181,12 +180,9 @@ def main(argv: Sequence[str] | None = None) -> int: "lse": lse_stats, "dlogp": dlogp_stats, } - report["reference_backend"] = ( - "rlkernel.cuda.deterministic_attention" - if config.strict_mode - else "rlkernel.pytorch.full_logical_kv_reference" - ) + report["reference_backend"] = "rlkernel.pytorch.full_logical_kv_reference" if config.strict_mode: + report.update(_strict_execution_report_fields(candidate.provenance)) report["diagnostic_drift_vs_pytorch"] = { "out": _drift_stats(candidate.out, pytorch_reference.out), "lse": _drift_stats(candidate.lse, pytorch_reference.lse), @@ -392,7 +388,7 @@ def _make_inputs(args: argparse.Namespace, device: torch.device) -> DecodeAttent return DecodeAttentionInputs(q=q, k_cache=k_cache, v_cache=v_cache, metadata=metadata) -def _run_strict_cuda_reference( +def _run_strict_platform_reference( inputs: DecodeAttentionInputs, config: FlashInferPagedAttentionConfig, paged_plan: Any, @@ -441,6 +437,28 @@ def _run_strict_cuda_reference( ) +def _strict_execution_report_fields(provenance: dict[str, Any]) -> dict[str, Any]: + """Describe the backend that actually executed, independent of the host running tests.""" + + backend = provenance.get("actual_backend", "unknown") + return { + "target": ( + f"Qwen3-8B TP-local {backend} strict production core; " + "CP transport validated separately" + ), + "reference_backend": backend, + "rope": { + "rope_backend": provenance.get("rope_backend"), + "rope_fusion": provenance.get("rope_fusion"), + "rope_fusion_boundary": provenance.get("rope_fusion_boundary"), + "rope_theta": provenance.get("rope_theta"), + "rotary_dim": provenance.get("rotary_dim"), + "q_rope_state": provenance.get("q_rope_state"), + "k_cache_rope_state": provenance.get("k_cache_rope_state"), + }, + } + + def _run_batch_invariance_sweep( op: FlashInferQwen3PagedAttentionOp, inputs: DecodeAttentionInputs, diff --git a/tests/test_flashinfer_pr7_attention.py b/tests/test_flashinfer_pr7_attention.py index 8bbbdf2a..40ffd3a7 100644 --- a/tests/test_flashinfer_pr7_attention.py +++ b/tests/test_flashinfer_pr7_attention.py @@ -1802,6 +1802,40 @@ def test_pr7_check_accepts_strict_rocm_production_core(): assert check_script._acceptance_errors(report, args) == [] +@pytest.mark.parametrize( + ("backend", "rope_backend"), + [ + ("flash_attention_4.cute", "rlkernel.cuda.rope_sm90"), + ("aiter.rocm.ck_dense_mha", "rlkernel.rocm.deterministic_rope"), + ], +) +def test_strict_report_uses_executed_platform_provenance(backend, rope_backend): + fields = check_script._strict_execution_report_fields( + { + "actual_backend": backend, + "rope_backend": rope_backend, + "rope_fusion": False, + "rope_fusion_boundary": "rlkernel_rope_then_attention", + "rope_theta": 1_000_000.0, + "rotary_dim": 128, + "q_rope_state": "post_rope", + "k_cache_rope_state": "post_rope", + } + ) + + assert fields["reference_backend"] == backend + assert backend in fields["target"] + assert fields["rope"] == { + "rope_backend": rope_backend, + "rope_fusion": False, + "rope_fusion_boundary": "rlkernel_rope_then_attention", + "rope_theta": 1_000_000.0, + "rotary_dim": 128, + "q_rope_state": "post_rope", + "k_cache_rope_state": "post_rope", + } + + def test_pr7_check_rejects_nonfinite_drift_and_wrong_tp_local_shape(): args = check_script._parse_args([]) report = { From 1bc4177e459e8e85c4887df2fcf25fc73c59a21a Mon Sep 17 00:00:00 2001 From: lamentropetion <3051000145@qq.com> Date: Thu, 20 Aug 2026 22:55:52 +0800 Subject: [PATCH 13/35] fix(attention): narrow injected AITER callables --- rl_engine/kernels/ops/rocm/attention/flash_attn.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rl_engine/kernels/ops/rocm/attention/flash_attn.py b/rl_engine/kernels/ops/rocm/attention/flash_attn.py index fff003a2..0a9b2b36 100644 --- a/rl_engine/kernels/ops/rocm/attention/flash_attn.py +++ b/rl_engine/kernels/ops/rocm/attention/flash_attn.py @@ -165,7 +165,9 @@ def __init__( if _mha_fwd is None: mha_fwd, mha_bwd, source_sha256 = _load_aiter_ck_ops() else: - mha_fwd, mha_bwd = _mha_fwd, _mha_bwd + assert _mha_bwd is not None + mha_fwd = _mha_fwd + mha_bwd = _mha_bwd source_sha256 = "test-double" if _source_sha256 is None else _source_sha256 if not callable(mha_fwd) or not callable(mha_bwd): raise StrictRocmAttentionUnavailable("AITER CK MHA entry points are not callable") From 3247e3ef390ac2f55bf93864ffa70b0a1350ec4c Mon Sep 17 00:00:00 2001 From: zhangj1an Date: Mon, 24 Aug 2026 13:36:49 +0000 Subject: [PATCH 14/35] feat(attention): route strict ROCm attention to Vime via contract dispatch 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) Claude-Session: https://claude.ai/code/session_01QfN3b2ep5DvxVDir7J36j3 --- benchmarks/benchmark_rocm_attention.py | 386 ++++++++++++++++ .../results/pr319_rocm_mi300x/report.md | 80 ++++ .../results/pr319_rocm_mi300x/results.json | 373 +++++++++++++++ rl_engine/integrations/__init__.py | 4 + rl_engine/integrations/vime/__init__.py | 12 + rl_engine/integrations/vime/attention.py | 428 ++++++++++++++++++ rl_engine/kernels/attention_contract.py | 37 ++ .../kernels/ops/rocm/attention/flash_attn.py | 7 +- rl_engine/kernels/registry.py | 254 +++++++++++ tests/test_attention_dispatch.py | 307 +++++++++++++ tests/test_vime_attention_provider.py | 272 +++++++++++ 11 files changed, 2159 insertions(+), 1 deletion(-) create mode 100644 benchmarks/benchmark_rocm_attention.py create mode 100644 benchmarks/results/pr319_rocm_mi300x/report.md create mode 100644 benchmarks/results/pr319_rocm_mi300x/results.json create mode 100644 rl_engine/integrations/__init__.py create mode 100644 rl_engine/integrations/vime/__init__.py create mode 100644 rl_engine/integrations/vime/attention.py create mode 100644 tests/test_attention_dispatch.py create mode 100644 tests/test_vime_attention_provider.py diff --git a/benchmarks/benchmark_rocm_attention.py b/benchmarks/benchmark_rocm_attention.py new file mode 100644 index 00000000..addaa553 --- /dev/null +++ b/benchmarks/benchmark_rocm_attention.py @@ -0,0 +1,386 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2026 RL-Kernel Contributors + +"""Operator-only benchmark for the strict ROCm attention path. + +Seeded Q/K/V only: no checkpoint, tokenizer, or serving engine. Defaults to the +Qwen3-8B dense head layout (``Hq=32``, ``Hkv=8``, ``D=128``), BF16, causal +prefill. + +Three backends are compared: + +``native`` + ``torch.nn.functional.scaled_dot_product_attention`` with the KV heads + expanded to the Q head count. Not batch-invariant; present as the + throughput reference every ROCm deployment already has. +``triton`` + ``flash_attn`` with the ROCm Triton backend enabled. +``strict`` + ``aiter.rocm.ck_dense_mha`` through the WS2 contract dispatch, i.e. the path + this PR adds. + +Two extra sections quantify what the strict contract actually costs, because +both are properties of the vendor backward rather than of the integration: + +* ``determinism_cost`` — AITER ``mha_bwd`` with ``deterministic`` on vs off. +* ``batch_composition`` — whether raw AITER returns the same bits for a batch + and for the same rows submitted one at a time, swept over shapes because the + answer varies with shape. +""" + +from __future__ import annotations + +import argparse +import json +import math +import statistics +from pathlib import Path +from typing import Any, Callable + +import torch + +DEFAULT_Q_HEADS = 32 +DEFAULT_KV_HEADS = 8 +DEFAULT_HEAD_DIM = 128 + + +class _ContextParallel: + rank = 0 + world_size = 1 + layout = "single" + + +class _Request: + """Structural request understood by the Vime attention provider.""" + + def __init__(self, query, key, value, metadata): + self.query = query + self.key = key + self.value = value + self.metadata = metadata + self.context_parallel = _ContextParallel() + self.tensor_parallel_group = None + self.key_padding_mask = None + + +def _metadata(q_heads: int, kv_heads: int) -> dict[str, Any]: + return { + "global_q_heads": q_heads, + "global_kv_heads": kv_heads, + "tp_rank": 0, + "tp_world_size": 1, + "attention_mode": "prefill", + "role": "train", + "causal": True, + } + + +def _tensors(batch, q_heads, kv_heads, seq_len, head_dim, dtype, seed=0): + generator = torch.Generator(device="cuda").manual_seed(seed) + query = torch.randn( + batch, q_heads, seq_len, head_dim, generator=generator, device="cuda", dtype=dtype + ) + key = torch.randn( + batch, kv_heads, seq_len, head_dim, generator=generator, device="cuda", dtype=dtype + ) + value = torch.randn( + batch, kv_heads, seq_len, head_dim, generator=generator, device="cuda", dtype=dtype + ) + return query, key, value + + +def _measure(run: Callable[[bool], None], *, backward: bool, warmup: int, iters: int): + torch.cuda.synchronize() + torch.cuda.empty_cache() + torch.cuda.reset_peak_memory_stats() + for _ in range(warmup): + run(backward) + torch.cuda.synchronize() + samples: list[float] = [] + for _ in range(iters): + start = torch.cuda.Event(enable_timing=True) + end = torch.cuda.Event(enable_timing=True) + start.record() + run(backward) + end.record() + torch.cuda.synchronize() + samples.append(start.elapsed_time(end)) + samples.sort() + return { + "median_ms": round(statistics.median(samples), 4), + "p95_ms": round(samples[int(0.95 * (len(samples) - 1))], 4), + "peak_mib": round(torch.cuda.max_memory_allocated() / 2**20, 1), + } + + +def _native_runner(query, key, value, q_heads, kv_heads): + repeats = q_heads // kv_heads + + def run(backward: bool) -> None: + q = query.detach().requires_grad_(backward) + k = key.detach().requires_grad_(backward) + v = value.detach().requires_grad_(backward) + out = torch.nn.functional.scaled_dot_product_attention( + q, + k.repeat_interleave(repeats, dim=1), + v.repeat_interleave(repeats, dim=1), + is_causal=True, + ) + if backward: + out.backward(torch.ones_like(out)) + + return run + + +def _triton_runner(query, key, value, head_dim): + import os + + os.environ["FLASH_ATTENTION_TRITON_AMD_ENABLE"] = "TRUE" + from flash_attn import flash_attn_func + + scale = 1.0 / math.sqrt(head_dim) + + def run(backward: bool) -> None: + q = query.detach().transpose(1, 2).contiguous().requires_grad_(backward) + k = key.detach().transpose(1, 2).contiguous().requires_grad_(backward) + v = value.detach().transpose(1, 2).contiguous().requires_grad_(backward) + out = flash_attn_func(q, k, v, dropout_p=0.0, softmax_scale=scale, causal=True) + if backward: + out.backward(torch.ones_like(out)) + + return run + + +def _strict_runner(query, key, value, metadata): + from rl_engine.integrations.vime import attention_provider + + def run(backward: bool) -> None: + q = query.detach().requires_grad_(backward) + k = key.detach().requires_grad_(backward) + v = value.detach().requires_grad_(backward) + result = attention_provider(_Request(q, k, v, metadata)) + if backward: + result.out.backward(torch.ones_like(result.out)) + + return run + + +def _determinism_cost(seq_lens, q_heads, kv_heads, head_dim, dtype, warmup, iters): + """AITER deterministic backward vs the non-deterministic one.""" + + from aiter.ops.mha import mha_bwd, mha_fwd + + scale = 1.0 / math.sqrt(head_dim) + rows = [] + for seq_len in seq_lens: + query, key, value = _tensors(1, q_heads, kv_heads, seq_len, head_dim, dtype) + # AITER consumes [B, S, H, D]. + q = query.transpose(1, 2).contiguous() + k = key.transpose(1, 2).contiguous() + v = value.transpose(1, 2).contiguous() + out, lse, _mask, rng_state = mha_fwd(q, k, v, 0.0, scale, True, -1, -1, 0, True, False) + grad_out = torch.ones_like(out) + entry: dict[str, Any] = {"seq_len": seq_len} + for deterministic in (True, False): + + # Bind the tensors as defaults: they are released below, and the + # closure must not depend on the enclosing names still existing. + def run( + _backward: bool, + deterministic=deterministic, + grad_out=grad_out, + q=q, + k=k, + v=v, + out=out, + lse=lse, + rng_state=rng_state, + ) -> None: + mha_bwd( + grad_out, + q, + k, + v, + out, + lse, + 0.0, + scale, + True, + -1, + -1, + deterministic, + rng_state=rng_state, + ) + + key_name = "deterministic" if deterministic else "non_deterministic" + entry[key_name] = _measure(run, backward=False, warmup=warmup, iters=iters) + entry["time_ratio"] = round( + entry["deterministic"]["median_ms"] / entry["non_deterministic"]["median_ms"], 2 + ) + entry["memory_ratio"] = round( + entry["deterministic"]["peak_mib"] / entry["non_deterministic"]["peak_mib"], 1 + ) + rows.append(entry) + del query, key, value, q, k, v, out, lse, grad_out + torch.cuda.empty_cache() + return rows + + +def _batch_composition(q_heads, kv_heads, head_dim, dtype, shapes): + """Whether raw AITER is batch-composition invariant, swept over shapes. + + The strict core sidesteps this by executing one logical row at a time; this + section records what the vendor kernel does without that constraint. The + sweep matters: invariance holds for most shapes and breaks for a few, so a + single-shape probe would report whichever answer it happened to land on. + """ + + from aiter.ops.mha import mha_fwd + + scale = 1.0 / math.sqrt(head_dim) + + def forward(query, key, value): + out, lse, _mask, _rng = mha_fwd( + query.transpose(1, 2).contiguous(), + key.transpose(1, 2).contiguous(), + value.transpose(1, 2).contiguous(), + 0.0, + scale, + True, + -1, + -1, + 0, + True, + False, + ) + return out.transpose(1, 2).contiguous(), lse + + rows = [] + for batch, seq_len in shapes: + if batch < 2: + continue + query, key, value = _tensors(batch, q_heads, kv_heads, seq_len, head_dim, dtype, seed=11) + batched_out, batched_lse = forward(query, key, value) + worst_out = worst_lse = 0.0 + for row in range(batch): + row_out, row_lse = forward( + query[row : row + 1], key[row : row + 1], value[row : row + 1] + ) + worst_out = max(worst_out, (batched_out[row : row + 1] - row_out).abs().max().item()) + worst_lse = max(worst_lse, (batched_lse[row : row + 1] - row_lse).abs().max().item()) + rows.append( + { + "batch": batch, + "seq_len": seq_len, + "raw_aiter_out_max_abs": worst_out, + "raw_aiter_lse_max_abs": worst_lse, + "raw_aiter_is_batch_invariant": worst_out == 0.0 and worst_lse == 0.0, + } + ) + print( + f"batch-composition B={batch} S={seq_len:5d} " + f"out {worst_out:.6e} lse {worst_lse:.6e} " + f"{'invariant' if rows[-1]['raw_aiter_is_batch_invariant'] else 'NOT INVARIANT'}", + flush=True, + ) + del query, key, value, batched_out, batched_lse + torch.cuda.empty_cache() + return rows + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output", type=Path, default=Path("attention_results.json")) + parser.add_argument("--q-heads", type=int, default=DEFAULT_Q_HEADS) + parser.add_argument("--kv-heads", type=int, default=DEFAULT_KV_HEADS) + parser.add_argument("--head-dim", type=int, default=DEFAULT_HEAD_DIM) + parser.add_argument("--warmup", type=int, default=10) + parser.add_argument("--iters", type=int, default=50) + parser.add_argument( + "--shapes", + default="1x1024,1x2048,1x4096,2x2048,4x2048", + help="comma-separated BATCHxSEQ pairs", + ) + args = parser.parse_args() + + if not torch.cuda.is_available(): + raise SystemExit("this benchmark requires a ROCm (or CUDA) device") + + dtype = torch.bfloat16 + metadata = _metadata(args.q_heads, args.kv_heads) + shapes = [tuple(int(part) for part in pair.split("x")) for pair in args.shapes.split(",")] + + rows = [] + for batch, seq_len in shapes: + query, key, value = _tensors( + batch, args.q_heads, args.kv_heads, seq_len, args.head_dim, dtype + ) + factories = { + "native": lambda q=query, k=key, v=value: _native_runner( + q, k, v, args.q_heads, args.kv_heads + ), + "triton": lambda q=query, k=key, v=value: _triton_runner(q, k, v, args.head_dim), + "strict": lambda q=query, k=key, v=value: _strict_runner(q, k, v, metadata), + } + for name, factory in factories.items(): + try: + runner = factory() + forward = _measure(runner, backward=False, warmup=args.warmup, iters=args.iters) + combined = _measure(runner, backward=True, warmup=args.warmup, iters=args.iters) + except Exception as exc: # noqa: BLE001 - report, do not abort the sweep + print(f"B={batch} S={seq_len} {name}: FAILED {type(exc).__name__}: {exc}") + continue + rows.append( + { + "batch": batch, + "seq_len": seq_len, + "backend": name, + "forward": forward, + "forward_backward": combined, + } + ) + print( + f"B={batch} S={seq_len:5d} {name:8s} " + f"fwd {forward['median_ms']:8.4f} p95 {forward['p95_ms']:8.4f} " + f"peak {forward['peak_mib']:9.1f} | " + f"fwd+bwd {combined['median_ms']:8.4f} peak {combined['peak_mib']:9.1f}", + flush=True, + ) + del query, key, value + torch.cuda.empty_cache() + + seq_lens = sorted({seq for _batch, seq in shapes}) + determinism = _determinism_cost( + seq_lens, args.q_heads, args.kv_heads, args.head_dim, dtype, args.warmup, args.iters + ) + composition_shapes = [ + (batch, seq_len) for batch in (2, 4) for seq_len in sorted({128, 256, 512, *seq_lens}) + ] + composition = _batch_composition( + args.q_heads, args.kv_heads, args.head_dim, dtype, composition_shapes + ) + + properties = torch.cuda.get_device_properties(0) + payload = { + "environment": { + "gpu": properties.name, + "arch": getattr(properties, "gcnArchName", "unknown"), + "device_count": torch.cuda.device_count(), + "torch": torch.__version__, + "hip": torch.version.hip, + "dtype": "bf16", + "q_heads": args.q_heads, + "kv_heads": args.kv_heads, + "head_dim": args.head_dim, + }, + "latency": rows, + "determinism_cost": determinism, + "batch_composition": composition, + } + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(json.dumps(payload, indent=2)) + print("wrote", args.output) + + +if __name__ == "__main__": + main() diff --git a/benchmarks/results/pr319_rocm_mi300x/report.md b/benchmarks/results/pr319_rocm_mi300x/report.md new file mode 100644 index 00000000..e5cbe016 --- /dev/null +++ b/benchmarks/results/pr319_rocm_mi300x/report.md @@ -0,0 +1,80 @@ +# PR #319 — strict ROCm attention, MI300X operator benchmark + +Operator-only: seeded Q/K/V, no checkpoint, tokenizer, or serving engine. + +| Field | Value | +|---|---| +| `gpu` | AMD Instinct MI300X | +| `arch` | gfx942:sramecc+:xnack- | +| `device_count` | 8 | +| `torch` | 2.12.0+rocm7.14.0a20260608 | +| `hip` | 7.14.60850 | +| `dtype` | bf16 | +| `q_heads` | 32 | +| `kv_heads` | 8 | +| `head_dim` | 128 | + +Backends: `native` = PyTorch SDPA with KV heads expanded; `triton` = flash-attn ROCm +Triton backend; `strict` = `aiter.rocm.ck_dense_mha` through WS2 contract dispatch. + +## Latency and peak memory (BF16, causal prefill) + +| B | S | Backend | Fwd median (ms) | Fwd p95 (ms) | Fwd peak MiB | Fwd+bwd median (ms) | Fwd+bwd peak MiB | +|---:|---:|---|---:|---:|---:|---:|---:| +| 1 | 1024 | native | 0.1438 | 0.1668 | 36.1 | 0.4741 | 84.3 | +| 1 | 1024 | triton | 0.3069 | 0.3300 | 32.1 | 1.7754 | 52.2 | +| 1 | 1024 | strict | 0.4045 | 0.4261 | 40.1 | 1.0480 | 1108.5 | +| 1 | 2048 | native | 0.2963 | 0.3018 | 72.3 | 1.0880 | 168.8 | +| 1 | 2048 | triton | 0.6404 | 0.6551 | 64.2 | 2.2759 | 104.5 | +| 1 | 2048 | strict | 0.4327 | 0.4475 | 80.3 | 2.0983 | 4265.0 | +| 1 | 4096 | native | 0.7014 | 0.7219 | 144.5 | 3.2456 | 337.5 | +| 1 | 4096 | triton | 1.4337 | 1.4886 | 128.5 | 4.6235 | 209.0 | +| 1 | 4096 | strict | 0.7415 | 0.7529 | 160.5 | 5.8933 | 16722.0 | +| 2 | 2048 | native | 0.4437 | 0.4696 | 144.5 | 1.9135 | 337.5 | +| 2 | 2048 | triton | 0.9821 | 0.9917 | 128.5 | 2.6656 | 209.0 | +| 2 | 2048 | strict | 0.7912 | 0.8101 | 120.5 | 3.9666 | 4369.3 | +| 4 | 2048 | native | 0.8211 | 0.8666 | 289.0 | 3.5754 | 675.0 | +| 4 | 2048 | triton | 1.7571 | 1.7957 | 257.0 | 5.1310 | 418.0 | +| 4 | 2048 | strict | 1.3856 | 1.4119 | 226.1 | 7.7189 | 4610.3 | + +## Cost of the deterministic backward + +Raw AITER `mha_bwd`, B=1, toggling only the `deterministic` flag. Peak values include the +already-resident forward tensors, so read the absolute det=on column for scaling. + +| S | det=on median (ms) | det=off median (ms) | Time | det=on peak MiB | det=off peak MiB | +|---:|---:|---:|---:|---:|---:| +| 1024 | 0.4455 | 0.1917 | 2.32x | 1188.3 | 180.3 | +| 2048 | 1.3399 | 0.5687 | 2.36x | 4328.5 | 264.5 | +| 4096 | 5.0313 | 2.0607 | 2.44x | 16753.0 | 433.0 | + +Deterministic backward peak memory scales as O(S^2): each doubling of S quadruples it. + +## Batch-composition invariance of raw AITER + +Batch vs the same rows submitted one at a time. Shape-dependent, which is why the strict +core materializes one logical row per call. + +| B | S | out max abs | lse max abs | Invariant | +|---:|---:|---:|---:|---| +| 2 | 128 | 0.000000e+00 | 0.000000e+00 | yes | +| 2 | 256 | 0.000000e+00 | 0.000000e+00 | yes | +| 2 | 512 | 1.562500e-02 | 9.536743e-07 | **no** | +| 2 | 1024 | 0.000000e+00 | 0.000000e+00 | yes | +| 2 | 2048 | 0.000000e+00 | 0.000000e+00 | yes | +| 2 | 4096 | 0.000000e+00 | 0.000000e+00 | yes | +| 4 | 128 | 0.000000e+00 | 0.000000e+00 | yes | +| 4 | 256 | 7.812500e-03 | 9.536743e-07 | **no** | +| 4 | 512 | 1.562500e-02 | 9.536743e-07 | **no** | +| 4 | 1024 | 0.000000e+00 | 0.000000e+00 | yes | +| 4 | 2048 | 0.000000e+00 | 0.000000e+00 | yes | +| 4 | 4096 | 0.000000e+00 | 0.000000e+00 | yes | + +Through the provider, every shape above is bitwise identical (max abs `0`). + +## Reproduce + +```bash +python benchmarks/benchmark_rocm_attention.py \ + --output benchmarks/results/pr319_rocm_mi300x/results.json +``` diff --git a/benchmarks/results/pr319_rocm_mi300x/results.json b/benchmarks/results/pr319_rocm_mi300x/results.json new file mode 100644 index 00000000..7aec1877 --- /dev/null +++ b/benchmarks/results/pr319_rocm_mi300x/results.json @@ -0,0 +1,373 @@ +{ + "environment": { + "gpu": "AMD Instinct MI300X", + "arch": "gfx942:sramecc+:xnack-", + "device_count": 8, + "torch": "2.12.0+rocm7.14.0a20260608", + "hip": "7.14.60850", + "dtype": "bf16", + "q_heads": 32, + "kv_heads": 8, + "head_dim": 128 + }, + "latency": [ + { + "batch": 1, + "seq_len": 1024, + "backend": "native", + "forward": { + "median_ms": 0.1438, + "p95_ms": 0.1668, + "peak_mib": 36.1 + }, + "forward_backward": { + "median_ms": 0.4741, + "p95_ms": 0.4915, + "peak_mib": 84.3 + } + }, + { + "batch": 1, + "seq_len": 1024, + "backend": "triton", + "forward": { + "median_ms": 0.3069, + "p95_ms": 0.33, + "peak_mib": 32.1 + }, + "forward_backward": { + "median_ms": 1.7754, + "p95_ms": 1.864, + "peak_mib": 52.2 + } + }, + { + "batch": 1, + "seq_len": 1024, + "backend": "strict", + "forward": { + "median_ms": 0.4045, + "p95_ms": 0.4261, + "peak_mib": 40.1 + }, + "forward_backward": { + "median_ms": 1.048, + "p95_ms": 1.0706, + "peak_mib": 1108.5 + } + }, + { + "batch": 1, + "seq_len": 2048, + "backend": "native", + "forward": { + "median_ms": 0.2963, + "p95_ms": 0.3018, + "peak_mib": 72.3 + }, + "forward_backward": { + "median_ms": 1.088, + "p95_ms": 1.0986, + "peak_mib": 168.8 + } + }, + { + "batch": 1, + "seq_len": 2048, + "backend": "triton", + "forward": { + "median_ms": 0.6404, + "p95_ms": 0.6551, + "peak_mib": 64.2 + }, + "forward_backward": { + "median_ms": 2.2759, + "p95_ms": 2.2853, + "peak_mib": 104.5 + } + }, + { + "batch": 1, + "seq_len": 2048, + "backend": "strict", + "forward": { + "median_ms": 0.4327, + "p95_ms": 0.4475, + "peak_mib": 80.3 + }, + "forward_backward": { + "median_ms": 2.0983, + "p95_ms": 2.2269, + "peak_mib": 4265.0 + } + }, + { + "batch": 1, + "seq_len": 4096, + "backend": "native", + "forward": { + "median_ms": 0.7014, + "p95_ms": 0.7219, + "peak_mib": 144.5 + }, + "forward_backward": { + "median_ms": 3.2456, + "p95_ms": 3.2945, + "peak_mib": 337.5 + } + }, + { + "batch": 1, + "seq_len": 4096, + "backend": "triton", + "forward": { + "median_ms": 1.4337, + "p95_ms": 1.4886, + "peak_mib": 128.5 + }, + "forward_backward": { + "median_ms": 4.6235, + "p95_ms": 4.672, + "peak_mib": 209.0 + } + }, + { + "batch": 1, + "seq_len": 4096, + "backend": "strict", + "forward": { + "median_ms": 0.7415, + "p95_ms": 0.7529, + "peak_mib": 160.5 + }, + "forward_backward": { + "median_ms": 5.8933, + "p95_ms": 5.9776, + "peak_mib": 16722.0 + } + }, + { + "batch": 2, + "seq_len": 2048, + "backend": "native", + "forward": { + "median_ms": 0.4437, + "p95_ms": 0.4696, + "peak_mib": 144.5 + }, + "forward_backward": { + "median_ms": 1.9135, + "p95_ms": 1.9372, + "peak_mib": 337.5 + } + }, + { + "batch": 2, + "seq_len": 2048, + "backend": "triton", + "forward": { + "median_ms": 0.9821, + "p95_ms": 0.9917, + "peak_mib": 128.5 + }, + "forward_backward": { + "median_ms": 2.6656, + "p95_ms": 2.6806, + "peak_mib": 209.0 + } + }, + { + "batch": 2, + "seq_len": 2048, + "backend": "strict", + "forward": { + "median_ms": 0.7912, + "p95_ms": 0.8101, + "peak_mib": 120.5 + }, + "forward_backward": { + "median_ms": 3.9666, + "p95_ms": 4.0203, + "peak_mib": 4369.3 + } + }, + { + "batch": 4, + "seq_len": 2048, + "backend": "native", + "forward": { + "median_ms": 0.8211, + "p95_ms": 0.8666, + "peak_mib": 289.0 + }, + "forward_backward": { + "median_ms": 3.5754, + "p95_ms": 3.6514, + "peak_mib": 675.0 + } + }, + { + "batch": 4, + "seq_len": 2048, + "backend": "triton", + "forward": { + "median_ms": 1.7571, + "p95_ms": 1.7957, + "peak_mib": 257.0 + }, + "forward_backward": { + "median_ms": 5.131, + "p95_ms": 5.1587, + "peak_mib": 418.0 + } + }, + { + "batch": 4, + "seq_len": 2048, + "backend": "strict", + "forward": { + "median_ms": 1.3856, + "p95_ms": 1.4119, + "peak_mib": 226.1 + }, + "forward_backward": { + "median_ms": 7.7189, + "p95_ms": 7.8018, + "peak_mib": 4610.3 + } + } + ], + "determinism_cost": [ + { + "seq_len": 1024, + "deterministic": { + "median_ms": 0.4455, + "p95_ms": 0.4746, + "peak_mib": 1188.3 + }, + "non_deterministic": { + "median_ms": 0.1917, + "p95_ms": 0.1971, + "peak_mib": 180.3 + }, + "time_ratio": 2.32, + "memory_ratio": 6.6 + }, + { + "seq_len": 2048, + "deterministic": { + "median_ms": 1.3399, + "p95_ms": 1.3742, + "peak_mib": 4328.5 + }, + "non_deterministic": { + "median_ms": 0.5687, + "p95_ms": 0.5762, + "peak_mib": 264.5 + }, + "time_ratio": 2.36, + "memory_ratio": 16.4 + }, + { + "seq_len": 4096, + "deterministic": { + "median_ms": 5.0313, + "p95_ms": 5.1068, + "peak_mib": 16753.0 + }, + "non_deterministic": { + "median_ms": 2.0607, + "p95_ms": 2.089, + "peak_mib": 433.0 + }, + "time_ratio": 2.44, + "memory_ratio": 38.7 + } + ], + "batch_composition": [ + { + "batch": 2, + "seq_len": 128, + "raw_aiter_out_max_abs": 0.0, + "raw_aiter_lse_max_abs": 0.0, + "raw_aiter_is_batch_invariant": true + }, + { + "batch": 2, + "seq_len": 256, + "raw_aiter_out_max_abs": 0.0, + "raw_aiter_lse_max_abs": 0.0, + "raw_aiter_is_batch_invariant": true + }, + { + "batch": 2, + "seq_len": 512, + "raw_aiter_out_max_abs": 0.015625, + "raw_aiter_lse_max_abs": 9.5367431640625e-07, + "raw_aiter_is_batch_invariant": false + }, + { + "batch": 2, + "seq_len": 1024, + "raw_aiter_out_max_abs": 0.0, + "raw_aiter_lse_max_abs": 0.0, + "raw_aiter_is_batch_invariant": true + }, + { + "batch": 2, + "seq_len": 2048, + "raw_aiter_out_max_abs": 0.0, + "raw_aiter_lse_max_abs": 0.0, + "raw_aiter_is_batch_invariant": true + }, + { + "batch": 2, + "seq_len": 4096, + "raw_aiter_out_max_abs": 0.0, + "raw_aiter_lse_max_abs": 0.0, + "raw_aiter_is_batch_invariant": true + }, + { + "batch": 4, + "seq_len": 128, + "raw_aiter_out_max_abs": 0.0, + "raw_aiter_lse_max_abs": 0.0, + "raw_aiter_is_batch_invariant": true + }, + { + "batch": 4, + "seq_len": 256, + "raw_aiter_out_max_abs": 0.0078125, + "raw_aiter_lse_max_abs": 9.5367431640625e-07, + "raw_aiter_is_batch_invariant": false + }, + { + "batch": 4, + "seq_len": 512, + "raw_aiter_out_max_abs": 0.015625, + "raw_aiter_lse_max_abs": 9.5367431640625e-07, + "raw_aiter_is_batch_invariant": false + }, + { + "batch": 4, + "seq_len": 1024, + "raw_aiter_out_max_abs": 0.0, + "raw_aiter_lse_max_abs": 0.0, + "raw_aiter_is_batch_invariant": true + }, + { + "batch": 4, + "seq_len": 2048, + "raw_aiter_out_max_abs": 0.0, + "raw_aiter_lse_max_abs": 0.0, + "raw_aiter_is_batch_invariant": true + }, + { + "batch": 4, + "seq_len": 4096, + "raw_aiter_out_max_abs": 0.0, + "raw_aiter_lse_max_abs": 0.0, + "raw_aiter_is_batch_invariant": true + } + ] +} \ No newline at end of file diff --git a/rl_engine/integrations/__init__.py b/rl_engine/integrations/__init__.py new file mode 100644 index 00000000..ec3e322c --- /dev/null +++ b/rl_engine/integrations/__init__.py @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2026 RL-Kernel Contributors + +"""Optional framework adapters owned by RL-Kernel.""" diff --git a/rl_engine/integrations/vime/__init__.py b/rl_engine/integrations/vime/__init__.py new file mode 100644 index 00000000..7e4aa140 --- /dev/null +++ b/rl_engine/integrations/vime/__init__.py @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2026 RL-Kernel Contributors + +"""Vime adapter entry points without a Vime runtime dependency.""" + +from .attention import AttentionProviderResult, AttentionProviderUnavailable, attention_provider + +__all__ = [ + "AttentionProviderResult", + "AttentionProviderUnavailable", + "attention_provider", +] diff --git a/rl_engine/integrations/vime/attention.py b/rl_engine/integrations/vime/attention.py new file mode 100644 index 00000000..c848b2d7 --- /dev/null +++ b/rl_engine/integrations/vime/attention.py @@ -0,0 +1,428 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2026 RL-Kernel Contributors + +"""Runtime strict-attention provider for Vime's Megatron backend. + +The adapter intentionally accepts and returns structural objects: RL-Kernel +never imports Vime. Vime remains responsible for materializing post-RoPE Q/K/V +for its locally owned rows; this provider owns only the attention core +arithmetic and the ``(out, lse)`` export. + +Two boundaries are deliberate and are enforced rather than documented: + +* The core executes one logical batch row at a time. Batching is a layout + choice, so a request of ``B`` rows must produce the same bits as ``B`` + single-row requests; the provider materializes rows independently instead of + trusting a vendor kernel to be batch-composition invariant. +* CP is not a merge axis here. The strict ROCm core owns single-rank attention + arithmetic only; the cross-rank ``(out, lse)`` merge lives in the CP transport + path. CP rank and layout are recorded as row-ownership provenance and a + request that asks this provider to perform the CP merge fails closed. +""" + +from __future__ import annotations + +import math +from collections.abc import Mapping +from dataclasses import dataclass +from typing import Any + +import torch + +from rl_engine.kernels.attention_contract import ( + AttentionContract, + AttentionDType, + AttentionMode, + AttentionRole, + ReductionSpec, + ShardingSpec, + SplitKVSpec, +) +from rl_engine.kernels.ops.rocm.attention.flash_attn import BACKEND_ID +from rl_engine.kernels.registry import kernel_registry + + +class AttentionProviderUnavailable(RuntimeError): + """Request Vime's native attention fallback in ``auto`` mode. + + Vime recognizes the marker instead of importing this class, which keeps the + dependency direction from Vime to RL-Kernel at runtime only. + """ + + attention_provider_unavailable = True + + +@dataclass(frozen=True) +class AttentionProviderResult: + """Structural result understood by the Vime attention boundary.""" + + out: torch.Tensor + lse: torch.Tensor + backend_id: str + contract_id: str + provenance: Mapping[str, Any] + + +_DTYPE_TO_CONTRACT = { + torch.bfloat16: AttentionDType.BF16, + torch.float16: AttentionDType.FP16, +} + +# Decode is deliberately absent: it requires KV-cache identity metadata that +# this core does not materialize. A decode request fails closed here rather +# than being silently served by the dense prefill core over a cache the +# provider never validated. +_MODE_BY_NAME = { + "prefill": AttentionMode.PREFILL, + "chunked_prefill": AttentionMode.CHUNKED_PREFILL, +} + +_ROLE_BY_NAME = { + "train": AttentionRole.TRAIN, + "infer": AttentionRole.INFER, +} + + +def _as_positive_int(value: Any, name: str) -> int: + if isinstance(value, bool) or not isinstance(value, int) or value <= 0: + raise AttentionProviderUnavailable(f"{name} must be a positive integer; got {value!r}") + return value + + +def _metadata(request: Any) -> Mapping[str, Any]: + value = getattr(request, "metadata", None) + if not isinstance(value, Mapping): + raise AttentionProviderUnavailable("request.metadata must provide attention metadata") + return value + + +def _request_tensor(request: Any, name: str) -> torch.Tensor: + value = getattr(request, name, None) + if not isinstance(value, torch.Tensor): + raise AttentionProviderUnavailable(f"request.{name} must be a torch.Tensor") + return value + + +def _tp_coordinates(tp_group: Any) -> tuple[int, int]: + if tp_group is not None and hasattr(tp_group, "rank") and hasattr(tp_group, "size"): + return int(tp_group.rank()), int(tp_group.size()) + + import torch.distributed as dist + + if dist.is_available() and dist.is_initialized(): + return dist.get_rank(group=tp_group), dist.get_world_size(group=tp_group) + return 0, 1 + + +def _reject_unsupported_materializations(request: Any, metadata: Mapping[str, Any]) -> None: + """Fail closed on every knob that would change the numerical definition.""" + + if getattr(request, "key_padding_mask", None) is not None: + raise AttentionProviderUnavailable( + "strict ROCm attention materializes each unpadded logical row; " + "pass unpadded per-row Q/K/V instead of a key padding mask" + ) + dropout_p = metadata.get("dropout_p", 0.0) + if dropout_p: + raise AttentionProviderUnavailable( + f"strict attention requires dropout_p=0.0; got {dropout_p!r}" + ) + for unsupported in ( + "alibi_slopes", + "attention_bias", + "logit_soft_cap", + "sliding_window", + "sink_tokens", + ): + if metadata.get(unsupported) is not None: + raise AttentionProviderUnavailable( + f"strict ROCm attention does not materialize {unsupported}" + ) + window = metadata.get("window_size") + if window is not None and tuple(window) != (-1, -1): + raise AttentionProviderUnavailable( + f"strict ROCm attention requires a full causal/full window; got {window!r}" + ) + + +def _contract_for_request( + request: Any, +) -> tuple[AttentionContract, float, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: + """Validate the request and derive the explicit WS2 attention contract.""" + + metadata = _metadata(request) + _reject_unsupported_materializations(request, metadata) + + query = _request_tensor(request, "query") + key = _request_tensor(request, "key") + value = _request_tensor(request, "value") + if query.ndim != 4 or key.ndim != 4 or value.ndim != 4: + raise AttentionProviderUnavailable("query/key/value must be 4-D [B, H, S, D] tensors") + if key.shape != value.shape: + raise AttentionProviderUnavailable("key and value must share one shape") + if query.shape[0] != key.shape[0]: + raise AttentionProviderUnavailable("query and key must share the logical batch size") + if query.shape[-1] != key.shape[-1]: + raise AttentionProviderUnavailable("query and key must share head_dim") + if query.dtype not in _DTYPE_TO_CONTRACT: + raise AttentionProviderUnavailable( + f"strict ROCm attention supports BF16/FP16 only; got {query.dtype}" + ) + if key.dtype != query.dtype or value.dtype != query.dtype: + raise AttentionProviderUnavailable("query/key/value must share one dtype") + if not (query.device == key.device == value.device): + raise AttentionProviderUnavailable("query/key/value must share one device") + + batch_size, local_q_heads, query_len, head_dim = query.shape + local_kv_heads = key.shape[1] + kv_len = key.shape[2] + if local_q_heads % local_kv_heads: + raise AttentionProviderUnavailable( + f"local Q heads={local_q_heads} must be divisible by local KV heads={local_kv_heads}" + ) + + cp = getattr(request, "context_parallel", None) + cp_world_size = _as_positive_int(getattr(cp, "world_size", None), "context_parallel.world_size") + cp_rank = getattr(cp, "rank", None) + if ( + isinstance(cp_rank, bool) + or not isinstance(cp_rank, int) + or not 0 <= cp_rank < cp_world_size + ): + raise AttentionProviderUnavailable( + f"context_parallel.rank={cp_rank!r} is invalid for CP={cp_world_size}" + ) + cp_layout = getattr(cp, "layout", None) + if cp_layout not in ({"single"} if cp_world_size == 1 else {"zigzag", "allgather"}): + raise AttentionProviderUnavailable( + "context_parallel layout does not describe local CP token ownership" + ) + if cp_world_size > 1: + # The strict ROCm core is single-rank attention arithmetic. Performing + # the cross-rank (out, lse) merge here would silently introduce a second + # merge implementation with its own order; the CP transport path owns it. + raise AttentionProviderUnavailable( + f"CP={cp_world_size} requires the strict CP transport path; this provider owns " + "single-rank attention arithmetic and will not perform the cross-rank " + "(out, lse) merge" + ) + + tp_rank, tp_world_size = _tp_coordinates(getattr(request, "tensor_parallel_group", None)) + declared_tp_rank = metadata.get("tp_rank") + declared_tp_world_size = metadata.get("tp_world_size") + if declared_tp_rank is not None and declared_tp_rank != tp_rank: + raise AttentionProviderUnavailable( + f"metadata tp_rank={declared_tp_rank} disagrees with TP group rank={tp_rank}" + ) + if declared_tp_world_size is not None and declared_tp_world_size != tp_world_size: + raise AttentionProviderUnavailable( + f"metadata tp_world_size={declared_tp_world_size} disagrees with " + f"TP group size={tp_world_size}" + ) + + global_q_heads = _as_positive_int(metadata.get("global_q_heads"), "global_q_heads") + global_kv_heads = _as_positive_int(metadata.get("global_kv_heads"), "global_kv_heads") + if local_q_heads * tp_world_size != global_q_heads: + raise AttentionProviderUnavailable( + "local Q heads and TP group do not cover global_q_heads exactly: " + f"{local_q_heads} * {tp_world_size} != {global_q_heads}" + ) + if local_kv_heads * tp_world_size != global_kv_heads: + raise AttentionProviderUnavailable( + "local KV heads and TP group do not cover global_kv_heads exactly: " + f"{local_kv_heads} * {tp_world_size} != {global_kv_heads}" + ) + + mode_name = str(metadata.get("attention_mode", "prefill")) + if mode_name == "decode": + raise AttentionProviderUnavailable( + "decode requires KV-cache identity metadata (cache_position, block table, " + "prefix-cache key) that the strict dense core does not materialize; use the " + "paged decode path" + ) + if mode_name not in _MODE_BY_NAME: + raise AttentionProviderUnavailable(f"unsupported attention_mode={mode_name!r}") + mode = _MODE_BY_NAME[mode_name] + role_name = str(metadata.get("role", "train")) + if role_name not in _ROLE_BY_NAME: + raise AttentionProviderUnavailable(f"unsupported role={role_name!r}") + role = _ROLE_BY_NAME[role_name] + + causal = metadata.get("causal", True) + if not isinstance(causal, bool): + raise AttentionProviderUnavailable(f"causal must be a bool; got {causal!r}") + if mode is AttentionMode.PREFILL and query_len != kv_len: + raise AttentionProviderUnavailable( + "prefill requires the query and KV lengths to describe one logical sequence; " + f"got Sq={query_len} and Skv={kv_len}" + ) + + if query_len > kv_len: + raise AttentionProviderUnavailable( + f"causal attention requires Sq <= Skv; got Sq={query_len} and Skv={kv_len}" + ) + + scale = metadata.get("softmax_scale") + resolved_scale = 1.0 / math.sqrt(head_dim) if scale is None else float(scale) + + # CP=1: this rank owns the whole logical sequence as one block. + sharding = ShardingSpec( + tp_rank=tp_rank, + tp_world_size=tp_world_size, + cp_rank=cp_rank, + cp_world_size=cp_world_size, + global_q_heads=global_q_heads, + global_kv_heads=global_kv_heads, + local_q_head_start=tp_rank * local_q_heads, + local_q_heads=local_q_heads, + local_kv_head_start=tp_rank * local_kv_heads, + local_kv_heads=local_kv_heads, + global_sequence_length=kv_len, + local_sequence_length=kv_len, + global_block_indices=(0,), + global_block_token_starts=(0,), + local_block_offsets=(0, kv_len), + ) + # Causal alignment: the query block is the tail of the logical sequence, so + # every batch entry carries the same offset between Q row 0 and KV token 0. + causal_offsets = (kv_len - query_len,) * batch_size if causal else None + + contract = AttentionContract( + role=role, + mode=mode, + dtype=_DTYPE_TO_CONTRACT[query.dtype], + batch_size=batch_size, + query_sequence_length=query_len if mode is not AttentionMode.PREFILL else kv_len, + head_dim=head_dim, + causal=causal, + causal_offsets=causal_offsets, + sharding=sharding, + reduction=ReductionSpec(), + split_kv=SplitKVSpec.disabled(), + kv_cache=None, + rope=None, + export_lse=True, + ) + key_position_ids = _key_position_ids(metadata, query.device, batch_size, kv_len) + return contract, resolved_scale, query, key, value, key_position_ids + + +def _key_position_ids( + metadata: Mapping[str, Any], + device: torch.device, + batch_size: int, + kv_len: int, +) -> torch.Tensor: + """Resolve the global KV token positions for this request. + + Position identity is part of the contract, not an implementation detail: it + is what makes a training-side full-sequence call and a rollout-side chunk + provably describe the same logical tokens. Vime may declare it; otherwise + the canonical contiguous ``[0, kv_len)`` block is used. + """ + + declared = metadata.get("key_position_ids") + if declared is None: + return ( + torch.arange(kv_len, device=device, dtype=torch.int64) + .unsqueeze(0) + .expand(batch_size, kv_len) + .contiguous() + ) + positions = declared if isinstance(declared, torch.Tensor) else torch.as_tensor(declared) + positions = positions.to(device=device, dtype=torch.int64) + if positions.ndim == 1: + positions = positions.unsqueeze(0).expand(batch_size, -1) + if tuple(positions.shape) != (batch_size, kv_len): + raise AttentionProviderUnavailable( + f"key_position_ids must have shape {(batch_size, kv_len)}; " + f"got {tuple(positions.shape)}" + ) + if kv_len > 1 and bool((positions[:, 1:] - positions[:, :-1] != 1).any()): + raise AttentionProviderUnavailable( + "key_position_ids must describe one contiguous increasing token block" + ) + return positions.contiguous() + + +def attention_provider(request: Any) -> AttentionProviderResult: + """Compute Vime attention on the explicit WS2 strict ROCm contract. + + Materializes each logical batch row independently so batch composition + cannot change the bits, then returns the stacked ``(out, lse)`` together + with the dispatch provenance Vime records alongside the result. + """ + + contract, scale, query, key, value, key_positions = _contract_for_request(request) + + dispatch = kernel_registry.get_attention_op(contract, requested_backend=BACKEND_ID) + if dispatch.provenance["actual_backend"] != BACKEND_ID or dispatch.provenance["fallback"]: + raise RuntimeError("explicit strict attention dispatch changed during materialization") + + query_len = query.shape[2] + outs: list[torch.Tensor] = [] + lses: list[torch.Tensor] = [] + core_provenance: dict[str, Any] | None = None + for row in range(query.shape[0]): + row_key_positions = key_positions[row : row + 1] + result = dispatch.op.forward_with_lse( + query[row : row + 1], + key[row : row + 1], + value[row : row + 1], + causal=contract.causal, + scale=scale, + key_padding_mask=None, + query_position_ids=row_key_positions[:, -query_len:] if contract.causal else None, + key_position_ids=row_key_positions if contract.causal else None, + output_dtype=query.dtype, + ) + outs.append(result.out) + lses.append(result.lse) + if core_provenance is None: + core_provenance = dict(result.provenance) + + out = torch.cat(outs, dim=0) + lse = torch.cat(lses, dim=0) + assert core_provenance is not None # batch_size is validated positive + + provenance = dict(dispatch.provenance) + provenance["core"] = core_provenance + provenance["request"] = { + "query_shape": list(query.shape), + "key_shape": list(key.shape), + "dtype": str(query.dtype).replace("torch.", ""), + "causal": contract.causal, + "softmax_scale": scale, + "tp_rank": contract.sharding.tp_rank, + "tp_world_size": contract.sharding.tp_world_size, + "cp_rank": contract.sharding.cp_rank, + "cp_world_size": contract.sharding.cp_world_size, + } + provenance["execution"] = { + "role": "vime_attention", + "strict_backend": True, + "batch_rows_materialized_independently": True, + "attention_mode": contract.mode.value, + } + provenance["cp_row_ownership"] = { + "cp_rank": contract.sharding.cp_rank, + "cp_world_size": contract.sharding.cp_world_size, + "layout": getattr(request.context_parallel, "layout"), + "local_token_rows": contract.sharding.local_sequence_length, + "cp_is_merge_axis": False, + } + provenance["lse_domain"] = "attention" + return AttentionProviderResult( + out=out, + lse=lse, + backend_id=dispatch.capability.backend_id, + contract_id=contract.cross_rank_fingerprint(), + provenance=provenance, + ) + + +__all__ = [ + "AttentionProviderResult", + "AttentionProviderUnavailable", + "attention_provider", +] diff --git a/rl_engine/kernels/attention_contract.py b/rl_engine/kernels/attention_contract.py index 549857c1..96acdeba 100644 --- a/rl_engine/kernels/attention_contract.py +++ b/rl_engine/kernels/attention_contract.py @@ -11,6 +11,8 @@ from __future__ import annotations +import hashlib +import json from dataclasses import dataclass, field from enum import Enum from typing import Any, Iterable, TypeVar @@ -1289,6 +1291,41 @@ def to_dict(self) -> dict[str, Any]: "rope": rope, } + def cross_rank_fingerprint(self) -> str: + """Rank-independent identity for preflight agreement across ranks. + + Excludes ``tp_rank``/``cp_rank`` and the local head/sequence bounds they + derive, so every rank of one logical attention invocation computes the + same value. All-gathering this fingerprint together with the resolved + backend id and aborting on mismatch is the documented preflight for + distributed dispatch; ``requested_backend="auto"`` is not + distributed-safe without it. + """ + + payload = self.to_dict() + payload["sharding"] = { + key: value + for key, value in payload["sharding"].items() + if key + not in { + "tp_rank", + "cp_rank", + "local_q_head_start", + "local_q_heads", + "local_kv_head_start", + "local_kv_heads", + "local_sequence_length", + "global_block_indices", + "global_block_token_starts", + "local_block_offsets", + } + } + # Note: Any future extensions to this payload MUST maintain strict JSON + # serialization determinism across environments to prevent cross-rank + # hashing mismatches. + encoded = json.dumps(payload, sort_keys=True, separators=(",", ":")) + return hashlib.sha256(encoded.encode("utf-8")).hexdigest() + @dataclass(frozen=True) class AttentionBackendCapability: diff --git a/rl_engine/kernels/ops/rocm/attention/flash_attn.py b/rl_engine/kernels/ops/rocm/attention/flash_attn.py index 0a9b2b36..1839bebe 100644 --- a/rl_engine/kernels/ops/rocm/attention/flash_attn.py +++ b/rl_engine/kernels/ops/rocm/attention/flash_attn.py @@ -30,6 +30,11 @@ _MAX_TESTED_ROCM_TRITON_HEAD_DIM = 512 _AITER_API_SOURCE = "aiter.ops.mha" +# Stable dispatch identity for the strict ROCm attention core. Kept at module +# scope so contract-aware dispatch and the Vime adapter name one constant +# instead of duplicating the string. +BACKEND_ID = "aiter.rocm.ck_dense_mha" + class StrictRocmAttentionUnavailable(RuntimeError): """Raised when the exact AITER CK strict contract is unavailable.""" @@ -134,7 +139,7 @@ class StrictRocmAiterCKAttentionCore: core_id = STRICT_ATTENTION_ROCM_PRODUCTION_CORE_ID strict_schedule = STRICT_ATTENTION_ROCM_SCHEDULE_ID - backend_id = "aiter.rocm.ck_dense_mha" + backend_id = BACKEND_ID api_source = _AITER_API_SOURCE merge_order = "global_block_index" accum_dtype = "fp32" diff --git a/rl_engine/kernels/registry.py b/rl_engine/kernels/registry.py index 24dd5f2e..613398c8 100644 --- a/rl_engine/kernels/registry.py +++ b/rl_engine/kernels/registry.py @@ -8,9 +8,22 @@ import torch +from rl_engine.kernels.attention_contract import ( + AttentionBackendCapability, + AttentionContract, + AttentionContractError, + AttentionDispatchResult, + AttentionDType, + AttentionMode, + AttentionRole, +) from rl_engine.platforms.device import device_ctx from rl_engine.utils.logger import logger +# Dispatch policy keywords accepted by ``get_attention_op`` in place of an +# exact backend id. They mirror ``AttentionBackendCapability.implementation_kind``. +ATTENTION_IMPLEMENTATION_KINDS = frozenset({"production", "reference", "deterministic"}) + class _KernelEnumMeta(EnumMeta): """Metaclass to provide enhanced error messaging for backend lookups.""" @@ -41,6 +54,13 @@ class OpBackend(Enum, metaclass=_KernelEnumMeta): ROCM_AITER = "rl_engine.kernels.ops.rocm.aiter.AiterOp" ROCM_CK = "rl_engine.kernels.ops.rocm.composable_kernel.CKOp" ROCM_FLASH_ATTN = "rl_engine.kernels.ops.rocm.attention.flash_attn.RocmFlashAttentionOp" + # WS2 strict ROCm attention core (AITER/CK dense MHA, Split-KV disabled). + # Registered for contract-aware dispatch only, never in the legacy + # ``get_op`` priority lists: it is not a drop-in for the SDPA-shaped + # ``attn`` wrappers and must not be reachable as a silent fallback. + ROCM_STRICT_ATTENTION = ( + "rl_engine.kernels.ops.rocm.attention.flash_attn.StrictRocmAiterCKAttentionCore" + ) # GRPO loss (group reward normalization + clipped surrogate + KL) TRITON_GRPO_LOSS = "rl_engine.kernels.ops.triton.loss.grpo_loss.TritonGRPOLossOp" @@ -167,6 +187,28 @@ def resolve_logp_op_type( return op_type +def _rocm_strict_attention_available() -> bool: + """Return whether the strict ROCm AITER/CK attention core can actually load. + + The probe mirrors the ROCm logprob backend gate: it must be true only when + this process can really execute the strict arithmetic, so an unavailable + vendor stack results in the backend never being registered rather than in a + registered backend that fails at materialization time. + """ + + if torch.version.hip is None: + return False + try: + from rl_engine.kernels.ops.rocm.attention.flash_attn import _load_aiter_ck_ops + except ImportError: + return False + try: + _load_aiter_ck_ops() + except Exception: # StrictRocmAttentionUnavailable and vendor import errors + return False + return True + + class KernelRegistry: """ Central dispatcher for high-performance kernels. @@ -313,6 +355,218 @@ def __init__(self): self._adjust_priority_for_hardware() self._adjust_priority_from_env() + # WS2 attention dispatch owns its own candidate list and starts empty on + # every platform. The legacy ``attn`` / ``attention`` priority lists stay + # untouched, so a strict 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 legacy caller as a silent substitution. + self._attention_candidates: Dict[str, list] = { + platform: [] for platform in self._priority_map + } + self._attention_capabilities: Dict[str, Dict[OpBackend, AttentionBackendCapability]] = { + platform: {} for platform in self._priority_map + } + + # Strict ROCm production core: AITER/CK dense MHA with Split-KV disabled. + # Registered only when the vendor entry points genuinely load, so an + # explicit request on a machine without AITER fails loudly in + # ``get_attention_op`` instead of resolving to different arithmetic. + rocm_strict_attention_capability = AttentionBackendCapability( + backend_id="aiter.rocm.ck_dense_mha", + roles=frozenset({AttentionRole.TRAIN, AttentionRole.INFER}), + modes=frozenset({AttentionMode.PREFILL, AttentionMode.CHUNKED_PREFILL}), + dtypes=frozenset({AttentionDType.BF16, AttentionDType.FP16}), + cp_world_sizes=(1,), + tp_world_sizes=None, + exports_attention_lse=True, + deterministic_cp_merge=False, + supports_packed_varlen=False, + supports_kv_cache=False, + supports_rope_metadata=True, + supports_fused_rope_attention=False, + supports_split_kv_disabled=True, + supports_split_kv_fixed=False, + supports_split_kv_auto=False, + reports_actual_split_kv_plan=True, + implementation_kind="production", + ) + if _rocm_strict_attention_available() and "rocm" in self._priority_map: + self.register_attention_backend( + OpBackend.ROCM_STRICT_ATTENTION, + rocm_strict_attention_capability, + platform="rocm", + prepend=True, + ) + + def register_attention_backend( + self, + backend: OpBackend, + capability: AttentionBackendCapability, + *, + platform: Optional[str] = None, + prepend: bool = False, + ) -> None: + """Register (or replace) a backend for WS2 contract-aware attention dispatch. + + This is the supported seam for making an attention backend selectable by + ``get_attention_op`` without touching the legacy ``get_op`` priority + lists. Registering the same backend again replaces its capability + without duplicating the candidate entry. + """ + + if not isinstance(backend, OpBackend): + raise AttentionContractError("backend must be an OpBackend") + if not isinstance(capability, AttentionBackendCapability): + raise AttentionContractError("capability must be an AttentionBackendCapability") + resolved_platform = platform if platform is not None else self._platform() + if resolved_platform not in self._priority_map: + raise AttentionContractError( + f"unsupported platform {resolved_platform!r}; expected one of " + f"{sorted(self._priority_map)}" + ) + candidates = self._attention_candidates.setdefault(resolved_platform, []) + self._attention_capabilities.setdefault(resolved_platform, {})[backend] = capability + if backend not in candidates: + if prepend: + candidates.insert(0, backend) + else: + candidates.append(backend) + + def get_attention_op( + self, + contract: AttentionContract, + *, + requested_backend: str = "auto", + ) -> AttentionDispatchResult: + """Resolve only a backend that explicitly supports the WS2 attention contract. + + Deliberately separate from the legacy ``get_op``: existing callers keep + their current behavior, while WS2 callers cannot silently fall back to a + backend with different reduction or Split-KV semantics. + + ``requested_backend`` is either a case-insensitive policy keyword + (``auto`` | ``production`` | ``reference``) or an exact, case-sensitive + stable backend id. Strictness comes from the contract's capability + checks rather than from this policy string. With ``cp_world_size > 1``, + ``auto`` is rejected: per-rank auto resolution can diverge across ranks, + so distributed callers must name a policy or backend id. + """ + + if not isinstance(contract, AttentionContract): + raise AttentionContractError("contract must be an AttentionContract") + if not isinstance(requested_backend, str) or not requested_backend.strip(): + raise AttentionContractError("requested_backend must be a non-empty string") + requested_backend = requested_backend.strip() + if requested_backend.lower() == "deterministic": + raise AttentionContractError( + 'requested_backend="deterministic" is not a dispatch policy; request ' + "determinism through ReductionSpec/SplitKVSpec and match it against the " + "backend capability flags instead" + ) + if requested_backend.lower() == "auto" and contract.sharding.cp_world_size > 1: + raise AttentionContractError( + "Unsafe dispatch: requested_backend='auto' is not permitted when " + "cp_world_size > 1 without explicit cross-rank preflighting." + ) + + platform = self._platform() + candidates = self._attention_candidates.get(platform, []) + rejected: list[str] = [] + # provenance["fallback"] reports only capability/load rejections of + # otherwise-eligible candidates; skips caused purely by the caller's own + # requested_backend policy filter are not fallbacks. + capability_rejections = 0 + + platform_capabilities = self._attention_capabilities.get(platform, {}) + for backend in candidates: + capability = platform_capabilities.get(backend) + if capability is None: + rejected.append(f"{backend.name}: no AttentionBackendCapability declared") + capability_rejections += 1 + continue + policy_mismatch = self._attention_policy_mismatch(requested_backend, capability) + if policy_mismatch is not None: + rejected.append(f"{backend.name}: {policy_mismatch}") + continue + capability_incompat = list(capability.incompatibilities(contract)) + if capability_incompat: + rejected.append(f"{backend.name}: " + "; ".join(capability_incompat)) + capability_rejections += 1 + continue + + op = self._get_or_create_backend(backend) + if op is None: + rejected.append(f"{backend.name}: backend could not be loaded or instantiated") + capability_rejections += 1 + continue + + provenance = { + "requested_backend": requested_backend, + "actual_backend": capability.backend_id, + "backend_enum": backend.name, + "platform": platform, + "fallback": capability_rejections > 0, + "fallback_reason": None, + "prior_rejections": list(rejected), + "contract": contract.to_dict(), + "capability": capability.to_dict(), + } + return AttentionDispatchResult(op=op, capability=capability, provenance=provenance) + + details = " | ".join(rejected) if rejected else "no candidates registered" + requested = contract.to_dict() + raise RuntimeError( + "No attention backend supports the requested WS2 contract on " + f"{platform}: role={requested['role']}, mode={requested['mode']}, " + f"dtype={requested['dtype']}, TP={contract.sharding.tp_world_size}, " + f"CP={contract.sharding.cp_world_size}, " + f"split_kv={contract.split_kv.mode.value}. Rejections: {details}" + ) + + @staticmethod + def _attention_policy_mismatch( + requested_backend: str, + capability: AttentionBackendCapability, + ) -> str | None: + policy = requested_backend.lower() + if policy == "auto": + return None + if policy in ATTENTION_IMPLEMENTATION_KINDS: + if capability.implementation_kind == policy: + return None + return ( + f"implementation_kind={capability.implementation_kind} does not satisfy " + f"requested_backend={policy}" + ) + if capability.backend_id == requested_backend: + return None + return ( + f"backend_id={capability.backend_id} does not match " + f"requested_backend={requested_backend}" + ) + + def _platform(self) -> str: + return self._platform_for_device(None) + + def _get_or_create_backend(self, backend: OpBackend) -> Any | None: + if backend.name in self._instance_cache: + return self._instance_cache[backend.name] + if backend.name in self._failed_backends: + return None + + op_class = self._load_backend(backend) + if op_class is None: + self._failed_backends.add(backend.name) + return None + try: + op = op_class() + except Exception as exc: + logger.error(f"Failed to instantiate {backend.name}: {exc}") + self._failed_backends.add(backend.name) + return None + self._instance_cache[backend.name] = op + return op + def _adjust_priority_from_env(self): rocm_attn_backend = os.getenv("RL_KERNEL_ROCM_ATTN_BACKEND", "").strip().lower() if rocm_attn_backend in {"flash_attn", "flash-attn", "flash_attention"}: diff --git a/tests/test_attention_dispatch.py b/tests/test_attention_dispatch.py new file mode 100644 index 00000000..4c174557 --- /dev/null +++ b/tests/test_attention_dispatch.py @@ -0,0 +1,307 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2026 RL-Kernel Contributors + +"""Contract-aware attention dispatch: registration, policy, and fail-closed. + +These cases use a fresh ``KernelRegistry`` so they never mutate the process +singleton, and they assert the property that motivates a separate dispatch +entry point: a WS2 attention caller must never be served by a backend that +declares different reduction, Split-KV, or LSE-export semantics. +""" + +from __future__ import annotations + +import pytest + +from rl_engine.kernels.attention_contract import ( + AttentionBackendCapability, + AttentionContract, + AttentionContractError, + AttentionDType, + AttentionMode, + AttentionRole, + ReductionSpec, + ShardingSpec, + SplitKVSpec, +) +from rl_engine.kernels.registry import KernelRegistry, OpBackend, _rocm_strict_attention_available + + +def _contract(*, cp_world_size: int = 1, seq_len: int = 128) -> AttentionContract: + sharding = ShardingSpec( + tp_rank=0, + tp_world_size=1, + cp_rank=0, + cp_world_size=cp_world_size, + global_q_heads=32, + global_kv_heads=8, + local_q_head_start=0, + local_q_heads=32, + local_kv_head_start=0, + local_kv_heads=8, + global_sequence_length=seq_len, + local_sequence_length=seq_len, + global_block_indices=(0,), + global_block_token_starts=(0,), + local_block_offsets=(0, seq_len), + ) + return AttentionContract( + role=AttentionRole.TRAIN, + mode=AttentionMode.PREFILL, + dtype=AttentionDType.BF16, + batch_size=1, + query_sequence_length=seq_len, + head_dim=128, + causal=True, + causal_offsets=(0,), + sharding=sharding, + reduction=ReductionSpec(), + split_kv=SplitKVSpec.disabled(), + export_lse=True, + ) + + +def _capability(**overrides) -> AttentionBackendCapability: + fields = { + "backend_id": "test.strict.core", + "roles": frozenset({AttentionRole.TRAIN, AttentionRole.INFER}), + "modes": frozenset({AttentionMode.PREFILL}), + "dtypes": frozenset({AttentionDType.BF16}), + "cp_world_sizes": (1,), + "exports_attention_lse": True, + "reports_actual_split_kv_plan": True, + "implementation_kind": "production", + } + fields.update(overrides) + return AttentionBackendCapability(**fields) + + +class _FakeCore: + pass + + +@pytest.fixture() +def registry(monkeypatch): + fresh = KernelRegistry() + # Serve a stand-in instance so dispatch never imports a vendor stack. + monkeypatch.setattr(fresh, "_get_or_create_backend", lambda backend: _FakeCore()) + return fresh + + +def _platform(registry) -> str: + return registry._platform() + + +def test_only_rocm_autoregisters_and_only_when_the_vendor_stack_loads(): + """Attention dispatch is opt-in per platform. + + The strict ROCm core is the single auto-registration, and it appears only + when ``aiter.ops.mha`` really loaded. No other platform gains an attention + candidate implicitly, so a CUDA or CPU WS2 caller still fails loudly rather + than being served something with different semantics. + """ + + fresh = KernelRegistry() + + for platform, candidates in fresh._attention_candidates.items(): + if platform == "rocm": + expected = ( + [OpBackend.ROCM_STRICT_ATTENTION] if _rocm_strict_attention_available() else [] + ) + assert candidates == expected + else: + assert candidates == [] + + +def test_registered_backend_resolves_with_provenance(registry): + registry.register_attention_backend( + OpBackend.ROCM_STRICT_ATTENTION, _capability(), platform=_platform(registry) + ) + + result = registry.get_attention_op(_contract(), requested_backend="test.strict.core") + + assert result.capability.backend_id == "test.strict.core" + assert result.provenance["actual_backend"] == "test.strict.core" + assert result.provenance["fallback"] is False + assert result.provenance["requested_backend"] == "test.strict.core" + assert result.provenance["contract"]["lse_domain"] == "attention" + + +def test_unregistered_contract_fails_loudly_instead_of_falling_back(registry): + # Drop every auto-registered candidate: a platform with no attention + # backend must raise rather than resolve something from the legacy lists. + for candidates in registry._attention_candidates.values(): + candidates.clear() + + with pytest.raises(RuntimeError, match="No attention backend supports"): + registry.get_attention_op(_contract(), requested_backend="auto") + + +def test_explicit_backend_id_never_resolves_to_a_different_backend(registry): + registry.register_attention_backend( + OpBackend.ROCM_STRICT_ATTENTION, _capability(), platform=_platform(registry) + ) + + with pytest.raises(RuntimeError, match="does not match requested_backend"): + registry.get_attention_op(_contract(), requested_backend="some.other.backend") + + +def test_capability_mismatch_is_rejected_rather_than_approximated(registry): + # A backend that cannot export attention-domain LSE must not serve a + # contract that requires it. + registry.register_attention_backend( + OpBackend.ROCM_STRICT_ATTENTION, + _capability(exports_attention_lse=False), + platform=_platform(registry), + ) + + with pytest.raises(RuntimeError, match="LSE export is unsupported"): + registry.get_attention_op(_contract(), requested_backend="test.strict.core") + + +def test_cp_contract_requires_deterministic_merge_support(registry): + registry.register_attention_backend( + OpBackend.ROCM_STRICT_ATTENTION, + _capability(cp_world_sizes=(1, 2)), + platform=_platform(registry), + ) + + with pytest.raises(RuntimeError, match="deterministic CP"): + registry.get_attention_op(_contract(cp_world_size=2), requested_backend="test.strict.core") + + +def test_auto_is_rejected_under_context_parallelism(registry): + registry.register_attention_backend( + OpBackend.ROCM_STRICT_ATTENTION, _capability(), platform=_platform(registry) + ) + + with pytest.raises(AttentionContractError, match="Unsafe dispatch"): + registry.get_attention_op(_contract(cp_world_size=2), requested_backend="auto") + + +def test_deterministic_is_not_a_dispatch_policy(registry): + with pytest.raises(AttentionContractError, match="not a dispatch policy"): + registry.get_attention_op(_contract(), requested_backend="deterministic") + + +def test_implementation_kind_policy_filters_without_marking_fallback(registry): + registry.register_attention_backend( + OpBackend.ROCM_STRICT_ATTENTION, _capability(), platform=_platform(registry) + ) + + result = registry.get_attention_op(_contract(), requested_backend="production") + assert result.provenance["fallback"] is False + + with pytest.raises(RuntimeError, match="does not satisfy requested_backend=reference"): + registry.get_attention_op(_contract(), requested_backend="reference") + + +def test_reregistration_replaces_capability_without_duplicating(registry): + platform = _platform(registry) + registry.register_attention_backend( + OpBackend.ROCM_STRICT_ATTENTION, _capability(), platform=platform + ) + registry.register_attention_backend( + OpBackend.ROCM_STRICT_ATTENTION, + _capability(implementation_kind="reference"), + platform=platform, + ) + + assert registry._attention_candidates[platform] == [OpBackend.ROCM_STRICT_ATTENTION] + capability = registry._attention_capabilities[platform][OpBackend.ROCM_STRICT_ATTENTION] + assert capability.implementation_kind == "reference" + + +def test_register_rejects_wrong_types_and_unknown_platforms(registry): + with pytest.raises(AttentionContractError, match="must be an OpBackend"): + registry.register_attention_backend("not-a-backend", _capability()) + with pytest.raises(AttentionContractError, match="must be an AttentionBackendCapability"): + registry.register_attention_backend(OpBackend.ROCM_STRICT_ATTENTION, object()) + with pytest.raises(AttentionContractError, match="unsupported platform"): + registry.register_attention_backend( + OpBackend.ROCM_STRICT_ATTENTION, _capability(), platform="quantum" + ) + + +def test_legacy_priority_map_is_untouched_by_attention_registration(registry): + platform = _platform(registry) + before = {op: list(v) for op, v in registry._priority_map[platform].items()} + + registry.register_attention_backend( + OpBackend.ROCM_STRICT_ATTENTION, _capability(), platform=platform + ) + + after = {op: list(v) for op, v in registry._priority_map[platform].items()} + assert before == after + # The strict core must not become reachable through legacy get_op keys. + for candidates in after.values(): + assert OpBackend.ROCM_STRICT_ATTENTION not in candidates + + +def test_contract_fingerprint_is_rank_independent(): + left = _contract() + right_sharding = ShardingSpec( + tp_rank=1, + tp_world_size=2, + cp_rank=0, + cp_world_size=1, + global_q_heads=32, + global_kv_heads=8, + local_q_head_start=16, + local_q_heads=16, + local_kv_head_start=4, + local_kv_heads=4, + global_sequence_length=128, + local_sequence_length=128, + global_block_indices=(0,), + global_block_token_starts=(0,), + local_block_offsets=(0, 128), + ) + left_tp2 = AttentionContract( + role=AttentionRole.TRAIN, + mode=AttentionMode.PREFILL, + dtype=AttentionDType.BF16, + batch_size=1, + query_sequence_length=128, + head_dim=128, + causal=True, + causal_offsets=(0,), + sharding=ShardingSpec( + tp_rank=0, + tp_world_size=2, + cp_rank=0, + cp_world_size=1, + global_q_heads=32, + global_kv_heads=8, + local_q_head_start=0, + local_q_heads=16, + local_kv_head_start=0, + local_kv_heads=4, + global_sequence_length=128, + local_sequence_length=128, + global_block_indices=(0,), + global_block_token_starts=(0,), + local_block_offsets=(0, 128), + ), + reduction=ReductionSpec(), + split_kv=SplitKVSpec.disabled(), + export_lse=True, + ) + right_tp2 = AttentionContract( + role=AttentionRole.TRAIN, + mode=AttentionMode.PREFILL, + dtype=AttentionDType.BF16, + batch_size=1, + query_sequence_length=128, + head_dim=128, + causal=True, + causal_offsets=(0,), + sharding=right_sharding, + reduction=ReductionSpec(), + split_kv=SplitKVSpec.disabled(), + export_lse=True, + ) + + # Both TP ranks of one logical invocation agree; a different TP degree does not. + assert left_tp2.cross_rank_fingerprint() == right_tp2.cross_rank_fingerprint() + assert left.cross_rank_fingerprint() != left_tp2.cross_rank_fingerprint() diff --git a/tests/test_vime_attention_provider.py b/tests/test_vime_attention_provider.py new file mode 100644 index 00000000..3a8afc35 --- /dev/null +++ b/tests/test_vime_attention_provider.py @@ -0,0 +1,272 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2026 RL-Kernel Contributors + +"""Coverage for the optional Vime WS2 strict attention adapter. + +The validation-shaped cases need a real ROCm device with AITER present and are +skipped elsewhere. The contract/fail-closed cases are pure metadata checks and +run anywhere, so a CUDA or CPU CI job still catches a provider that silently +widens what it accepts. +""" + +from __future__ import annotations + +import math +from types import SimpleNamespace + +import pytest +import torch + +from rl_engine.integrations.vime.attention import AttentionProviderUnavailable, attention_provider +from rl_engine.kernels.registry import _rocm_strict_attention_available + +STRICT_ROCM = _rocm_strict_attention_available() +requires_strict_rocm = pytest.mark.skipif( + not STRICT_ROCM, + reason="strict ROCm attention requires a ROCm device with aiter.ops.mha", +) + +# Qwen3-8B dense head layout, TP=1 local view. +GLOBAL_Q_HEADS = 32 +GLOBAL_KV_HEADS = 8 +HEAD_DIM = 128 + + +def _metadata(**overrides): + metadata = { + "global_q_heads": GLOBAL_Q_HEADS, + "global_kv_heads": GLOBAL_KV_HEADS, + "tp_rank": 0, + "tp_world_size": 1, + "attention_mode": "prefill", + "role": "train", + "causal": True, + } + metadata.update(overrides) + return metadata + + +def _request( + *, + batch_size: int = 1, + seq_len: int = 128, + query_len: int | None = None, + dtype: torch.dtype = torch.bfloat16, + device: str = "cuda", + cp_world_size: int = 1, + cp_rank: int = 0, + cp_layout: str = "single", + seed: int = 0, + **metadata_overrides, +): + generator = torch.Generator(device=device).manual_seed(seed) + shape_q = (batch_size, GLOBAL_Q_HEADS, query_len or seq_len, HEAD_DIM) + shape_kv = (batch_size, GLOBAL_KV_HEADS, seq_len, HEAD_DIM) + return SimpleNamespace( + query=torch.randn(*shape_q, generator=generator, device=device, dtype=dtype), + key=torch.randn(*shape_kv, generator=generator, device=device, dtype=dtype), + value=torch.randn(*shape_kv, generator=generator, device=device, dtype=dtype), + key_padding_mask=None, + tensor_parallel_group=None, + context_parallel=SimpleNamespace(world_size=cp_world_size, rank=cp_rank, layout=cp_layout), + metadata=_metadata(**metadata_overrides), + ) + + +def _cpu_request(**kwargs): + """A request whose rejection happens before any device work.""" + + kwargs.setdefault("device", "cpu") + kwargs.setdefault("seq_len", 8) + return _request(**kwargs) + + +# --------------------------------------------------------------------------- +# Contract and fail-closed behavior (device independent) +# --------------------------------------------------------------------------- + + +def test_cp_greater_than_one_refuses_the_cross_rank_merge(): + request = _cpu_request(cp_world_size=2, cp_rank=1, cp_layout="zigzag") + + with pytest.raises(AttentionProviderUnavailable, match="strict CP transport path"): + attention_provider(request) + + +def test_decode_without_kv_cache_identity_fails_closed(): + request = _cpu_request(attention_mode="decode") + + with pytest.raises(AttentionProviderUnavailable, match="KV-cache identity"): + attention_provider(request) + + +@pytest.mark.parametrize( + "overrides", + [ + {"dropout_p": 0.1}, + {"sliding_window": 128}, + {"logit_soft_cap": 30.0}, + {"alibi_slopes": [0.1]}, + {"window_size": (256, 0)}, + ], +) +def test_distribution_changing_knobs_fail_closed(overrides): + request = _cpu_request(**overrides) + + with pytest.raises(AttentionProviderUnavailable): + attention_provider(request) + + +def test_key_padding_mask_is_refused(): + request = _cpu_request() + request.key_padding_mask = torch.ones(1, 8, dtype=torch.bool) + + with pytest.raises(AttentionProviderUnavailable, match="unpadded logical row"): + attention_provider(request) + + +def test_fp32_is_refused(): + request = _cpu_request(dtype=torch.float32) + + with pytest.raises(AttentionProviderUnavailable, match="BF16/FP16"): + attention_provider(request) + + +def test_head_counts_must_cover_the_tp_group_exactly(): + request = _cpu_request(global_q_heads=GLOBAL_Q_HEADS * 2) + + with pytest.raises(AttentionProviderUnavailable, match="do not cover global_q_heads"): + attention_provider(request) + + +def test_declared_tp_rank_must_agree_with_the_group(): + request = _cpu_request(tp_rank=3) + + with pytest.raises(AttentionProviderUnavailable, match="disagrees with TP group rank"): + attention_provider(request) + + +def test_cp_layout_must_describe_local_ownership(): + request = _cpu_request(cp_layout="unknown") + + with pytest.raises(AttentionProviderUnavailable, match="local CP token ownership"): + attention_provider(request) + + +def test_non_contiguous_key_positions_are_refused(): + request = _cpu_request(key_position_ids=[0, 1, 2, 3, 9, 10, 11, 12]) + + with pytest.raises(AttentionProviderUnavailable, match="contiguous increasing"): + attention_provider(request) + + +# --------------------------------------------------------------------------- +# Strict arithmetic (requires a ROCm device with AITER) +# --------------------------------------------------------------------------- + + +@requires_strict_rocm +def test_provider_exports_attention_lse_and_strict_provenance(): + result = attention_provider(_request(seq_len=256)) + + assert result.backend_id == "aiter.rocm.ck_dense_mha" + assert result.out.shape == (1, GLOBAL_Q_HEADS, 256, HEAD_DIM) + assert result.lse.shape == (1, GLOBAL_Q_HEADS, 256) + assert result.lse.dtype == torch.float32 + assert result.provenance["fallback"] is False + assert result.provenance["actual_backend"] == "aiter.rocm.ck_dense_mha" + assert result.provenance["lse_domain"] == "attention" + + core = result.provenance["core"] + assert core["native_attention_arithmetic"] is True + assert core["deterministic_backward"] is True + assert core["num_splits"] == 1 + assert core["fallback"] is False + assert core["merge_order"] == "global_block_index" + assert core["accum_dtype"] == "fp32" + assert core["downcast_at"] == "final_write" + + +@requires_strict_rocm +def test_training_and_rollout_roles_are_bitwise_identical(): + train = attention_provider(_request(seq_len=256, role="train", seed=3)) + rollout = attention_provider(_request(seq_len=256, role="infer", seed=3)) + + assert torch.equal(train.out, rollout.out) + assert torch.equal(train.lse, rollout.lse) + + +@requires_strict_rocm +@pytest.mark.parametrize("batch_size", [2, 4]) +@pytest.mark.parametrize("seq_len", [256, 512, 2048]) +def test_batch_composition_is_bitwise_invariant(batch_size, seq_len): + """A batch must equal the same rows submitted one at a time. + + Raw AITER does not provide this for every shape: measured on MI300X it is + batch-composition sensitive in BF16 at ``S=256`` (B=4) and ``S=512`` (B=2 + and B=4), while holding at 128/1024/2048/4096. Shape-dependent breakage is + exactly what a per-row rule has to defend against, because the shapes that + hold would otherwise make the bug look absent. ``S=512`` is kept in this + parametrization deliberately. + """ + + batched_request = _request(batch_size=batch_size, seq_len=seq_len, seed=11) + batched = attention_provider(batched_request) + + for row in range(batch_size): + single = SimpleNamespace( + query=batched_request.query[row : row + 1], + key=batched_request.key[row : row + 1], + value=batched_request.value[row : row + 1], + key_padding_mask=None, + tensor_parallel_group=None, + context_parallel=batched_request.context_parallel, + metadata=batched_request.metadata, + ) + row_result = attention_provider(single) + assert torch.equal(batched.out[row : row + 1], row_result.out) + assert torch.equal(batched.lse[row : row + 1], row_result.lse) + + +@requires_strict_rocm +def test_repeated_invocations_are_bitwise_identical(): + first = attention_provider(_request(seq_len=512, seed=5)) + second = attention_provider(_request(seq_len=512, seed=5)) + + assert torch.equal(first.out, second.out) + assert torch.equal(first.lse, second.lse) + + +@requires_strict_rocm +def test_backward_gradients_are_deterministic(): + def run(): + request = _request(seq_len=256, seed=17) + request.query.requires_grad_(True) + request.key.requires_grad_(True) + request.value.requires_grad_(True) + result = attention_provider(request) + result.out.backward(torch.ones_like(result.out)) + return request.query.grad, request.key.grad, request.value.grad + + first = run() + second = run() + for lhs, rhs in zip(first, second, strict=True): + assert torch.equal(lhs, rhs) + + +@requires_strict_rocm +def test_contract_fingerprint_is_rank_independent(): + left = attention_provider(_request(seq_len=128, seed=2)) + right = attention_provider(_request(seq_len=128, seed=2)) + + assert left.contract_id == right.contract_id + assert len(left.contract_id) == 64 + + +@requires_strict_rocm +def test_explicit_scale_is_honored(): + scale = 1.0 / math.sqrt(HEAD_DIM) + default = attention_provider(_request(seq_len=128, seed=8)) + explicit = attention_provider(_request(seq_len=128, seed=8, softmax_scale=scale)) + + assert torch.equal(default.out, explicit.out) From 3446042f78e1893b3cb81c7a63396d5bc865625d Mon Sep 17 00:00:00 2001 From: zhangj1an Date: Mon, 24 Aug 2026 16:11:34 +0000 Subject: [PATCH 15/35] feat(attention): bind TP/CP degree and add multi-rank strict validation 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) Claude-Session: https://claude.ai/code/session_01QfN3b2ep5DvxVDir7J36j3 --- benchmarks/benchmark_rocm_attention.py | 87 +- .../pr319_rocm_mi300x/distributed/report.md | 16 + .../distributed/strict_rccl_ag_rs_w2.json | 582 +++++ .../distributed/strict_rccl_ag_rs_w4.json | 1142 +++++++++ .../distributed/strict_rccl_ag_rs_w8.json | 2262 +++++++++++++++++ .../results/pr319_rocm_mi300x/report.md | 62 +- .../results/pr319_rocm_mi300x/results.json | 258 +- rl_engine/integrations/vime/attention.py | 18 + rl_engine/kernels/attention_contract.py | 77 + tests/test_attention_dispatch.py | 129 + tests/test_vime_attention_provider.py | 48 + 11 files changed, 4587 insertions(+), 94 deletions(-) create mode 100644 benchmarks/results/pr319_rocm_mi300x/distributed/report.md create mode 100644 benchmarks/results/pr319_rocm_mi300x/distributed/strict_rccl_ag_rs_w2.json create mode 100644 benchmarks/results/pr319_rocm_mi300x/distributed/strict_rccl_ag_rs_w4.json create mode 100644 benchmarks/results/pr319_rocm_mi300x/distributed/strict_rccl_ag_rs_w8.json diff --git a/benchmarks/benchmark_rocm_attention.py b/benchmarks/benchmark_rocm_attention.py index addaa553..e3c250e2 100644 --- a/benchmarks/benchmark_rocm_attention.py +++ b/benchmarks/benchmark_rocm_attention.py @@ -19,13 +19,16 @@ ``aiter.rocm.ck_dense_mha`` through the WS2 contract dispatch, i.e. the path this PR adds. -Two extra sections quantify what the strict contract actually costs, because -both are properties of the vendor backward rather than of the integration: +Three extra sections quantify what the strict contract actually costs and +where it stops holding. All three are properties of the vendor kernel rather +than of the integration: * ``determinism_cost`` — AITER ``mha_bwd`` with ``deterministic`` on vs off. * ``batch_composition`` — whether raw AITER returns the same bits for a batch and for the same rows submitted one at a time, swept over shapes because the answer varies with shape. +* ``tp_head_count_sensitivity`` — whether a head shard depends on how many + heads shared its launch, i.e. whether the path is TP-degree invariant. """ from __future__ import annotations @@ -288,6 +291,77 @@ def forward(query, key, value): return rows +def _tp_head_count_sensitivity(q_heads, kv_heads, head_dim, dtype, seq_lens, tp_degrees): + """Does a head shard depend on how many heads shared its launch? + + TP shards attention by head and performs no cross-rank reduction, so a rank + computing its own head slice ought to match the corresponding slice of an + unsharded run. Where it does not, the strict path is not TP-degree + invariant and training/rollout must be pinned to one degree. + """ + + from aiter.ops.mha import mha_fwd + + scale = 1.0 / math.sqrt(head_dim) + + def forward(query, key, value): + out, lse, _mask, _rng = mha_fwd( + query.transpose(1, 2).contiguous(), + key.transpose(1, 2).contiguous(), + value.transpose(1, 2).contiguous(), + 0.0, + scale, + True, + -1, + -1, + 0, + True, + False, + ) + return out.transpose(1, 2).contiguous(), lse + + rows = [] + for seq_len in seq_lens: + query, key, value = _tensors(1, q_heads, kv_heads, seq_len, head_dim, dtype, seed=3) + full_out, full_lse = forward(query, key, value) + for tp in tp_degrees: + if q_heads % tp or kv_heads % tp: + continue + local_q, local_kv = q_heads // tp, kv_heads // tp + worst_out = worst_lse = 0.0 + for rank in range(tp): + shard_out, shard_lse = forward( + query[:, rank * local_q : (rank + 1) * local_q], + key[:, rank * local_kv : (rank + 1) * local_kv], + value[:, rank * local_kv : (rank + 1) * local_kv], + ) + ref_out = full_out[:, rank * local_q : (rank + 1) * local_q] + ref_lse = full_lse[:, rank * local_q : (rank + 1) * local_q] + worst_out = max(worst_out, (shard_out - ref_out).abs().max().item()) + worst_lse = max(worst_lse, (shard_lse - ref_lse).abs().max().item()) + invariant = worst_out == 0.0 and worst_lse == 0.0 + rows.append( + { + "seq_len": seq_len, + "tp": tp, + "local_q_heads": local_q, + "local_kv_heads": local_kv, + "out_max_abs": worst_out, + "lse_max_abs": worst_lse, + "tp_degree_invariant": invariant, + } + ) + print( + f"tp-sensitivity S={seq_len:5d} TP={tp} (Hq={local_q},Hkv={local_kv}) " + f"out {worst_out:.6e} lse {worst_lse:.6e} " + f"{'invariant' if invariant else 'NOT INVARIANT'}", + flush=True, + ) + del query, key, value, full_out, full_lse + torch.cuda.empty_cache() + return rows + + def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--output", type=Path, default=Path("attention_results.json")) @@ -359,6 +433,14 @@ def main() -> None: composition = _batch_composition( args.q_heads, args.kv_heads, args.head_dim, dtype, composition_shapes ) + tp_sensitivity = _tp_head_count_sensitivity( + args.q_heads, + args.kv_heads, + args.head_dim, + dtype, + sorted({512, *seq_lens}), + (2, 4, 8), + ) properties = torch.cuda.get_device_properties(0) payload = { @@ -376,6 +458,7 @@ def main() -> None: "latency": rows, "determinism_cost": determinism, "batch_composition": composition, + "tp_head_count_sensitivity": tp_sensitivity, } args.output.parent.mkdir(parents=True, exist_ok=True) args.output.write_text(json.dumps(payload, indent=2)) diff --git a/benchmarks/results/pr319_rocm_mi300x/distributed/report.md b/benchmarks/results/pr319_rocm_mi300x/distributed/report.md new file mode 100644 index 00000000..e46222af --- /dev/null +++ b/benchmarks/results/pr319_rocm_mi300x/distributed/report.md @@ -0,0 +1,16 @@ +# PR #319 — strict ROCm CP attention, multi-rank acceptance (MI300X) + +`torchrun --standalone --nproc-per-node=N scripts/ws2_p2p_nccl_attention_reference_check.py \ + --transport rccl_ag_rs --strict-shared-core` + +Requires the native extension built for the active GPU platform +(`PYTORCH_ROCM_ARCH=gfx942 python setup.py build_ext --inplace`); without it the strict path +fails closed on the ROCm deterministic RoPE operator rather than running a different one. + +| world | TP | CP | replicas | transport | ranks passed | out | lse | dQ | dK | dV | +|---:|---:|---:|---:|---|---|---|---|---|---|---| +| 2 | 1 | 2 | 1 | rccl_ag_rs | 2/2 | bitwise | bitwise | bitwise | bitwise | bitwise | +| 4 | 2 | 2 | 1 | rccl_ag_rs | 4/4 | bitwise | bitwise | bitwise | bitwise | bitwise | +| 8 | 2 | 2 | 2 | rccl_ag_rs | 8/8 | bitwise | bitwise | bitwise | bitwise | bitwise | + +Every rank reports `strict_shared_core.executed=true` and `passed=true`. diff --git a/benchmarks/results/pr319_rocm_mi300x/distributed/strict_rccl_ag_rs_w2.json b/benchmarks/results/pr319_rocm_mi300x/distributed/strict_rccl_ag_rs_w2.json new file mode 100644 index 00000000..b7f1a00d --- /dev/null +++ b/benchmarks/results/pr319_rocm_mi300x/distributed/strict_rccl_ag_rs_w2.json @@ -0,0 +1,582 @@ +{ + "backend": "nccl", + "collective_version": [ + 2, + 28, + 9 + ], + "cp_world_size": 2, + "device_name": "AMD Instinct MI300X", + "git_commit": "3247e3ef390ac2f55bf93864ffa70b0a1350ec4c", + "global_failure_count": 0, + "platform": "rocm", + "ranks": [ + { + "accum_dtype": "fp32", + "atol": 0.0002, + "cp_rank": 0, + "cp_world_size": 2, + "device": "cuda:0", + "downcast_at": "final_write", + "dtype": "bf16", + "expected_block_manifest": [ + { + "global_block_index": 0, + "kv_block_end": 4, + "kv_block_start": 0, + "owner_cp_rank": 0, + "owner_tp_rank": 0 + }, + { + "global_block_index": 1, + "kv_block_end": 8, + "kv_block_start": 4, + "owner_cp_rank": 0, + "owner_tp_rank": 0 + }, + { + "global_block_index": 2, + "kv_block_end": 12, + "kv_block_start": 8, + "owner_cp_rank": 1, + "owner_tp_rank": 0 + }, + { + "global_block_index": 3, + "kv_block_end": 16, + "kv_block_start": 12, + "owner_cp_rank": 1, + "owner_tp_rank": 0 + } + ], + "final_out_max_abs": 0.00390625, + "final_output_dtype": "bfloat16", + "final_write_atol": 0.02, + "gathered_block_indices": [ + 0, + 1, + 2, + 3 + ], + "global_failure_count": 0, + "global_world_size": 2, + "local_block_indices": [ + 0, + 1 + ], + "lse_max_abs": 2.384185791015625e-07, + "out_max_abs": 7.152557373046875e-07, + "passed": true, + "protocol": "ag_query_local_kv_rs_out_lse", + "query_ag": "rccl_ag_rs", + "query_ag_max_abs": 0.0, + "query_range": [ + 0, + 8 + ], + "rank": 0, + "repeat_count": 3, + "repeat_lse_bitwise": true, + "repeat_manifest_bitwise": true, + "repeat_out_bitwise": true, + "repeat_query_bitwise": true, + "replica_count": 1, + "replica_index": 0, + "strict_protocol": "ag_qkv_positions_shared_core_rs_out_lse", + "strict_shared_core": { + "actual_backend": "aiter.rocm.ck_dense_mha", + "bitwise": { + "dk": true, + "dq": true, + "dv": true, + "lse": true, + "out": true + }, + "communication_autograd": true, + "communication_backend": "rccl_ag_rs", + "executed": true, + "fallback": false, + "identity_errors": [], + "max_abs": { + "dk": 0.0, + "dq": 0.0, + "dv": 0.0, + "lse": 0.0, + "out": 0.0 + }, + "native_attention_arithmetic": true, + "passed": true, + "production_ready": true, + "repeat_lse_bitwise": true, + "repeat_out_bitwise": true, + "split_kv_policy": "disabled", + "strict_core_id": "rlkernel.attention.rocm.aiter_ck_dense_mha.v1", + "strict_mode": true, + "strict_provenance": { + "accum_dtype": "fp32", + "actual_backend": "aiter.rocm.ck_dense_mha", + "adapter_backend": "flashinfer", + "aiter_api_source": "aiter.ops.mha", + "aiter_source_sha256": "db61d7ce62a907c5079830622723f0abcc7fd35816d88006e381140cb6566ff4", + "arithmetic_plan_source": "aiter.ops.mha", + "arithmetic_semantics_verified": true, + "attention_backend": "aiter.rocm.ck_dense_mha", + "attention_mode": "prefill", + "batch_invariant_claim": "strict_runtime_verified", + "causal": true, + "communication_backend": "rccl_ag_rs", + "communication_overlap": "disabled", + "compute_communication": "decoupled", + "compute_order": [ + 0, + 1 + ], + "compute_schedule": "rlkernel.attention.strict_ring_state.v1", + "cp_comm_accum_dtype": "fp32", + "cp_comm_attention_numeric_reduction": false, + "cp_comm_backend": "rccl_ag_rs", + "cp_comm_compute_communication": "decoupled", + "cp_comm_contract": "partial_out_lse_global_block_index", + "cp_comm_expected_blocks": [ + { + "global_block_index": 0, + "kv_block_end": 4, + "kv_block_start": 0, + "owner_cp_rank": 0, + "owner_tp_rank": 0 + }, + { + "global_block_index": 1, + "kv_block_end": 8, + "kv_block_start": 4, + "owner_cp_rank": 0, + "owner_tp_rank": 0 + }, + { + "global_block_index": 2, + "kv_block_end": 12, + "kv_block_start": 8, + "owner_cp_rank": 1, + "owner_tp_rank": 0 + }, + { + "global_block_index": 3, + "kv_block_end": 16, + "kv_block_start": 12, + "owner_cp_rank": 1, + "owner_tp_rank": 0 + } + ], + "cp_comm_expected_kv_token_range": [ + 0, + 16 + ], + "cp_comm_merge_order": "global_block_index", + "cp_comm_merge_root_cp_rank": 0, + "cp_comm_pattern": "ag_rs", + "cp_comm_query_token_ranges": [ + [ + 0, + 8 + ], + [ + 8, + 16 + ] + ], + "cp_comm_required": true, + "cp_comm_return_lse": true, + "cp_comm_runtime": "rccl", + "cp_comm_status": "implemented", + "cp_comm_strict_backward": "rs_out_backward_ag_then_ag_qkv_backward_rs", + "cp_comm_strict_contract": "ag_qkv_positions_shared_core_rs_out_lse", + "cp_comm_strict_kv_communication": "all_gather", + "cp_comm_strict_position_communication": "all_gather", + "cp_rank": 0, + "cp_world_size": 2, + "deterministic_backward": true, + "downcast_at": "final_write", + "fa_api_source": null, + "fa_package_version": null, + "fallback": false, + "fallback_reason": null, + "k_cache_rope_state": "post_rope", + "lse_domain": "attention", + "lse_dtype": "fp32", + "lse_exported": true, + "materialization": "ag_qkv_positions_shared_core_rs", + "merge_order_indices": [ + 0, + 1 + ], + "native_attention_arithmetic": true, + "num_splits": 1, + "platform": "rocm", + "production_ready": true, + "q_rope_state": "post_rope", + "reference_only": false, + "requested_backend": "flashinfer_layout_adapter", + "ring_partial_arithmetic": false, + "ring_schedule_default": true, + "rope_backend": "rlkernel.rocm.deterministic_rope", + "rope_fusion": false, + "rope_fusion_boundary": "rlkernel_rope_then_attention", + "rope_theta": 1000000.0, + "rotary_dim": null, + "softmax_scale": null, + "split_kv_control": "dense_non_split_api", + "strict_comm_autograd": true, + "strict_core_id": "rlkernel.attention.rocm.aiter_ck_dense_mha.v1", + "strict_core_row_plans": [ + { + "actual_split_boundaries": [ + [ + 0, + 16 + ] + ], + "actual_split_kv_count": 1, + "actual_split_kv_policy": "disabled", + "actual_split_kv_size": null, + "requested_split_kv_policy": "disabled", + "requested_split_kv_size": null, + "split_kv_accum_dtype": "fp32", + "split_kv_backend": "aiter.rocm.ck_dense_mha", + "split_kv_downcast_at": "final_write", + "split_kv_fallback": false, + "split_kv_fallback_reason": null, + "split_kv_merge_order": "global_block_index", + "split_kv_plan_source": "contract_exact" + }, + { + "actual_split_boundaries": [ + [ + 0, + 16 + ] + ], + "actual_split_kv_count": 1, + "actual_split_kv_policy": "disabled", + "actual_split_kv_size": null, + "requested_split_kv_policy": "disabled", + "requested_split_kv_size": null, + "split_kv_accum_dtype": "fp32", + "split_kv_backend": "aiter.rocm.ck_dense_mha", + "split_kv_downcast_at": "final_write", + "split_kv_fallback": false, + "split_kv_fallback_reason": null, + "split_kv_merge_order": "global_block_index", + "split_kv_plan_source": "contract_exact" + } + ], + "strict_full_qkv_all_gather": true, + "strict_local_kv_range": [ + 0, + 8 + ], + "strict_local_query_range": [ + 0, + 8 + ], + "strict_mode": true, + "strict_position_ids_all_gather": true, + "strict_schedule": "single_batch_aiter_ck_dense_mha_no_splitkv", + "strict_split_kv": "disabled", + "tp_rank": 0, + "tp_world_size": 2 + }, + "strict_schedule": "single_batch_aiter_ck_dense_mha_no_splitkv" + }, + "tp_rank": 0, + "tp_world_size": 1, + "transport": "rccl_ag_rs" + }, + { + "accum_dtype": "fp32", + "atol": 0.0002, + "cp_rank": 1, + "cp_world_size": 2, + "device": "cuda:1", + "downcast_at": "final_write", + "dtype": "bf16", + "expected_block_manifest": [ + { + "global_block_index": 0, + "kv_block_end": 4, + "kv_block_start": 0, + "owner_cp_rank": 0, + "owner_tp_rank": 0 + }, + { + "global_block_index": 1, + "kv_block_end": 8, + "kv_block_start": 4, + "owner_cp_rank": 0, + "owner_tp_rank": 0 + }, + { + "global_block_index": 2, + "kv_block_end": 12, + "kv_block_start": 8, + "owner_cp_rank": 1, + "owner_tp_rank": 0 + }, + { + "global_block_index": 3, + "kv_block_end": 16, + "kv_block_start": 12, + "owner_cp_rank": 1, + "owner_tp_rank": 0 + } + ], + "final_out_max_abs": 0.00390625, + "final_output_dtype": "bfloat16", + "final_write_atol": 0.02, + "gathered_block_indices": [ + 0, + 1, + 2, + 3 + ], + "global_failure_count": 0, + "global_world_size": 2, + "local_block_indices": [ + 2, + 3 + ], + "lse_max_abs": 4.76837158203125e-07, + "out_max_abs": 5.960464477539062e-07, + "passed": true, + "protocol": "ag_query_local_kv_rs_out_lse", + "query_ag": "rccl_ag_rs", + "query_ag_max_abs": 0.0, + "query_range": [ + 8, + 16 + ], + "rank": 1, + "repeat_count": 3, + "repeat_lse_bitwise": true, + "repeat_manifest_bitwise": true, + "repeat_out_bitwise": true, + "repeat_query_bitwise": true, + "replica_count": 1, + "replica_index": 0, + "strict_protocol": "ag_qkv_positions_shared_core_rs_out_lse", + "strict_shared_core": { + "actual_backend": "aiter.rocm.ck_dense_mha", + "bitwise": { + "dk": true, + "dq": true, + "dv": true, + "lse": true, + "out": true + }, + "communication_autograd": true, + "communication_backend": "rccl_ag_rs", + "executed": true, + "fallback": false, + "identity_errors": [], + "max_abs": { + "dk": 0.0, + "dq": 0.0, + "dv": 0.0, + "lse": 0.0, + "out": 0.0 + }, + "native_attention_arithmetic": true, + "passed": true, + "production_ready": true, + "repeat_lse_bitwise": true, + "repeat_out_bitwise": true, + "split_kv_policy": "disabled", + "strict_core_id": "rlkernel.attention.rocm.aiter_ck_dense_mha.v1", + "strict_mode": true, + "strict_provenance": { + "accum_dtype": "fp32", + "actual_backend": "aiter.rocm.ck_dense_mha", + "adapter_backend": "flashinfer", + "aiter_api_source": "aiter.ops.mha", + "aiter_source_sha256": "db61d7ce62a907c5079830622723f0abcc7fd35816d88006e381140cb6566ff4", + "arithmetic_plan_source": "aiter.ops.mha", + "arithmetic_semantics_verified": true, + "attention_backend": "aiter.rocm.ck_dense_mha", + "attention_mode": "prefill", + "batch_invariant_claim": "strict_runtime_verified", + "causal": true, + "communication_backend": "rccl_ag_rs", + "communication_overlap": "disabled", + "compute_communication": "decoupled", + "compute_order": [ + 0, + 1 + ], + "compute_schedule": "rlkernel.attention.strict_ring_state.v1", + "cp_comm_accum_dtype": "fp32", + "cp_comm_attention_numeric_reduction": false, + "cp_comm_backend": "rccl_ag_rs", + "cp_comm_compute_communication": "decoupled", + "cp_comm_contract": "partial_out_lse_global_block_index", + "cp_comm_expected_blocks": [ + { + "global_block_index": 0, + "kv_block_end": 4, + "kv_block_start": 0, + "owner_cp_rank": 0, + "owner_tp_rank": 0 + }, + { + "global_block_index": 1, + "kv_block_end": 8, + "kv_block_start": 4, + "owner_cp_rank": 0, + "owner_tp_rank": 0 + }, + { + "global_block_index": 2, + "kv_block_end": 12, + "kv_block_start": 8, + "owner_cp_rank": 1, + "owner_tp_rank": 0 + }, + { + "global_block_index": 3, + "kv_block_end": 16, + "kv_block_start": 12, + "owner_cp_rank": 1, + "owner_tp_rank": 0 + } + ], + "cp_comm_expected_kv_token_range": [ + 0, + 16 + ], + "cp_comm_merge_order": "global_block_index", + "cp_comm_merge_root_cp_rank": 0, + "cp_comm_pattern": "ag_rs", + "cp_comm_query_token_ranges": [ + [ + 0, + 8 + ], + [ + 8, + 16 + ] + ], + "cp_comm_required": true, + "cp_comm_return_lse": true, + "cp_comm_runtime": "rccl", + "cp_comm_status": "implemented", + "cp_comm_strict_backward": "rs_out_backward_ag_then_ag_qkv_backward_rs", + "cp_comm_strict_contract": "ag_qkv_positions_shared_core_rs_out_lse", + "cp_comm_strict_kv_communication": "all_gather", + "cp_comm_strict_position_communication": "all_gather", + "cp_rank": 1, + "cp_world_size": 2, + "deterministic_backward": true, + "downcast_at": "final_write", + "fa_api_source": null, + "fa_package_version": null, + "fallback": false, + "fallback_reason": null, + "k_cache_rope_state": "post_rope", + "lse_domain": "attention", + "lse_dtype": "fp32", + "lse_exported": true, + "materialization": "ag_qkv_positions_shared_core_rs", + "merge_order_indices": [ + 0, + 1 + ], + "native_attention_arithmetic": true, + "num_splits": 1, + "platform": "rocm", + "production_ready": true, + "q_rope_state": "post_rope", + "reference_only": false, + "requested_backend": "flashinfer_layout_adapter", + "ring_partial_arithmetic": false, + "ring_schedule_default": true, + "rope_backend": "rlkernel.rocm.deterministic_rope", + "rope_fusion": false, + "rope_fusion_boundary": "rlkernel_rope_then_attention", + "rope_theta": 1000000.0, + "rotary_dim": null, + "softmax_scale": null, + "split_kv_control": "dense_non_split_api", + "strict_comm_autograd": true, + "strict_core_id": "rlkernel.attention.rocm.aiter_ck_dense_mha.v1", + "strict_core_row_plans": [ + { + "actual_split_boundaries": [ + [ + 0, + 16 + ] + ], + "actual_split_kv_count": 1, + "actual_split_kv_policy": "disabled", + "actual_split_kv_size": null, + "requested_split_kv_policy": "disabled", + "requested_split_kv_size": null, + "split_kv_accum_dtype": "fp32", + "split_kv_backend": "aiter.rocm.ck_dense_mha", + "split_kv_downcast_at": "final_write", + "split_kv_fallback": false, + "split_kv_fallback_reason": null, + "split_kv_merge_order": "global_block_index", + "split_kv_plan_source": "contract_exact" + }, + { + "actual_split_boundaries": [ + [ + 0, + 16 + ] + ], + "actual_split_kv_count": 1, + "actual_split_kv_policy": "disabled", + "actual_split_kv_size": null, + "requested_split_kv_policy": "disabled", + "requested_split_kv_size": null, + "split_kv_accum_dtype": "fp32", + "split_kv_backend": "aiter.rocm.ck_dense_mha", + "split_kv_downcast_at": "final_write", + "split_kv_fallback": false, + "split_kv_fallback_reason": null, + "split_kv_merge_order": "global_block_index", + "split_kv_plan_source": "contract_exact" + } + ], + "strict_full_qkv_all_gather": true, + "strict_local_kv_range": [ + 8, + 16 + ], + "strict_local_query_range": [ + 8, + 16 + ], + "strict_mode": true, + "strict_position_ids_all_gather": true, + "strict_schedule": "single_batch_aiter_ck_dense_mha_no_splitkv", + "strict_split_kv": "disabled", + "tp_rank": 0, + "tp_world_size": 2 + }, + "strict_schedule": "single_batch_aiter_ck_dense_mha_no_splitkv" + }, + "tp_rank": 0, + "tp_world_size": 1, + "transport": "rccl_ag_rs" + } + ], + "replica_count": 1, + "runtime_version": "7.14.60850", + "schema_version": "ws2_rccl_ag_rs_attention/v2", + "torch_version": "2.12.0+rocm7.14.0a20260608", + "tp_world_size": 1, + "transport": "rccl_ag_rs", + "world_size": 2 +} diff --git a/benchmarks/results/pr319_rocm_mi300x/distributed/strict_rccl_ag_rs_w4.json b/benchmarks/results/pr319_rocm_mi300x/distributed/strict_rccl_ag_rs_w4.json new file mode 100644 index 00000000..72c267f1 --- /dev/null +++ b/benchmarks/results/pr319_rocm_mi300x/distributed/strict_rccl_ag_rs_w4.json @@ -0,0 +1,1142 @@ +{ + "backend": "nccl", + "collective_version": [ + 2, + 28, + 9 + ], + "cp_world_size": 2, + "device_name": "AMD Instinct MI300X", + "git_commit": "3247e3ef390ac2f55bf93864ffa70b0a1350ec4c", + "global_failure_count": 0, + "platform": "rocm", + "ranks": [ + { + "accum_dtype": "fp32", + "atol": 0.0002, + "cp_rank": 0, + "cp_world_size": 2, + "device": "cuda:0", + "downcast_at": "final_write", + "dtype": "bf16", + "expected_block_manifest": [ + { + "global_block_index": 0, + "kv_block_end": 4, + "kv_block_start": 0, + "owner_cp_rank": 0, + "owner_tp_rank": 0 + }, + { + "global_block_index": 1, + "kv_block_end": 8, + "kv_block_start": 4, + "owner_cp_rank": 0, + "owner_tp_rank": 0 + }, + { + "global_block_index": 2, + "kv_block_end": 12, + "kv_block_start": 8, + "owner_cp_rank": 1, + "owner_tp_rank": 0 + }, + { + "global_block_index": 3, + "kv_block_end": 16, + "kv_block_start": 12, + "owner_cp_rank": 1, + "owner_tp_rank": 0 + } + ], + "final_out_max_abs": 0.00390625, + "final_output_dtype": "bfloat16", + "final_write_atol": 0.02, + "gathered_block_indices": [ + 0, + 1, + 2, + 3 + ], + "global_failure_count": 0, + "global_world_size": 4, + "local_block_indices": [ + 0, + 1 + ], + "lse_max_abs": 2.384185791015625e-07, + "out_max_abs": 7.152557373046875e-07, + "passed": true, + "protocol": "ag_query_local_kv_rs_out_lse", + "query_ag": "rccl_ag_rs", + "query_ag_max_abs": 0.0, + "query_range": [ + 0, + 8 + ], + "rank": 0, + "repeat_count": 3, + "repeat_lse_bitwise": true, + "repeat_manifest_bitwise": true, + "repeat_out_bitwise": true, + "repeat_query_bitwise": true, + "replica_count": 1, + "replica_index": 0, + "strict_protocol": "ag_qkv_positions_shared_core_rs_out_lse", + "strict_shared_core": { + "actual_backend": "aiter.rocm.ck_dense_mha", + "bitwise": { + "dk": true, + "dq": true, + "dv": true, + "lse": true, + "out": true + }, + "communication_autograd": true, + "communication_backend": "rccl_ag_rs", + "executed": true, + "fallback": false, + "identity_errors": [], + "max_abs": { + "dk": 0.0, + "dq": 0.0, + "dv": 0.0, + "lse": 0.0, + "out": 0.0 + }, + "native_attention_arithmetic": true, + "passed": true, + "production_ready": true, + "repeat_lse_bitwise": true, + "repeat_out_bitwise": true, + "split_kv_policy": "disabled", + "strict_core_id": "rlkernel.attention.rocm.aiter_ck_dense_mha.v1", + "strict_mode": true, + "strict_provenance": { + "accum_dtype": "fp32", + "actual_backend": "aiter.rocm.ck_dense_mha", + "adapter_backend": "flashinfer", + "aiter_api_source": "aiter.ops.mha", + "aiter_source_sha256": "db61d7ce62a907c5079830622723f0abcc7fd35816d88006e381140cb6566ff4", + "arithmetic_plan_source": "aiter.ops.mha", + "arithmetic_semantics_verified": true, + "attention_backend": "aiter.rocm.ck_dense_mha", + "attention_mode": "prefill", + "batch_invariant_claim": "strict_runtime_verified", + "causal": true, + "communication_backend": "rccl_ag_rs", + "communication_overlap": "disabled", + "compute_communication": "decoupled", + "compute_order": [ + 0, + 1 + ], + "compute_schedule": "rlkernel.attention.strict_ring_state.v1", + "cp_comm_accum_dtype": "fp32", + "cp_comm_attention_numeric_reduction": false, + "cp_comm_backend": "rccl_ag_rs", + "cp_comm_compute_communication": "decoupled", + "cp_comm_contract": "partial_out_lse_global_block_index", + "cp_comm_expected_blocks": [ + { + "global_block_index": 0, + "kv_block_end": 4, + "kv_block_start": 0, + "owner_cp_rank": 0, + "owner_tp_rank": 0 + }, + { + "global_block_index": 1, + "kv_block_end": 8, + "kv_block_start": 4, + "owner_cp_rank": 0, + "owner_tp_rank": 0 + }, + { + "global_block_index": 2, + "kv_block_end": 12, + "kv_block_start": 8, + "owner_cp_rank": 1, + "owner_tp_rank": 0 + }, + { + "global_block_index": 3, + "kv_block_end": 16, + "kv_block_start": 12, + "owner_cp_rank": 1, + "owner_tp_rank": 0 + } + ], + "cp_comm_expected_kv_token_range": [ + 0, + 16 + ], + "cp_comm_merge_order": "global_block_index", + "cp_comm_merge_root_cp_rank": 0, + "cp_comm_pattern": "ag_rs", + "cp_comm_query_token_ranges": [ + [ + 0, + 8 + ], + [ + 8, + 16 + ] + ], + "cp_comm_required": true, + "cp_comm_return_lse": true, + "cp_comm_runtime": "rccl", + "cp_comm_status": "implemented", + "cp_comm_strict_backward": "rs_out_backward_ag_then_ag_qkv_backward_rs", + "cp_comm_strict_contract": "ag_qkv_positions_shared_core_rs_out_lse", + "cp_comm_strict_kv_communication": "all_gather", + "cp_comm_strict_position_communication": "all_gather", + "cp_rank": 0, + "cp_world_size": 2, + "deterministic_backward": true, + "downcast_at": "final_write", + "fa_api_source": null, + "fa_package_version": null, + "fallback": false, + "fallback_reason": null, + "k_cache_rope_state": "post_rope", + "lse_domain": "attention", + "lse_dtype": "fp32", + "lse_exported": true, + "materialization": "ag_qkv_positions_shared_core_rs", + "merge_order_indices": [ + 0, + 1 + ], + "native_attention_arithmetic": true, + "num_splits": 1, + "platform": "rocm", + "production_ready": true, + "q_rope_state": "post_rope", + "reference_only": false, + "requested_backend": "flashinfer_layout_adapter", + "ring_partial_arithmetic": false, + "ring_schedule_default": true, + "rope_backend": "rlkernel.rocm.deterministic_rope", + "rope_fusion": false, + "rope_fusion_boundary": "rlkernel_rope_then_attention", + "rope_theta": 1000000.0, + "rotary_dim": null, + "softmax_scale": null, + "split_kv_control": "dense_non_split_api", + "strict_comm_autograd": true, + "strict_core_id": "rlkernel.attention.rocm.aiter_ck_dense_mha.v1", + "strict_core_row_plans": [ + { + "actual_split_boundaries": [ + [ + 0, + 16 + ] + ], + "actual_split_kv_count": 1, + "actual_split_kv_policy": "disabled", + "actual_split_kv_size": null, + "requested_split_kv_policy": "disabled", + "requested_split_kv_size": null, + "split_kv_accum_dtype": "fp32", + "split_kv_backend": "aiter.rocm.ck_dense_mha", + "split_kv_downcast_at": "final_write", + "split_kv_fallback": false, + "split_kv_fallback_reason": null, + "split_kv_merge_order": "global_block_index", + "split_kv_plan_source": "contract_exact" + }, + { + "actual_split_boundaries": [ + [ + 0, + 16 + ] + ], + "actual_split_kv_count": 1, + "actual_split_kv_policy": "disabled", + "actual_split_kv_size": null, + "requested_split_kv_policy": "disabled", + "requested_split_kv_size": null, + "split_kv_accum_dtype": "fp32", + "split_kv_backend": "aiter.rocm.ck_dense_mha", + "split_kv_downcast_at": "final_write", + "split_kv_fallback": false, + "split_kv_fallback_reason": null, + "split_kv_merge_order": "global_block_index", + "split_kv_plan_source": "contract_exact" + } + ], + "strict_full_qkv_all_gather": true, + "strict_local_kv_range": [ + 0, + 8 + ], + "strict_local_query_range": [ + 0, + 8 + ], + "strict_mode": true, + "strict_position_ids_all_gather": true, + "strict_schedule": "single_batch_aiter_ck_dense_mha_no_splitkv", + "strict_split_kv": "disabled", + "tp_rank": 0, + "tp_world_size": 2 + }, + "strict_schedule": "single_batch_aiter_ck_dense_mha_no_splitkv" + }, + "tp_rank": 0, + "tp_world_size": 2, + "transport": "rccl_ag_rs" + }, + { + "accum_dtype": "fp32", + "atol": 0.0002, + "cp_rank": 1, + "cp_world_size": 2, + "device": "cuda:1", + "downcast_at": "final_write", + "dtype": "bf16", + "expected_block_manifest": [ + { + "global_block_index": 0, + "kv_block_end": 4, + "kv_block_start": 0, + "owner_cp_rank": 0, + "owner_tp_rank": 0 + }, + { + "global_block_index": 1, + "kv_block_end": 8, + "kv_block_start": 4, + "owner_cp_rank": 0, + "owner_tp_rank": 0 + }, + { + "global_block_index": 2, + "kv_block_end": 12, + "kv_block_start": 8, + "owner_cp_rank": 1, + "owner_tp_rank": 0 + }, + { + "global_block_index": 3, + "kv_block_end": 16, + "kv_block_start": 12, + "owner_cp_rank": 1, + "owner_tp_rank": 0 + } + ], + "final_out_max_abs": 0.00390625, + "final_output_dtype": "bfloat16", + "final_write_atol": 0.02, + "gathered_block_indices": [ + 0, + 1, + 2, + 3 + ], + "global_failure_count": 0, + "global_world_size": 4, + "local_block_indices": [ + 2, + 3 + ], + "lse_max_abs": 4.76837158203125e-07, + "out_max_abs": 5.960464477539062e-07, + "passed": true, + "protocol": "ag_query_local_kv_rs_out_lse", + "query_ag": "rccl_ag_rs", + "query_ag_max_abs": 0.0, + "query_range": [ + 8, + 16 + ], + "rank": 1, + "repeat_count": 3, + "repeat_lse_bitwise": true, + "repeat_manifest_bitwise": true, + "repeat_out_bitwise": true, + "repeat_query_bitwise": true, + "replica_count": 1, + "replica_index": 0, + "strict_protocol": "ag_qkv_positions_shared_core_rs_out_lse", + "strict_shared_core": { + "actual_backend": "aiter.rocm.ck_dense_mha", + "bitwise": { + "dk": true, + "dq": true, + "dv": true, + "lse": true, + "out": true + }, + "communication_autograd": true, + "communication_backend": "rccl_ag_rs", + "executed": true, + "fallback": false, + "identity_errors": [], + "max_abs": { + "dk": 0.0, + "dq": 0.0, + "dv": 0.0, + "lse": 0.0, + "out": 0.0 + }, + "native_attention_arithmetic": true, + "passed": true, + "production_ready": true, + "repeat_lse_bitwise": true, + "repeat_out_bitwise": true, + "split_kv_policy": "disabled", + "strict_core_id": "rlkernel.attention.rocm.aiter_ck_dense_mha.v1", + "strict_mode": true, + "strict_provenance": { + "accum_dtype": "fp32", + "actual_backend": "aiter.rocm.ck_dense_mha", + "adapter_backend": "flashinfer", + "aiter_api_source": "aiter.ops.mha", + "aiter_source_sha256": "db61d7ce62a907c5079830622723f0abcc7fd35816d88006e381140cb6566ff4", + "arithmetic_plan_source": "aiter.ops.mha", + "arithmetic_semantics_verified": true, + "attention_backend": "aiter.rocm.ck_dense_mha", + "attention_mode": "prefill", + "batch_invariant_claim": "strict_runtime_verified", + "causal": true, + "communication_backend": "rccl_ag_rs", + "communication_overlap": "disabled", + "compute_communication": "decoupled", + "compute_order": [ + 0, + 1 + ], + "compute_schedule": "rlkernel.attention.strict_ring_state.v1", + "cp_comm_accum_dtype": "fp32", + "cp_comm_attention_numeric_reduction": false, + "cp_comm_backend": "rccl_ag_rs", + "cp_comm_compute_communication": "decoupled", + "cp_comm_contract": "partial_out_lse_global_block_index", + "cp_comm_expected_blocks": [ + { + "global_block_index": 0, + "kv_block_end": 4, + "kv_block_start": 0, + "owner_cp_rank": 0, + "owner_tp_rank": 0 + }, + { + "global_block_index": 1, + "kv_block_end": 8, + "kv_block_start": 4, + "owner_cp_rank": 0, + "owner_tp_rank": 0 + }, + { + "global_block_index": 2, + "kv_block_end": 12, + "kv_block_start": 8, + "owner_cp_rank": 1, + "owner_tp_rank": 0 + }, + { + "global_block_index": 3, + "kv_block_end": 16, + "kv_block_start": 12, + "owner_cp_rank": 1, + "owner_tp_rank": 0 + } + ], + "cp_comm_expected_kv_token_range": [ + 0, + 16 + ], + "cp_comm_merge_order": "global_block_index", + "cp_comm_merge_root_cp_rank": 0, + "cp_comm_pattern": "ag_rs", + "cp_comm_query_token_ranges": [ + [ + 0, + 8 + ], + [ + 8, + 16 + ] + ], + "cp_comm_required": true, + "cp_comm_return_lse": true, + "cp_comm_runtime": "rccl", + "cp_comm_status": "implemented", + "cp_comm_strict_backward": "rs_out_backward_ag_then_ag_qkv_backward_rs", + "cp_comm_strict_contract": "ag_qkv_positions_shared_core_rs_out_lse", + "cp_comm_strict_kv_communication": "all_gather", + "cp_comm_strict_position_communication": "all_gather", + "cp_rank": 1, + "cp_world_size": 2, + "deterministic_backward": true, + "downcast_at": "final_write", + "fa_api_source": null, + "fa_package_version": null, + "fallback": false, + "fallback_reason": null, + "k_cache_rope_state": "post_rope", + "lse_domain": "attention", + "lse_dtype": "fp32", + "lse_exported": true, + "materialization": "ag_qkv_positions_shared_core_rs", + "merge_order_indices": [ + 0, + 1 + ], + "native_attention_arithmetic": true, + "num_splits": 1, + "platform": "rocm", + "production_ready": true, + "q_rope_state": "post_rope", + "reference_only": false, + "requested_backend": "flashinfer_layout_adapter", + "ring_partial_arithmetic": false, + "ring_schedule_default": true, + "rope_backend": "rlkernel.rocm.deterministic_rope", + "rope_fusion": false, + "rope_fusion_boundary": "rlkernel_rope_then_attention", + "rope_theta": 1000000.0, + "rotary_dim": null, + "softmax_scale": null, + "split_kv_control": "dense_non_split_api", + "strict_comm_autograd": true, + "strict_core_id": "rlkernel.attention.rocm.aiter_ck_dense_mha.v1", + "strict_core_row_plans": [ + { + "actual_split_boundaries": [ + [ + 0, + 16 + ] + ], + "actual_split_kv_count": 1, + "actual_split_kv_policy": "disabled", + "actual_split_kv_size": null, + "requested_split_kv_policy": "disabled", + "requested_split_kv_size": null, + "split_kv_accum_dtype": "fp32", + "split_kv_backend": "aiter.rocm.ck_dense_mha", + "split_kv_downcast_at": "final_write", + "split_kv_fallback": false, + "split_kv_fallback_reason": null, + "split_kv_merge_order": "global_block_index", + "split_kv_plan_source": "contract_exact" + }, + { + "actual_split_boundaries": [ + [ + 0, + 16 + ] + ], + "actual_split_kv_count": 1, + "actual_split_kv_policy": "disabled", + "actual_split_kv_size": null, + "requested_split_kv_policy": "disabled", + "requested_split_kv_size": null, + "split_kv_accum_dtype": "fp32", + "split_kv_backend": "aiter.rocm.ck_dense_mha", + "split_kv_downcast_at": "final_write", + "split_kv_fallback": false, + "split_kv_fallback_reason": null, + "split_kv_merge_order": "global_block_index", + "split_kv_plan_source": "contract_exact" + } + ], + "strict_full_qkv_all_gather": true, + "strict_local_kv_range": [ + 8, + 16 + ], + "strict_local_query_range": [ + 8, + 16 + ], + "strict_mode": true, + "strict_position_ids_all_gather": true, + "strict_schedule": "single_batch_aiter_ck_dense_mha_no_splitkv", + "strict_split_kv": "disabled", + "tp_rank": 0, + "tp_world_size": 2 + }, + "strict_schedule": "single_batch_aiter_ck_dense_mha_no_splitkv" + }, + "tp_rank": 0, + "tp_world_size": 2, + "transport": "rccl_ag_rs" + }, + { + "accum_dtype": "fp32", + "atol": 0.0002, + "cp_rank": 0, + "cp_world_size": 2, + "device": "cuda:2", + "downcast_at": "final_write", + "dtype": "bf16", + "expected_block_manifest": [ + { + "global_block_index": 0, + "kv_block_end": 4, + "kv_block_start": 0, + "owner_cp_rank": 0, + "owner_tp_rank": 1 + }, + { + "global_block_index": 1, + "kv_block_end": 8, + "kv_block_start": 4, + "owner_cp_rank": 0, + "owner_tp_rank": 1 + }, + { + "global_block_index": 2, + "kv_block_end": 12, + "kv_block_start": 8, + "owner_cp_rank": 1, + "owner_tp_rank": 1 + }, + { + "global_block_index": 3, + "kv_block_end": 16, + "kv_block_start": 12, + "owner_cp_rank": 1, + "owner_tp_rank": 1 + } + ], + "final_out_max_abs": 0.001953125, + "final_output_dtype": "bfloat16", + "final_write_atol": 0.02, + "gathered_block_indices": [ + 0, + 1, + 2, + 3 + ], + "global_failure_count": 0, + "global_world_size": 4, + "local_block_indices": [ + 0, + 1 + ], + "lse_max_abs": 2.384185791015625e-07, + "out_max_abs": 7.152557373046875e-07, + "passed": true, + "protocol": "ag_query_local_kv_rs_out_lse", + "query_ag": "rccl_ag_rs", + "query_ag_max_abs": 0.0, + "query_range": [ + 0, + 8 + ], + "rank": 2, + "repeat_count": 3, + "repeat_lse_bitwise": true, + "repeat_manifest_bitwise": true, + "repeat_out_bitwise": true, + "repeat_query_bitwise": true, + "replica_count": 1, + "replica_index": 0, + "strict_protocol": "ag_qkv_positions_shared_core_rs_out_lse", + "strict_shared_core": { + "actual_backend": "aiter.rocm.ck_dense_mha", + "bitwise": { + "dk": true, + "dq": true, + "dv": true, + "lse": true, + "out": true + }, + "communication_autograd": true, + "communication_backend": "rccl_ag_rs", + "executed": true, + "fallback": false, + "identity_errors": [], + "max_abs": { + "dk": 0.0, + "dq": 0.0, + "dv": 0.0, + "lse": 0.0, + "out": 0.0 + }, + "native_attention_arithmetic": true, + "passed": true, + "production_ready": true, + "repeat_lse_bitwise": true, + "repeat_out_bitwise": true, + "split_kv_policy": "disabled", + "strict_core_id": "rlkernel.attention.rocm.aiter_ck_dense_mha.v1", + "strict_mode": true, + "strict_provenance": { + "accum_dtype": "fp32", + "actual_backend": "aiter.rocm.ck_dense_mha", + "adapter_backend": "flashinfer", + "aiter_api_source": "aiter.ops.mha", + "aiter_source_sha256": "db61d7ce62a907c5079830622723f0abcc7fd35816d88006e381140cb6566ff4", + "arithmetic_plan_source": "aiter.ops.mha", + "arithmetic_semantics_verified": true, + "attention_backend": "aiter.rocm.ck_dense_mha", + "attention_mode": "prefill", + "batch_invariant_claim": "strict_runtime_verified", + "causal": true, + "communication_backend": "rccl_ag_rs", + "communication_overlap": "disabled", + "compute_communication": "decoupled", + "compute_order": [ + 0, + 1 + ], + "compute_schedule": "rlkernel.attention.strict_ring_state.v1", + "cp_comm_accum_dtype": "fp32", + "cp_comm_attention_numeric_reduction": false, + "cp_comm_backend": "rccl_ag_rs", + "cp_comm_compute_communication": "decoupled", + "cp_comm_contract": "partial_out_lse_global_block_index", + "cp_comm_expected_blocks": [ + { + "global_block_index": 0, + "kv_block_end": 4, + "kv_block_start": 0, + "owner_cp_rank": 0, + "owner_tp_rank": 1 + }, + { + "global_block_index": 1, + "kv_block_end": 8, + "kv_block_start": 4, + "owner_cp_rank": 0, + "owner_tp_rank": 1 + }, + { + "global_block_index": 2, + "kv_block_end": 12, + "kv_block_start": 8, + "owner_cp_rank": 1, + "owner_tp_rank": 1 + }, + { + "global_block_index": 3, + "kv_block_end": 16, + "kv_block_start": 12, + "owner_cp_rank": 1, + "owner_tp_rank": 1 + } + ], + "cp_comm_expected_kv_token_range": [ + 0, + 16 + ], + "cp_comm_merge_order": "global_block_index", + "cp_comm_merge_root_cp_rank": 0, + "cp_comm_pattern": "ag_rs", + "cp_comm_query_token_ranges": [ + [ + 0, + 8 + ], + [ + 8, + 16 + ] + ], + "cp_comm_required": true, + "cp_comm_return_lse": true, + "cp_comm_runtime": "rccl", + "cp_comm_status": "implemented", + "cp_comm_strict_backward": "rs_out_backward_ag_then_ag_qkv_backward_rs", + "cp_comm_strict_contract": "ag_qkv_positions_shared_core_rs_out_lse", + "cp_comm_strict_kv_communication": "all_gather", + "cp_comm_strict_position_communication": "all_gather", + "cp_rank": 0, + "cp_world_size": 2, + "deterministic_backward": true, + "downcast_at": "final_write", + "fa_api_source": null, + "fa_package_version": null, + "fallback": false, + "fallback_reason": null, + "k_cache_rope_state": "post_rope", + "lse_domain": "attention", + "lse_dtype": "fp32", + "lse_exported": true, + "materialization": "ag_qkv_positions_shared_core_rs", + "merge_order_indices": [ + 0, + 1 + ], + "native_attention_arithmetic": true, + "num_splits": 1, + "platform": "rocm", + "production_ready": true, + "q_rope_state": "post_rope", + "reference_only": false, + "requested_backend": "flashinfer_layout_adapter", + "ring_partial_arithmetic": false, + "ring_schedule_default": true, + "rope_backend": "rlkernel.rocm.deterministic_rope", + "rope_fusion": false, + "rope_fusion_boundary": "rlkernel_rope_then_attention", + "rope_theta": 1000000.0, + "rotary_dim": null, + "softmax_scale": null, + "split_kv_control": "dense_non_split_api", + "strict_comm_autograd": true, + "strict_core_id": "rlkernel.attention.rocm.aiter_ck_dense_mha.v1", + "strict_core_row_plans": [ + { + "actual_split_boundaries": [ + [ + 0, + 16 + ] + ], + "actual_split_kv_count": 1, + "actual_split_kv_policy": "disabled", + "actual_split_kv_size": null, + "requested_split_kv_policy": "disabled", + "requested_split_kv_size": null, + "split_kv_accum_dtype": "fp32", + "split_kv_backend": "aiter.rocm.ck_dense_mha", + "split_kv_downcast_at": "final_write", + "split_kv_fallback": false, + "split_kv_fallback_reason": null, + "split_kv_merge_order": "global_block_index", + "split_kv_plan_source": "contract_exact" + }, + { + "actual_split_boundaries": [ + [ + 0, + 16 + ] + ], + "actual_split_kv_count": 1, + "actual_split_kv_policy": "disabled", + "actual_split_kv_size": null, + "requested_split_kv_policy": "disabled", + "requested_split_kv_size": null, + "split_kv_accum_dtype": "fp32", + "split_kv_backend": "aiter.rocm.ck_dense_mha", + "split_kv_downcast_at": "final_write", + "split_kv_fallback": false, + "split_kv_fallback_reason": null, + "split_kv_merge_order": "global_block_index", + "split_kv_plan_source": "contract_exact" + } + ], + "strict_full_qkv_all_gather": true, + "strict_local_kv_range": [ + 0, + 8 + ], + "strict_local_query_range": [ + 0, + 8 + ], + "strict_mode": true, + "strict_position_ids_all_gather": true, + "strict_schedule": "single_batch_aiter_ck_dense_mha_no_splitkv", + "strict_split_kv": "disabled", + "tp_rank": 1, + "tp_world_size": 2 + }, + "strict_schedule": "single_batch_aiter_ck_dense_mha_no_splitkv" + }, + "tp_rank": 1, + "tp_world_size": 2, + "transport": "rccl_ag_rs" + }, + { + "accum_dtype": "fp32", + "atol": 0.0002, + "cp_rank": 1, + "cp_world_size": 2, + "device": "cuda:3", + "downcast_at": "final_write", + "dtype": "bf16", + "expected_block_manifest": [ + { + "global_block_index": 0, + "kv_block_end": 4, + "kv_block_start": 0, + "owner_cp_rank": 0, + "owner_tp_rank": 1 + }, + { + "global_block_index": 1, + "kv_block_end": 8, + "kv_block_start": 4, + "owner_cp_rank": 0, + "owner_tp_rank": 1 + }, + { + "global_block_index": 2, + "kv_block_end": 12, + "kv_block_start": 8, + "owner_cp_rank": 1, + "owner_tp_rank": 1 + }, + { + "global_block_index": 3, + "kv_block_end": 16, + "kv_block_start": 12, + "owner_cp_rank": 1, + "owner_tp_rank": 1 + } + ], + "final_out_max_abs": 3.814697265625e-06, + "final_output_dtype": "bfloat16", + "final_write_atol": 0.02, + "gathered_block_indices": [ + 0, + 1, + 2, + 3 + ], + "global_failure_count": 0, + "global_world_size": 4, + "local_block_indices": [ + 2, + 3 + ], + "lse_max_abs": 4.76837158203125e-07, + "out_max_abs": 7.152557373046875e-07, + "passed": true, + "protocol": "ag_query_local_kv_rs_out_lse", + "query_ag": "rccl_ag_rs", + "query_ag_max_abs": 0.0, + "query_range": [ + 8, + 16 + ], + "rank": 3, + "repeat_count": 3, + "repeat_lse_bitwise": true, + "repeat_manifest_bitwise": true, + "repeat_out_bitwise": true, + "repeat_query_bitwise": true, + "replica_count": 1, + "replica_index": 0, + "strict_protocol": "ag_qkv_positions_shared_core_rs_out_lse", + "strict_shared_core": { + "actual_backend": "aiter.rocm.ck_dense_mha", + "bitwise": { + "dk": true, + "dq": true, + "dv": true, + "lse": true, + "out": true + }, + "communication_autograd": true, + "communication_backend": "rccl_ag_rs", + "executed": true, + "fallback": false, + "identity_errors": [], + "max_abs": { + "dk": 0.0, + "dq": 0.0, + "dv": 0.0, + "lse": 0.0, + "out": 0.0 + }, + "native_attention_arithmetic": true, + "passed": true, + "production_ready": true, + "repeat_lse_bitwise": true, + "repeat_out_bitwise": true, + "split_kv_policy": "disabled", + "strict_core_id": "rlkernel.attention.rocm.aiter_ck_dense_mha.v1", + "strict_mode": true, + "strict_provenance": { + "accum_dtype": "fp32", + "actual_backend": "aiter.rocm.ck_dense_mha", + "adapter_backend": "flashinfer", + "aiter_api_source": "aiter.ops.mha", + "aiter_source_sha256": "db61d7ce62a907c5079830622723f0abcc7fd35816d88006e381140cb6566ff4", + "arithmetic_plan_source": "aiter.ops.mha", + "arithmetic_semantics_verified": true, + "attention_backend": "aiter.rocm.ck_dense_mha", + "attention_mode": "prefill", + "batch_invariant_claim": "strict_runtime_verified", + "causal": true, + "communication_backend": "rccl_ag_rs", + "communication_overlap": "disabled", + "compute_communication": "decoupled", + "compute_order": [ + 0, + 1 + ], + "compute_schedule": "rlkernel.attention.strict_ring_state.v1", + "cp_comm_accum_dtype": "fp32", + "cp_comm_attention_numeric_reduction": false, + "cp_comm_backend": "rccl_ag_rs", + "cp_comm_compute_communication": "decoupled", + "cp_comm_contract": "partial_out_lse_global_block_index", + "cp_comm_expected_blocks": [ + { + "global_block_index": 0, + "kv_block_end": 4, + "kv_block_start": 0, + "owner_cp_rank": 0, + "owner_tp_rank": 1 + }, + { + "global_block_index": 1, + "kv_block_end": 8, + "kv_block_start": 4, + "owner_cp_rank": 0, + "owner_tp_rank": 1 + }, + { + "global_block_index": 2, + "kv_block_end": 12, + "kv_block_start": 8, + "owner_cp_rank": 1, + "owner_tp_rank": 1 + }, + { + "global_block_index": 3, + "kv_block_end": 16, + "kv_block_start": 12, + "owner_cp_rank": 1, + "owner_tp_rank": 1 + } + ], + "cp_comm_expected_kv_token_range": [ + 0, + 16 + ], + "cp_comm_merge_order": "global_block_index", + "cp_comm_merge_root_cp_rank": 0, + "cp_comm_pattern": "ag_rs", + "cp_comm_query_token_ranges": [ + [ + 0, + 8 + ], + [ + 8, + 16 + ] + ], + "cp_comm_required": true, + "cp_comm_return_lse": true, + "cp_comm_runtime": "rccl", + "cp_comm_status": "implemented", + "cp_comm_strict_backward": "rs_out_backward_ag_then_ag_qkv_backward_rs", + "cp_comm_strict_contract": "ag_qkv_positions_shared_core_rs_out_lse", + "cp_comm_strict_kv_communication": "all_gather", + "cp_comm_strict_position_communication": "all_gather", + "cp_rank": 1, + "cp_world_size": 2, + "deterministic_backward": true, + "downcast_at": "final_write", + "fa_api_source": null, + "fa_package_version": null, + "fallback": false, + "fallback_reason": null, + "k_cache_rope_state": "post_rope", + "lse_domain": "attention", + "lse_dtype": "fp32", + "lse_exported": true, + "materialization": "ag_qkv_positions_shared_core_rs", + "merge_order_indices": [ + 0, + 1 + ], + "native_attention_arithmetic": true, + "num_splits": 1, + "platform": "rocm", + "production_ready": true, + "q_rope_state": "post_rope", + "reference_only": false, + "requested_backend": "flashinfer_layout_adapter", + "ring_partial_arithmetic": false, + "ring_schedule_default": true, + "rope_backend": "rlkernel.rocm.deterministic_rope", + "rope_fusion": false, + "rope_fusion_boundary": "rlkernel_rope_then_attention", + "rope_theta": 1000000.0, + "rotary_dim": null, + "softmax_scale": null, + "split_kv_control": "dense_non_split_api", + "strict_comm_autograd": true, + "strict_core_id": "rlkernel.attention.rocm.aiter_ck_dense_mha.v1", + "strict_core_row_plans": [ + { + "actual_split_boundaries": [ + [ + 0, + 16 + ] + ], + "actual_split_kv_count": 1, + "actual_split_kv_policy": "disabled", + "actual_split_kv_size": null, + "requested_split_kv_policy": "disabled", + "requested_split_kv_size": null, + "split_kv_accum_dtype": "fp32", + "split_kv_backend": "aiter.rocm.ck_dense_mha", + "split_kv_downcast_at": "final_write", + "split_kv_fallback": false, + "split_kv_fallback_reason": null, + "split_kv_merge_order": "global_block_index", + "split_kv_plan_source": "contract_exact" + }, + { + "actual_split_boundaries": [ + [ + 0, + 16 + ] + ], + "actual_split_kv_count": 1, + "actual_split_kv_policy": "disabled", + "actual_split_kv_size": null, + "requested_split_kv_policy": "disabled", + "requested_split_kv_size": null, + "split_kv_accum_dtype": "fp32", + "split_kv_backend": "aiter.rocm.ck_dense_mha", + "split_kv_downcast_at": "final_write", + "split_kv_fallback": false, + "split_kv_fallback_reason": null, + "split_kv_merge_order": "global_block_index", + "split_kv_plan_source": "contract_exact" + } + ], + "strict_full_qkv_all_gather": true, + "strict_local_kv_range": [ + 8, + 16 + ], + "strict_local_query_range": [ + 8, + 16 + ], + "strict_mode": true, + "strict_position_ids_all_gather": true, + "strict_schedule": "single_batch_aiter_ck_dense_mha_no_splitkv", + "strict_split_kv": "disabled", + "tp_rank": 1, + "tp_world_size": 2 + }, + "strict_schedule": "single_batch_aiter_ck_dense_mha_no_splitkv" + }, + "tp_rank": 1, + "tp_world_size": 2, + "transport": "rccl_ag_rs" + } + ], + "replica_count": 1, + "runtime_version": "7.14.60850", + "schema_version": "ws2_rccl_ag_rs_attention/v2", + "torch_version": "2.12.0+rocm7.14.0a20260608", + "tp_world_size": 2, + "transport": "rccl_ag_rs", + "world_size": 4 +} diff --git a/benchmarks/results/pr319_rocm_mi300x/distributed/strict_rccl_ag_rs_w8.json b/benchmarks/results/pr319_rocm_mi300x/distributed/strict_rccl_ag_rs_w8.json new file mode 100644 index 00000000..0c9a7d1a --- /dev/null +++ b/benchmarks/results/pr319_rocm_mi300x/distributed/strict_rccl_ag_rs_w8.json @@ -0,0 +1,2262 @@ +{ + "backend": "nccl", + "collective_version": [ + 2, + 28, + 9 + ], + "cp_world_size": 2, + "device_name": "AMD Instinct MI300X", + "git_commit": "3247e3ef390ac2f55bf93864ffa70b0a1350ec4c", + "global_failure_count": 0, + "platform": "rocm", + "ranks": [ + { + "accum_dtype": "fp32", + "atol": 0.0002, + "cp_rank": 0, + "cp_world_size": 2, + "device": "cuda:0", + "downcast_at": "final_write", + "dtype": "bf16", + "expected_block_manifest": [ + { + "global_block_index": 0, + "kv_block_end": 4, + "kv_block_start": 0, + "owner_cp_rank": 0, + "owner_tp_rank": 0 + }, + { + "global_block_index": 1, + "kv_block_end": 8, + "kv_block_start": 4, + "owner_cp_rank": 0, + "owner_tp_rank": 0 + }, + { + "global_block_index": 2, + "kv_block_end": 12, + "kv_block_start": 8, + "owner_cp_rank": 1, + "owner_tp_rank": 0 + }, + { + "global_block_index": 3, + "kv_block_end": 16, + "kv_block_start": 12, + "owner_cp_rank": 1, + "owner_tp_rank": 0 + } + ], + "final_out_max_abs": 0.00390625, + "final_output_dtype": "bfloat16", + "final_write_atol": 0.02, + "gathered_block_indices": [ + 0, + 1, + 2, + 3 + ], + "global_failure_count": 0, + "global_world_size": 8, + "local_block_indices": [ + 0, + 1 + ], + "lse_max_abs": 2.384185791015625e-07, + "out_max_abs": 7.152557373046875e-07, + "passed": true, + "protocol": "ag_query_local_kv_rs_out_lse", + "query_ag": "rccl_ag_rs", + "query_ag_max_abs": 0.0, + "query_range": [ + 0, + 8 + ], + "rank": 0, + "repeat_count": 3, + "repeat_lse_bitwise": true, + "repeat_manifest_bitwise": true, + "repeat_out_bitwise": true, + "repeat_query_bitwise": true, + "replica_count": 2, + "replica_index": 0, + "strict_protocol": "ag_qkv_positions_shared_core_rs_out_lse", + "strict_shared_core": { + "actual_backend": "aiter.rocm.ck_dense_mha", + "bitwise": { + "dk": true, + "dq": true, + "dv": true, + "lse": true, + "out": true + }, + "communication_autograd": true, + "communication_backend": "rccl_ag_rs", + "executed": true, + "fallback": false, + "identity_errors": [], + "max_abs": { + "dk": 0.0, + "dq": 0.0, + "dv": 0.0, + "lse": 0.0, + "out": 0.0 + }, + "native_attention_arithmetic": true, + "passed": true, + "production_ready": true, + "repeat_lse_bitwise": true, + "repeat_out_bitwise": true, + "split_kv_policy": "disabled", + "strict_core_id": "rlkernel.attention.rocm.aiter_ck_dense_mha.v1", + "strict_mode": true, + "strict_provenance": { + "accum_dtype": "fp32", + "actual_backend": "aiter.rocm.ck_dense_mha", + "adapter_backend": "flashinfer", + "aiter_api_source": "aiter.ops.mha", + "aiter_source_sha256": "db61d7ce62a907c5079830622723f0abcc7fd35816d88006e381140cb6566ff4", + "arithmetic_plan_source": "aiter.ops.mha", + "arithmetic_semantics_verified": true, + "attention_backend": "aiter.rocm.ck_dense_mha", + "attention_mode": "prefill", + "batch_invariant_claim": "strict_runtime_verified", + "causal": true, + "communication_backend": "rccl_ag_rs", + "communication_overlap": "disabled", + "compute_communication": "decoupled", + "compute_order": [ + 0, + 1 + ], + "compute_schedule": "rlkernel.attention.strict_ring_state.v1", + "cp_comm_accum_dtype": "fp32", + "cp_comm_attention_numeric_reduction": false, + "cp_comm_backend": "rccl_ag_rs", + "cp_comm_compute_communication": "decoupled", + "cp_comm_contract": "partial_out_lse_global_block_index", + "cp_comm_expected_blocks": [ + { + "global_block_index": 0, + "kv_block_end": 4, + "kv_block_start": 0, + "owner_cp_rank": 0, + "owner_tp_rank": 0 + }, + { + "global_block_index": 1, + "kv_block_end": 8, + "kv_block_start": 4, + "owner_cp_rank": 0, + "owner_tp_rank": 0 + }, + { + "global_block_index": 2, + "kv_block_end": 12, + "kv_block_start": 8, + "owner_cp_rank": 1, + "owner_tp_rank": 0 + }, + { + "global_block_index": 3, + "kv_block_end": 16, + "kv_block_start": 12, + "owner_cp_rank": 1, + "owner_tp_rank": 0 + } + ], + "cp_comm_expected_kv_token_range": [ + 0, + 16 + ], + "cp_comm_merge_order": "global_block_index", + "cp_comm_merge_root_cp_rank": 0, + "cp_comm_pattern": "ag_rs", + "cp_comm_query_token_ranges": [ + [ + 0, + 8 + ], + [ + 8, + 16 + ] + ], + "cp_comm_required": true, + "cp_comm_return_lse": true, + "cp_comm_runtime": "rccl", + "cp_comm_status": "implemented", + "cp_comm_strict_backward": "rs_out_backward_ag_then_ag_qkv_backward_rs", + "cp_comm_strict_contract": "ag_qkv_positions_shared_core_rs_out_lse", + "cp_comm_strict_kv_communication": "all_gather", + "cp_comm_strict_position_communication": "all_gather", + "cp_rank": 0, + "cp_world_size": 2, + "deterministic_backward": true, + "downcast_at": "final_write", + "fa_api_source": null, + "fa_package_version": null, + "fallback": false, + "fallback_reason": null, + "k_cache_rope_state": "post_rope", + "lse_domain": "attention", + "lse_dtype": "fp32", + "lse_exported": true, + "materialization": "ag_qkv_positions_shared_core_rs", + "merge_order_indices": [ + 0, + 1 + ], + "native_attention_arithmetic": true, + "num_splits": 1, + "platform": "rocm", + "production_ready": true, + "q_rope_state": "post_rope", + "reference_only": false, + "requested_backend": "flashinfer_layout_adapter", + "ring_partial_arithmetic": false, + "ring_schedule_default": true, + "rope_backend": "rlkernel.rocm.deterministic_rope", + "rope_fusion": false, + "rope_fusion_boundary": "rlkernel_rope_then_attention", + "rope_theta": 1000000.0, + "rotary_dim": null, + "softmax_scale": null, + "split_kv_control": "dense_non_split_api", + "strict_comm_autograd": true, + "strict_core_id": "rlkernel.attention.rocm.aiter_ck_dense_mha.v1", + "strict_core_row_plans": [ + { + "actual_split_boundaries": [ + [ + 0, + 16 + ] + ], + "actual_split_kv_count": 1, + "actual_split_kv_policy": "disabled", + "actual_split_kv_size": null, + "requested_split_kv_policy": "disabled", + "requested_split_kv_size": null, + "split_kv_accum_dtype": "fp32", + "split_kv_backend": "aiter.rocm.ck_dense_mha", + "split_kv_downcast_at": "final_write", + "split_kv_fallback": false, + "split_kv_fallback_reason": null, + "split_kv_merge_order": "global_block_index", + "split_kv_plan_source": "contract_exact" + }, + { + "actual_split_boundaries": [ + [ + 0, + 16 + ] + ], + "actual_split_kv_count": 1, + "actual_split_kv_policy": "disabled", + "actual_split_kv_size": null, + "requested_split_kv_policy": "disabled", + "requested_split_kv_size": null, + "split_kv_accum_dtype": "fp32", + "split_kv_backend": "aiter.rocm.ck_dense_mha", + "split_kv_downcast_at": "final_write", + "split_kv_fallback": false, + "split_kv_fallback_reason": null, + "split_kv_merge_order": "global_block_index", + "split_kv_plan_source": "contract_exact" + } + ], + "strict_full_qkv_all_gather": true, + "strict_local_kv_range": [ + 0, + 8 + ], + "strict_local_query_range": [ + 0, + 8 + ], + "strict_mode": true, + "strict_position_ids_all_gather": true, + "strict_schedule": "single_batch_aiter_ck_dense_mha_no_splitkv", + "strict_split_kv": "disabled", + "tp_rank": 0, + "tp_world_size": 2 + }, + "strict_schedule": "single_batch_aiter_ck_dense_mha_no_splitkv" + }, + "tp_rank": 0, + "tp_world_size": 2, + "transport": "rccl_ag_rs" + }, + { + "accum_dtype": "fp32", + "atol": 0.0002, + "cp_rank": 1, + "cp_world_size": 2, + "device": "cuda:1", + "downcast_at": "final_write", + "dtype": "bf16", + "expected_block_manifest": [ + { + "global_block_index": 0, + "kv_block_end": 4, + "kv_block_start": 0, + "owner_cp_rank": 0, + "owner_tp_rank": 0 + }, + { + "global_block_index": 1, + "kv_block_end": 8, + "kv_block_start": 4, + "owner_cp_rank": 0, + "owner_tp_rank": 0 + }, + { + "global_block_index": 2, + "kv_block_end": 12, + "kv_block_start": 8, + "owner_cp_rank": 1, + "owner_tp_rank": 0 + }, + { + "global_block_index": 3, + "kv_block_end": 16, + "kv_block_start": 12, + "owner_cp_rank": 1, + "owner_tp_rank": 0 + } + ], + "final_out_max_abs": 0.00390625, + "final_output_dtype": "bfloat16", + "final_write_atol": 0.02, + "gathered_block_indices": [ + 0, + 1, + 2, + 3 + ], + "global_failure_count": 0, + "global_world_size": 8, + "local_block_indices": [ + 2, + 3 + ], + "lse_max_abs": 4.76837158203125e-07, + "out_max_abs": 5.960464477539062e-07, + "passed": true, + "protocol": "ag_query_local_kv_rs_out_lse", + "query_ag": "rccl_ag_rs", + "query_ag_max_abs": 0.0, + "query_range": [ + 8, + 16 + ], + "rank": 1, + "repeat_count": 3, + "repeat_lse_bitwise": true, + "repeat_manifest_bitwise": true, + "repeat_out_bitwise": true, + "repeat_query_bitwise": true, + "replica_count": 2, + "replica_index": 0, + "strict_protocol": "ag_qkv_positions_shared_core_rs_out_lse", + "strict_shared_core": { + "actual_backend": "aiter.rocm.ck_dense_mha", + "bitwise": { + "dk": true, + "dq": true, + "dv": true, + "lse": true, + "out": true + }, + "communication_autograd": true, + "communication_backend": "rccl_ag_rs", + "executed": true, + "fallback": false, + "identity_errors": [], + "max_abs": { + "dk": 0.0, + "dq": 0.0, + "dv": 0.0, + "lse": 0.0, + "out": 0.0 + }, + "native_attention_arithmetic": true, + "passed": true, + "production_ready": true, + "repeat_lse_bitwise": true, + "repeat_out_bitwise": true, + "split_kv_policy": "disabled", + "strict_core_id": "rlkernel.attention.rocm.aiter_ck_dense_mha.v1", + "strict_mode": true, + "strict_provenance": { + "accum_dtype": "fp32", + "actual_backend": "aiter.rocm.ck_dense_mha", + "adapter_backend": "flashinfer", + "aiter_api_source": "aiter.ops.mha", + "aiter_source_sha256": "db61d7ce62a907c5079830622723f0abcc7fd35816d88006e381140cb6566ff4", + "arithmetic_plan_source": "aiter.ops.mha", + "arithmetic_semantics_verified": true, + "attention_backend": "aiter.rocm.ck_dense_mha", + "attention_mode": "prefill", + "batch_invariant_claim": "strict_runtime_verified", + "causal": true, + "communication_backend": "rccl_ag_rs", + "communication_overlap": "disabled", + "compute_communication": "decoupled", + "compute_order": [ + 0, + 1 + ], + "compute_schedule": "rlkernel.attention.strict_ring_state.v1", + "cp_comm_accum_dtype": "fp32", + "cp_comm_attention_numeric_reduction": false, + "cp_comm_backend": "rccl_ag_rs", + "cp_comm_compute_communication": "decoupled", + "cp_comm_contract": "partial_out_lse_global_block_index", + "cp_comm_expected_blocks": [ + { + "global_block_index": 0, + "kv_block_end": 4, + "kv_block_start": 0, + "owner_cp_rank": 0, + "owner_tp_rank": 0 + }, + { + "global_block_index": 1, + "kv_block_end": 8, + "kv_block_start": 4, + "owner_cp_rank": 0, + "owner_tp_rank": 0 + }, + { + "global_block_index": 2, + "kv_block_end": 12, + "kv_block_start": 8, + "owner_cp_rank": 1, + "owner_tp_rank": 0 + }, + { + "global_block_index": 3, + "kv_block_end": 16, + "kv_block_start": 12, + "owner_cp_rank": 1, + "owner_tp_rank": 0 + } + ], + "cp_comm_expected_kv_token_range": [ + 0, + 16 + ], + "cp_comm_merge_order": "global_block_index", + "cp_comm_merge_root_cp_rank": 0, + "cp_comm_pattern": "ag_rs", + "cp_comm_query_token_ranges": [ + [ + 0, + 8 + ], + [ + 8, + 16 + ] + ], + "cp_comm_required": true, + "cp_comm_return_lse": true, + "cp_comm_runtime": "rccl", + "cp_comm_status": "implemented", + "cp_comm_strict_backward": "rs_out_backward_ag_then_ag_qkv_backward_rs", + "cp_comm_strict_contract": "ag_qkv_positions_shared_core_rs_out_lse", + "cp_comm_strict_kv_communication": "all_gather", + "cp_comm_strict_position_communication": "all_gather", + "cp_rank": 1, + "cp_world_size": 2, + "deterministic_backward": true, + "downcast_at": "final_write", + "fa_api_source": null, + "fa_package_version": null, + "fallback": false, + "fallback_reason": null, + "k_cache_rope_state": "post_rope", + "lse_domain": "attention", + "lse_dtype": "fp32", + "lse_exported": true, + "materialization": "ag_qkv_positions_shared_core_rs", + "merge_order_indices": [ + 0, + 1 + ], + "native_attention_arithmetic": true, + "num_splits": 1, + "platform": "rocm", + "production_ready": true, + "q_rope_state": "post_rope", + "reference_only": false, + "requested_backend": "flashinfer_layout_adapter", + "ring_partial_arithmetic": false, + "ring_schedule_default": true, + "rope_backend": "rlkernel.rocm.deterministic_rope", + "rope_fusion": false, + "rope_fusion_boundary": "rlkernel_rope_then_attention", + "rope_theta": 1000000.0, + "rotary_dim": null, + "softmax_scale": null, + "split_kv_control": "dense_non_split_api", + "strict_comm_autograd": true, + "strict_core_id": "rlkernel.attention.rocm.aiter_ck_dense_mha.v1", + "strict_core_row_plans": [ + { + "actual_split_boundaries": [ + [ + 0, + 16 + ] + ], + "actual_split_kv_count": 1, + "actual_split_kv_policy": "disabled", + "actual_split_kv_size": null, + "requested_split_kv_policy": "disabled", + "requested_split_kv_size": null, + "split_kv_accum_dtype": "fp32", + "split_kv_backend": "aiter.rocm.ck_dense_mha", + "split_kv_downcast_at": "final_write", + "split_kv_fallback": false, + "split_kv_fallback_reason": null, + "split_kv_merge_order": "global_block_index", + "split_kv_plan_source": "contract_exact" + }, + { + "actual_split_boundaries": [ + [ + 0, + 16 + ] + ], + "actual_split_kv_count": 1, + "actual_split_kv_policy": "disabled", + "actual_split_kv_size": null, + "requested_split_kv_policy": "disabled", + "requested_split_kv_size": null, + "split_kv_accum_dtype": "fp32", + "split_kv_backend": "aiter.rocm.ck_dense_mha", + "split_kv_downcast_at": "final_write", + "split_kv_fallback": false, + "split_kv_fallback_reason": null, + "split_kv_merge_order": "global_block_index", + "split_kv_plan_source": "contract_exact" + } + ], + "strict_full_qkv_all_gather": true, + "strict_local_kv_range": [ + 8, + 16 + ], + "strict_local_query_range": [ + 8, + 16 + ], + "strict_mode": true, + "strict_position_ids_all_gather": true, + "strict_schedule": "single_batch_aiter_ck_dense_mha_no_splitkv", + "strict_split_kv": "disabled", + "tp_rank": 0, + "tp_world_size": 2 + }, + "strict_schedule": "single_batch_aiter_ck_dense_mha_no_splitkv" + }, + "tp_rank": 0, + "tp_world_size": 2, + "transport": "rccl_ag_rs" + }, + { + "accum_dtype": "fp32", + "atol": 0.0002, + "cp_rank": 0, + "cp_world_size": 2, + "device": "cuda:2", + "downcast_at": "final_write", + "dtype": "bf16", + "expected_block_manifest": [ + { + "global_block_index": 0, + "kv_block_end": 4, + "kv_block_start": 0, + "owner_cp_rank": 0, + "owner_tp_rank": 1 + }, + { + "global_block_index": 1, + "kv_block_end": 8, + "kv_block_start": 4, + "owner_cp_rank": 0, + "owner_tp_rank": 1 + }, + { + "global_block_index": 2, + "kv_block_end": 12, + "kv_block_start": 8, + "owner_cp_rank": 1, + "owner_tp_rank": 1 + }, + { + "global_block_index": 3, + "kv_block_end": 16, + "kv_block_start": 12, + "owner_cp_rank": 1, + "owner_tp_rank": 1 + } + ], + "final_out_max_abs": 0.001953125, + "final_output_dtype": "bfloat16", + "final_write_atol": 0.02, + "gathered_block_indices": [ + 0, + 1, + 2, + 3 + ], + "global_failure_count": 0, + "global_world_size": 8, + "local_block_indices": [ + 0, + 1 + ], + "lse_max_abs": 2.384185791015625e-07, + "out_max_abs": 7.152557373046875e-07, + "passed": true, + "protocol": "ag_query_local_kv_rs_out_lse", + "query_ag": "rccl_ag_rs", + "query_ag_max_abs": 0.0, + "query_range": [ + 0, + 8 + ], + "rank": 2, + "repeat_count": 3, + "repeat_lse_bitwise": true, + "repeat_manifest_bitwise": true, + "repeat_out_bitwise": true, + "repeat_query_bitwise": true, + "replica_count": 2, + "replica_index": 0, + "strict_protocol": "ag_qkv_positions_shared_core_rs_out_lse", + "strict_shared_core": { + "actual_backend": "aiter.rocm.ck_dense_mha", + "bitwise": { + "dk": true, + "dq": true, + "dv": true, + "lse": true, + "out": true + }, + "communication_autograd": true, + "communication_backend": "rccl_ag_rs", + "executed": true, + "fallback": false, + "identity_errors": [], + "max_abs": { + "dk": 0.0, + "dq": 0.0, + "dv": 0.0, + "lse": 0.0, + "out": 0.0 + }, + "native_attention_arithmetic": true, + "passed": true, + "production_ready": true, + "repeat_lse_bitwise": true, + "repeat_out_bitwise": true, + "split_kv_policy": "disabled", + "strict_core_id": "rlkernel.attention.rocm.aiter_ck_dense_mha.v1", + "strict_mode": true, + "strict_provenance": { + "accum_dtype": "fp32", + "actual_backend": "aiter.rocm.ck_dense_mha", + "adapter_backend": "flashinfer", + "aiter_api_source": "aiter.ops.mha", + "aiter_source_sha256": "db61d7ce62a907c5079830622723f0abcc7fd35816d88006e381140cb6566ff4", + "arithmetic_plan_source": "aiter.ops.mha", + "arithmetic_semantics_verified": true, + "attention_backend": "aiter.rocm.ck_dense_mha", + "attention_mode": "prefill", + "batch_invariant_claim": "strict_runtime_verified", + "causal": true, + "communication_backend": "rccl_ag_rs", + "communication_overlap": "disabled", + "compute_communication": "decoupled", + "compute_order": [ + 0, + 1 + ], + "compute_schedule": "rlkernel.attention.strict_ring_state.v1", + "cp_comm_accum_dtype": "fp32", + "cp_comm_attention_numeric_reduction": false, + "cp_comm_backend": "rccl_ag_rs", + "cp_comm_compute_communication": "decoupled", + "cp_comm_contract": "partial_out_lse_global_block_index", + "cp_comm_expected_blocks": [ + { + "global_block_index": 0, + "kv_block_end": 4, + "kv_block_start": 0, + "owner_cp_rank": 0, + "owner_tp_rank": 1 + }, + { + "global_block_index": 1, + "kv_block_end": 8, + "kv_block_start": 4, + "owner_cp_rank": 0, + "owner_tp_rank": 1 + }, + { + "global_block_index": 2, + "kv_block_end": 12, + "kv_block_start": 8, + "owner_cp_rank": 1, + "owner_tp_rank": 1 + }, + { + "global_block_index": 3, + "kv_block_end": 16, + "kv_block_start": 12, + "owner_cp_rank": 1, + "owner_tp_rank": 1 + } + ], + "cp_comm_expected_kv_token_range": [ + 0, + 16 + ], + "cp_comm_merge_order": "global_block_index", + "cp_comm_merge_root_cp_rank": 0, + "cp_comm_pattern": "ag_rs", + "cp_comm_query_token_ranges": [ + [ + 0, + 8 + ], + [ + 8, + 16 + ] + ], + "cp_comm_required": true, + "cp_comm_return_lse": true, + "cp_comm_runtime": "rccl", + "cp_comm_status": "implemented", + "cp_comm_strict_backward": "rs_out_backward_ag_then_ag_qkv_backward_rs", + "cp_comm_strict_contract": "ag_qkv_positions_shared_core_rs_out_lse", + "cp_comm_strict_kv_communication": "all_gather", + "cp_comm_strict_position_communication": "all_gather", + "cp_rank": 0, + "cp_world_size": 2, + "deterministic_backward": true, + "downcast_at": "final_write", + "fa_api_source": null, + "fa_package_version": null, + "fallback": false, + "fallback_reason": null, + "k_cache_rope_state": "post_rope", + "lse_domain": "attention", + "lse_dtype": "fp32", + "lse_exported": true, + "materialization": "ag_qkv_positions_shared_core_rs", + "merge_order_indices": [ + 0, + 1 + ], + "native_attention_arithmetic": true, + "num_splits": 1, + "platform": "rocm", + "production_ready": true, + "q_rope_state": "post_rope", + "reference_only": false, + "requested_backend": "flashinfer_layout_adapter", + "ring_partial_arithmetic": false, + "ring_schedule_default": true, + "rope_backend": "rlkernel.rocm.deterministic_rope", + "rope_fusion": false, + "rope_fusion_boundary": "rlkernel_rope_then_attention", + "rope_theta": 1000000.0, + "rotary_dim": null, + "softmax_scale": null, + "split_kv_control": "dense_non_split_api", + "strict_comm_autograd": true, + "strict_core_id": "rlkernel.attention.rocm.aiter_ck_dense_mha.v1", + "strict_core_row_plans": [ + { + "actual_split_boundaries": [ + [ + 0, + 16 + ] + ], + "actual_split_kv_count": 1, + "actual_split_kv_policy": "disabled", + "actual_split_kv_size": null, + "requested_split_kv_policy": "disabled", + "requested_split_kv_size": null, + "split_kv_accum_dtype": "fp32", + "split_kv_backend": "aiter.rocm.ck_dense_mha", + "split_kv_downcast_at": "final_write", + "split_kv_fallback": false, + "split_kv_fallback_reason": null, + "split_kv_merge_order": "global_block_index", + "split_kv_plan_source": "contract_exact" + }, + { + "actual_split_boundaries": [ + [ + 0, + 16 + ] + ], + "actual_split_kv_count": 1, + "actual_split_kv_policy": "disabled", + "actual_split_kv_size": null, + "requested_split_kv_policy": "disabled", + "requested_split_kv_size": null, + "split_kv_accum_dtype": "fp32", + "split_kv_backend": "aiter.rocm.ck_dense_mha", + "split_kv_downcast_at": "final_write", + "split_kv_fallback": false, + "split_kv_fallback_reason": null, + "split_kv_merge_order": "global_block_index", + "split_kv_plan_source": "contract_exact" + } + ], + "strict_full_qkv_all_gather": true, + "strict_local_kv_range": [ + 0, + 8 + ], + "strict_local_query_range": [ + 0, + 8 + ], + "strict_mode": true, + "strict_position_ids_all_gather": true, + "strict_schedule": "single_batch_aiter_ck_dense_mha_no_splitkv", + "strict_split_kv": "disabled", + "tp_rank": 1, + "tp_world_size": 2 + }, + "strict_schedule": "single_batch_aiter_ck_dense_mha_no_splitkv" + }, + "tp_rank": 1, + "tp_world_size": 2, + "transport": "rccl_ag_rs" + }, + { + "accum_dtype": "fp32", + "atol": 0.0002, + "cp_rank": 1, + "cp_world_size": 2, + "device": "cuda:3", + "downcast_at": "final_write", + "dtype": "bf16", + "expected_block_manifest": [ + { + "global_block_index": 0, + "kv_block_end": 4, + "kv_block_start": 0, + "owner_cp_rank": 0, + "owner_tp_rank": 1 + }, + { + "global_block_index": 1, + "kv_block_end": 8, + "kv_block_start": 4, + "owner_cp_rank": 0, + "owner_tp_rank": 1 + }, + { + "global_block_index": 2, + "kv_block_end": 12, + "kv_block_start": 8, + "owner_cp_rank": 1, + "owner_tp_rank": 1 + }, + { + "global_block_index": 3, + "kv_block_end": 16, + "kv_block_start": 12, + "owner_cp_rank": 1, + "owner_tp_rank": 1 + } + ], + "final_out_max_abs": 3.814697265625e-06, + "final_output_dtype": "bfloat16", + "final_write_atol": 0.02, + "gathered_block_indices": [ + 0, + 1, + 2, + 3 + ], + "global_failure_count": 0, + "global_world_size": 8, + "local_block_indices": [ + 2, + 3 + ], + "lse_max_abs": 4.76837158203125e-07, + "out_max_abs": 7.152557373046875e-07, + "passed": true, + "protocol": "ag_query_local_kv_rs_out_lse", + "query_ag": "rccl_ag_rs", + "query_ag_max_abs": 0.0, + "query_range": [ + 8, + 16 + ], + "rank": 3, + "repeat_count": 3, + "repeat_lse_bitwise": true, + "repeat_manifest_bitwise": true, + "repeat_out_bitwise": true, + "repeat_query_bitwise": true, + "replica_count": 2, + "replica_index": 0, + "strict_protocol": "ag_qkv_positions_shared_core_rs_out_lse", + "strict_shared_core": { + "actual_backend": "aiter.rocm.ck_dense_mha", + "bitwise": { + "dk": true, + "dq": true, + "dv": true, + "lse": true, + "out": true + }, + "communication_autograd": true, + "communication_backend": "rccl_ag_rs", + "executed": true, + "fallback": false, + "identity_errors": [], + "max_abs": { + "dk": 0.0, + "dq": 0.0, + "dv": 0.0, + "lse": 0.0, + "out": 0.0 + }, + "native_attention_arithmetic": true, + "passed": true, + "production_ready": true, + "repeat_lse_bitwise": true, + "repeat_out_bitwise": true, + "split_kv_policy": "disabled", + "strict_core_id": "rlkernel.attention.rocm.aiter_ck_dense_mha.v1", + "strict_mode": true, + "strict_provenance": { + "accum_dtype": "fp32", + "actual_backend": "aiter.rocm.ck_dense_mha", + "adapter_backend": "flashinfer", + "aiter_api_source": "aiter.ops.mha", + "aiter_source_sha256": "db61d7ce62a907c5079830622723f0abcc7fd35816d88006e381140cb6566ff4", + "arithmetic_plan_source": "aiter.ops.mha", + "arithmetic_semantics_verified": true, + "attention_backend": "aiter.rocm.ck_dense_mha", + "attention_mode": "prefill", + "batch_invariant_claim": "strict_runtime_verified", + "causal": true, + "communication_backend": "rccl_ag_rs", + "communication_overlap": "disabled", + "compute_communication": "decoupled", + "compute_order": [ + 0, + 1 + ], + "compute_schedule": "rlkernel.attention.strict_ring_state.v1", + "cp_comm_accum_dtype": "fp32", + "cp_comm_attention_numeric_reduction": false, + "cp_comm_backend": "rccl_ag_rs", + "cp_comm_compute_communication": "decoupled", + "cp_comm_contract": "partial_out_lse_global_block_index", + "cp_comm_expected_blocks": [ + { + "global_block_index": 0, + "kv_block_end": 4, + "kv_block_start": 0, + "owner_cp_rank": 0, + "owner_tp_rank": 1 + }, + { + "global_block_index": 1, + "kv_block_end": 8, + "kv_block_start": 4, + "owner_cp_rank": 0, + "owner_tp_rank": 1 + }, + { + "global_block_index": 2, + "kv_block_end": 12, + "kv_block_start": 8, + "owner_cp_rank": 1, + "owner_tp_rank": 1 + }, + { + "global_block_index": 3, + "kv_block_end": 16, + "kv_block_start": 12, + "owner_cp_rank": 1, + "owner_tp_rank": 1 + } + ], + "cp_comm_expected_kv_token_range": [ + 0, + 16 + ], + "cp_comm_merge_order": "global_block_index", + "cp_comm_merge_root_cp_rank": 0, + "cp_comm_pattern": "ag_rs", + "cp_comm_query_token_ranges": [ + [ + 0, + 8 + ], + [ + 8, + 16 + ] + ], + "cp_comm_required": true, + "cp_comm_return_lse": true, + "cp_comm_runtime": "rccl", + "cp_comm_status": "implemented", + "cp_comm_strict_backward": "rs_out_backward_ag_then_ag_qkv_backward_rs", + "cp_comm_strict_contract": "ag_qkv_positions_shared_core_rs_out_lse", + "cp_comm_strict_kv_communication": "all_gather", + "cp_comm_strict_position_communication": "all_gather", + "cp_rank": 1, + "cp_world_size": 2, + "deterministic_backward": true, + "downcast_at": "final_write", + "fa_api_source": null, + "fa_package_version": null, + "fallback": false, + "fallback_reason": null, + "k_cache_rope_state": "post_rope", + "lse_domain": "attention", + "lse_dtype": "fp32", + "lse_exported": true, + "materialization": "ag_qkv_positions_shared_core_rs", + "merge_order_indices": [ + 0, + 1 + ], + "native_attention_arithmetic": true, + "num_splits": 1, + "platform": "rocm", + "production_ready": true, + "q_rope_state": "post_rope", + "reference_only": false, + "requested_backend": "flashinfer_layout_adapter", + "ring_partial_arithmetic": false, + "ring_schedule_default": true, + "rope_backend": "rlkernel.rocm.deterministic_rope", + "rope_fusion": false, + "rope_fusion_boundary": "rlkernel_rope_then_attention", + "rope_theta": 1000000.0, + "rotary_dim": null, + "softmax_scale": null, + "split_kv_control": "dense_non_split_api", + "strict_comm_autograd": true, + "strict_core_id": "rlkernel.attention.rocm.aiter_ck_dense_mha.v1", + "strict_core_row_plans": [ + { + "actual_split_boundaries": [ + [ + 0, + 16 + ] + ], + "actual_split_kv_count": 1, + "actual_split_kv_policy": "disabled", + "actual_split_kv_size": null, + "requested_split_kv_policy": "disabled", + "requested_split_kv_size": null, + "split_kv_accum_dtype": "fp32", + "split_kv_backend": "aiter.rocm.ck_dense_mha", + "split_kv_downcast_at": "final_write", + "split_kv_fallback": false, + "split_kv_fallback_reason": null, + "split_kv_merge_order": "global_block_index", + "split_kv_plan_source": "contract_exact" + }, + { + "actual_split_boundaries": [ + [ + 0, + 16 + ] + ], + "actual_split_kv_count": 1, + "actual_split_kv_policy": "disabled", + "actual_split_kv_size": null, + "requested_split_kv_policy": "disabled", + "requested_split_kv_size": null, + "split_kv_accum_dtype": "fp32", + "split_kv_backend": "aiter.rocm.ck_dense_mha", + "split_kv_downcast_at": "final_write", + "split_kv_fallback": false, + "split_kv_fallback_reason": null, + "split_kv_merge_order": "global_block_index", + "split_kv_plan_source": "contract_exact" + } + ], + "strict_full_qkv_all_gather": true, + "strict_local_kv_range": [ + 8, + 16 + ], + "strict_local_query_range": [ + 8, + 16 + ], + "strict_mode": true, + "strict_position_ids_all_gather": true, + "strict_schedule": "single_batch_aiter_ck_dense_mha_no_splitkv", + "strict_split_kv": "disabled", + "tp_rank": 1, + "tp_world_size": 2 + }, + "strict_schedule": "single_batch_aiter_ck_dense_mha_no_splitkv" + }, + "tp_rank": 1, + "tp_world_size": 2, + "transport": "rccl_ag_rs" + }, + { + "accum_dtype": "fp32", + "atol": 0.0002, + "cp_rank": 0, + "cp_world_size": 2, + "device": "cuda:4", + "downcast_at": "final_write", + "dtype": "bf16", + "expected_block_manifest": [ + { + "global_block_index": 0, + "kv_block_end": 4, + "kv_block_start": 0, + "owner_cp_rank": 0, + "owner_tp_rank": 0 + }, + { + "global_block_index": 1, + "kv_block_end": 8, + "kv_block_start": 4, + "owner_cp_rank": 0, + "owner_tp_rank": 0 + }, + { + "global_block_index": 2, + "kv_block_end": 12, + "kv_block_start": 8, + "owner_cp_rank": 1, + "owner_tp_rank": 0 + }, + { + "global_block_index": 3, + "kv_block_end": 16, + "kv_block_start": 12, + "owner_cp_rank": 1, + "owner_tp_rank": 0 + } + ], + "final_out_max_abs": 1.52587890625e-05, + "final_output_dtype": "bfloat16", + "final_write_atol": 0.02, + "gathered_block_indices": [ + 0, + 1, + 2, + 3 + ], + "global_failure_count": 0, + "global_world_size": 8, + "local_block_indices": [ + 0, + 1 + ], + "lse_max_abs": 2.384185791015625e-07, + "out_max_abs": 7.152557373046875e-07, + "passed": true, + "protocol": "ag_query_local_kv_rs_out_lse", + "query_ag": "rccl_ag_rs", + "query_ag_max_abs": 0.0, + "query_range": [ + 0, + 8 + ], + "rank": 4, + "repeat_count": 3, + "repeat_lse_bitwise": true, + "repeat_manifest_bitwise": true, + "repeat_out_bitwise": true, + "repeat_query_bitwise": true, + "replica_count": 2, + "replica_index": 1, + "strict_protocol": "ag_qkv_positions_shared_core_rs_out_lse", + "strict_shared_core": { + "actual_backend": "aiter.rocm.ck_dense_mha", + "bitwise": { + "dk": true, + "dq": true, + "dv": true, + "lse": true, + "out": true + }, + "communication_autograd": true, + "communication_backend": "rccl_ag_rs", + "executed": true, + "fallback": false, + "identity_errors": [], + "max_abs": { + "dk": 0.0, + "dq": 0.0, + "dv": 0.0, + "lse": 0.0, + "out": 0.0 + }, + "native_attention_arithmetic": true, + "passed": true, + "production_ready": true, + "repeat_lse_bitwise": true, + "repeat_out_bitwise": true, + "split_kv_policy": "disabled", + "strict_core_id": "rlkernel.attention.rocm.aiter_ck_dense_mha.v1", + "strict_mode": true, + "strict_provenance": { + "accum_dtype": "fp32", + "actual_backend": "aiter.rocm.ck_dense_mha", + "adapter_backend": "flashinfer", + "aiter_api_source": "aiter.ops.mha", + "aiter_source_sha256": "db61d7ce62a907c5079830622723f0abcc7fd35816d88006e381140cb6566ff4", + "arithmetic_plan_source": "aiter.ops.mha", + "arithmetic_semantics_verified": true, + "attention_backend": "aiter.rocm.ck_dense_mha", + "attention_mode": "prefill", + "batch_invariant_claim": "strict_runtime_verified", + "causal": true, + "communication_backend": "rccl_ag_rs", + "communication_overlap": "disabled", + "compute_communication": "decoupled", + "compute_order": [ + 0, + 1 + ], + "compute_schedule": "rlkernel.attention.strict_ring_state.v1", + "cp_comm_accum_dtype": "fp32", + "cp_comm_attention_numeric_reduction": false, + "cp_comm_backend": "rccl_ag_rs", + "cp_comm_compute_communication": "decoupled", + "cp_comm_contract": "partial_out_lse_global_block_index", + "cp_comm_expected_blocks": [ + { + "global_block_index": 0, + "kv_block_end": 4, + "kv_block_start": 0, + "owner_cp_rank": 0, + "owner_tp_rank": 0 + }, + { + "global_block_index": 1, + "kv_block_end": 8, + "kv_block_start": 4, + "owner_cp_rank": 0, + "owner_tp_rank": 0 + }, + { + "global_block_index": 2, + "kv_block_end": 12, + "kv_block_start": 8, + "owner_cp_rank": 1, + "owner_tp_rank": 0 + }, + { + "global_block_index": 3, + "kv_block_end": 16, + "kv_block_start": 12, + "owner_cp_rank": 1, + "owner_tp_rank": 0 + } + ], + "cp_comm_expected_kv_token_range": [ + 0, + 16 + ], + "cp_comm_merge_order": "global_block_index", + "cp_comm_merge_root_cp_rank": 0, + "cp_comm_pattern": "ag_rs", + "cp_comm_query_token_ranges": [ + [ + 0, + 8 + ], + [ + 8, + 16 + ] + ], + "cp_comm_required": true, + "cp_comm_return_lse": true, + "cp_comm_runtime": "rccl", + "cp_comm_status": "implemented", + "cp_comm_strict_backward": "rs_out_backward_ag_then_ag_qkv_backward_rs", + "cp_comm_strict_contract": "ag_qkv_positions_shared_core_rs_out_lse", + "cp_comm_strict_kv_communication": "all_gather", + "cp_comm_strict_position_communication": "all_gather", + "cp_rank": 0, + "cp_world_size": 2, + "deterministic_backward": true, + "downcast_at": "final_write", + "fa_api_source": null, + "fa_package_version": null, + "fallback": false, + "fallback_reason": null, + "k_cache_rope_state": "post_rope", + "lse_domain": "attention", + "lse_dtype": "fp32", + "lse_exported": true, + "materialization": "ag_qkv_positions_shared_core_rs", + "merge_order_indices": [ + 0, + 1 + ], + "native_attention_arithmetic": true, + "num_splits": 1, + "platform": "rocm", + "production_ready": true, + "q_rope_state": "post_rope", + "reference_only": false, + "requested_backend": "flashinfer_layout_adapter", + "ring_partial_arithmetic": false, + "ring_schedule_default": true, + "rope_backend": "rlkernel.rocm.deterministic_rope", + "rope_fusion": false, + "rope_fusion_boundary": "rlkernel_rope_then_attention", + "rope_theta": 1000000.0, + "rotary_dim": null, + "softmax_scale": null, + "split_kv_control": "dense_non_split_api", + "strict_comm_autograd": true, + "strict_core_id": "rlkernel.attention.rocm.aiter_ck_dense_mha.v1", + "strict_core_row_plans": [ + { + "actual_split_boundaries": [ + [ + 0, + 16 + ] + ], + "actual_split_kv_count": 1, + "actual_split_kv_policy": "disabled", + "actual_split_kv_size": null, + "requested_split_kv_policy": "disabled", + "requested_split_kv_size": null, + "split_kv_accum_dtype": "fp32", + "split_kv_backend": "aiter.rocm.ck_dense_mha", + "split_kv_downcast_at": "final_write", + "split_kv_fallback": false, + "split_kv_fallback_reason": null, + "split_kv_merge_order": "global_block_index", + "split_kv_plan_source": "contract_exact" + }, + { + "actual_split_boundaries": [ + [ + 0, + 16 + ] + ], + "actual_split_kv_count": 1, + "actual_split_kv_policy": "disabled", + "actual_split_kv_size": null, + "requested_split_kv_policy": "disabled", + "requested_split_kv_size": null, + "split_kv_accum_dtype": "fp32", + "split_kv_backend": "aiter.rocm.ck_dense_mha", + "split_kv_downcast_at": "final_write", + "split_kv_fallback": false, + "split_kv_fallback_reason": null, + "split_kv_merge_order": "global_block_index", + "split_kv_plan_source": "contract_exact" + } + ], + "strict_full_qkv_all_gather": true, + "strict_local_kv_range": [ + 0, + 8 + ], + "strict_local_query_range": [ + 0, + 8 + ], + "strict_mode": true, + "strict_position_ids_all_gather": true, + "strict_schedule": "single_batch_aiter_ck_dense_mha_no_splitkv", + "strict_split_kv": "disabled", + "tp_rank": 0, + "tp_world_size": 2 + }, + "strict_schedule": "single_batch_aiter_ck_dense_mha_no_splitkv" + }, + "tp_rank": 0, + "tp_world_size": 2, + "transport": "rccl_ag_rs" + }, + { + "accum_dtype": "fp32", + "atol": 0.0002, + "cp_rank": 1, + "cp_world_size": 2, + "device": "cuda:5", + "downcast_at": "final_write", + "dtype": "bf16", + "expected_block_manifest": [ + { + "global_block_index": 0, + "kv_block_end": 4, + "kv_block_start": 0, + "owner_cp_rank": 0, + "owner_tp_rank": 0 + }, + { + "global_block_index": 1, + "kv_block_end": 8, + "kv_block_start": 4, + "owner_cp_rank": 0, + "owner_tp_rank": 0 + }, + { + "global_block_index": 2, + "kv_block_end": 12, + "kv_block_start": 8, + "owner_cp_rank": 1, + "owner_tp_rank": 0 + }, + { + "global_block_index": 3, + "kv_block_end": 16, + "kv_block_start": 12, + "owner_cp_rank": 1, + "owner_tp_rank": 0 + } + ], + "final_out_max_abs": 0.00048828125, + "final_output_dtype": "bfloat16", + "final_write_atol": 0.02, + "gathered_block_indices": [ + 0, + 1, + 2, + 3 + ], + "global_failure_count": 0, + "global_world_size": 8, + "local_block_indices": [ + 2, + 3 + ], + "lse_max_abs": 4.76837158203125e-07, + "out_max_abs": 9.5367431640625e-07, + "passed": true, + "protocol": "ag_query_local_kv_rs_out_lse", + "query_ag": "rccl_ag_rs", + "query_ag_max_abs": 0.0, + "query_range": [ + 8, + 16 + ], + "rank": 5, + "repeat_count": 3, + "repeat_lse_bitwise": true, + "repeat_manifest_bitwise": true, + "repeat_out_bitwise": true, + "repeat_query_bitwise": true, + "replica_count": 2, + "replica_index": 1, + "strict_protocol": "ag_qkv_positions_shared_core_rs_out_lse", + "strict_shared_core": { + "actual_backend": "aiter.rocm.ck_dense_mha", + "bitwise": { + "dk": true, + "dq": true, + "dv": true, + "lse": true, + "out": true + }, + "communication_autograd": true, + "communication_backend": "rccl_ag_rs", + "executed": true, + "fallback": false, + "identity_errors": [], + "max_abs": { + "dk": 0.0, + "dq": 0.0, + "dv": 0.0, + "lse": 0.0, + "out": 0.0 + }, + "native_attention_arithmetic": true, + "passed": true, + "production_ready": true, + "repeat_lse_bitwise": true, + "repeat_out_bitwise": true, + "split_kv_policy": "disabled", + "strict_core_id": "rlkernel.attention.rocm.aiter_ck_dense_mha.v1", + "strict_mode": true, + "strict_provenance": { + "accum_dtype": "fp32", + "actual_backend": "aiter.rocm.ck_dense_mha", + "adapter_backend": "flashinfer", + "aiter_api_source": "aiter.ops.mha", + "aiter_source_sha256": "db61d7ce62a907c5079830622723f0abcc7fd35816d88006e381140cb6566ff4", + "arithmetic_plan_source": "aiter.ops.mha", + "arithmetic_semantics_verified": true, + "attention_backend": "aiter.rocm.ck_dense_mha", + "attention_mode": "prefill", + "batch_invariant_claim": "strict_runtime_verified", + "causal": true, + "communication_backend": "rccl_ag_rs", + "communication_overlap": "disabled", + "compute_communication": "decoupled", + "compute_order": [ + 0, + 1 + ], + "compute_schedule": "rlkernel.attention.strict_ring_state.v1", + "cp_comm_accum_dtype": "fp32", + "cp_comm_attention_numeric_reduction": false, + "cp_comm_backend": "rccl_ag_rs", + "cp_comm_compute_communication": "decoupled", + "cp_comm_contract": "partial_out_lse_global_block_index", + "cp_comm_expected_blocks": [ + { + "global_block_index": 0, + "kv_block_end": 4, + "kv_block_start": 0, + "owner_cp_rank": 0, + "owner_tp_rank": 0 + }, + { + "global_block_index": 1, + "kv_block_end": 8, + "kv_block_start": 4, + "owner_cp_rank": 0, + "owner_tp_rank": 0 + }, + { + "global_block_index": 2, + "kv_block_end": 12, + "kv_block_start": 8, + "owner_cp_rank": 1, + "owner_tp_rank": 0 + }, + { + "global_block_index": 3, + "kv_block_end": 16, + "kv_block_start": 12, + "owner_cp_rank": 1, + "owner_tp_rank": 0 + } + ], + "cp_comm_expected_kv_token_range": [ + 0, + 16 + ], + "cp_comm_merge_order": "global_block_index", + "cp_comm_merge_root_cp_rank": 0, + "cp_comm_pattern": "ag_rs", + "cp_comm_query_token_ranges": [ + [ + 0, + 8 + ], + [ + 8, + 16 + ] + ], + "cp_comm_required": true, + "cp_comm_return_lse": true, + "cp_comm_runtime": "rccl", + "cp_comm_status": "implemented", + "cp_comm_strict_backward": "rs_out_backward_ag_then_ag_qkv_backward_rs", + "cp_comm_strict_contract": "ag_qkv_positions_shared_core_rs_out_lse", + "cp_comm_strict_kv_communication": "all_gather", + "cp_comm_strict_position_communication": "all_gather", + "cp_rank": 1, + "cp_world_size": 2, + "deterministic_backward": true, + "downcast_at": "final_write", + "fa_api_source": null, + "fa_package_version": null, + "fallback": false, + "fallback_reason": null, + "k_cache_rope_state": "post_rope", + "lse_domain": "attention", + "lse_dtype": "fp32", + "lse_exported": true, + "materialization": "ag_qkv_positions_shared_core_rs", + "merge_order_indices": [ + 0, + 1 + ], + "native_attention_arithmetic": true, + "num_splits": 1, + "platform": "rocm", + "production_ready": true, + "q_rope_state": "post_rope", + "reference_only": false, + "requested_backend": "flashinfer_layout_adapter", + "ring_partial_arithmetic": false, + "ring_schedule_default": true, + "rope_backend": "rlkernel.rocm.deterministic_rope", + "rope_fusion": false, + "rope_fusion_boundary": "rlkernel_rope_then_attention", + "rope_theta": 1000000.0, + "rotary_dim": null, + "softmax_scale": null, + "split_kv_control": "dense_non_split_api", + "strict_comm_autograd": true, + "strict_core_id": "rlkernel.attention.rocm.aiter_ck_dense_mha.v1", + "strict_core_row_plans": [ + { + "actual_split_boundaries": [ + [ + 0, + 16 + ] + ], + "actual_split_kv_count": 1, + "actual_split_kv_policy": "disabled", + "actual_split_kv_size": null, + "requested_split_kv_policy": "disabled", + "requested_split_kv_size": null, + "split_kv_accum_dtype": "fp32", + "split_kv_backend": "aiter.rocm.ck_dense_mha", + "split_kv_downcast_at": "final_write", + "split_kv_fallback": false, + "split_kv_fallback_reason": null, + "split_kv_merge_order": "global_block_index", + "split_kv_plan_source": "contract_exact" + }, + { + "actual_split_boundaries": [ + [ + 0, + 16 + ] + ], + "actual_split_kv_count": 1, + "actual_split_kv_policy": "disabled", + "actual_split_kv_size": null, + "requested_split_kv_policy": "disabled", + "requested_split_kv_size": null, + "split_kv_accum_dtype": "fp32", + "split_kv_backend": "aiter.rocm.ck_dense_mha", + "split_kv_downcast_at": "final_write", + "split_kv_fallback": false, + "split_kv_fallback_reason": null, + "split_kv_merge_order": "global_block_index", + "split_kv_plan_source": "contract_exact" + } + ], + "strict_full_qkv_all_gather": true, + "strict_local_kv_range": [ + 8, + 16 + ], + "strict_local_query_range": [ + 8, + 16 + ], + "strict_mode": true, + "strict_position_ids_all_gather": true, + "strict_schedule": "single_batch_aiter_ck_dense_mha_no_splitkv", + "strict_split_kv": "disabled", + "tp_rank": 0, + "tp_world_size": 2 + }, + "strict_schedule": "single_batch_aiter_ck_dense_mha_no_splitkv" + }, + "tp_rank": 0, + "tp_world_size": 2, + "transport": "rccl_ag_rs" + }, + { + "accum_dtype": "fp32", + "atol": 0.0002, + "cp_rank": 0, + "cp_world_size": 2, + "device": "cuda:6", + "downcast_at": "final_write", + "dtype": "bf16", + "expected_block_manifest": [ + { + "global_block_index": 0, + "kv_block_end": 4, + "kv_block_start": 0, + "owner_cp_rank": 0, + "owner_tp_rank": 1 + }, + { + "global_block_index": 1, + "kv_block_end": 8, + "kv_block_start": 4, + "owner_cp_rank": 0, + "owner_tp_rank": 1 + }, + { + "global_block_index": 2, + "kv_block_end": 12, + "kv_block_start": 8, + "owner_cp_rank": 1, + "owner_tp_rank": 1 + }, + { + "global_block_index": 3, + "kv_block_end": 16, + "kv_block_start": 12, + "owner_cp_rank": 1, + "owner_tp_rank": 1 + } + ], + "final_out_max_abs": 0.000244140625, + "final_output_dtype": "bfloat16", + "final_write_atol": 0.02, + "gathered_block_indices": [ + 0, + 1, + 2, + 3 + ], + "global_failure_count": 0, + "global_world_size": 8, + "local_block_indices": [ + 0, + 1 + ], + "lse_max_abs": 2.384185791015625e-07, + "out_max_abs": 7.152557373046875e-07, + "passed": true, + "protocol": "ag_query_local_kv_rs_out_lse", + "query_ag": "rccl_ag_rs", + "query_ag_max_abs": 0.0, + "query_range": [ + 0, + 8 + ], + "rank": 6, + "repeat_count": 3, + "repeat_lse_bitwise": true, + "repeat_manifest_bitwise": true, + "repeat_out_bitwise": true, + "repeat_query_bitwise": true, + "replica_count": 2, + "replica_index": 1, + "strict_protocol": "ag_qkv_positions_shared_core_rs_out_lse", + "strict_shared_core": { + "actual_backend": "aiter.rocm.ck_dense_mha", + "bitwise": { + "dk": true, + "dq": true, + "dv": true, + "lse": true, + "out": true + }, + "communication_autograd": true, + "communication_backend": "rccl_ag_rs", + "executed": true, + "fallback": false, + "identity_errors": [], + "max_abs": { + "dk": 0.0, + "dq": 0.0, + "dv": 0.0, + "lse": 0.0, + "out": 0.0 + }, + "native_attention_arithmetic": true, + "passed": true, + "production_ready": true, + "repeat_lse_bitwise": true, + "repeat_out_bitwise": true, + "split_kv_policy": "disabled", + "strict_core_id": "rlkernel.attention.rocm.aiter_ck_dense_mha.v1", + "strict_mode": true, + "strict_provenance": { + "accum_dtype": "fp32", + "actual_backend": "aiter.rocm.ck_dense_mha", + "adapter_backend": "flashinfer", + "aiter_api_source": "aiter.ops.mha", + "aiter_source_sha256": "db61d7ce62a907c5079830622723f0abcc7fd35816d88006e381140cb6566ff4", + "arithmetic_plan_source": "aiter.ops.mha", + "arithmetic_semantics_verified": true, + "attention_backend": "aiter.rocm.ck_dense_mha", + "attention_mode": "prefill", + "batch_invariant_claim": "strict_runtime_verified", + "causal": true, + "communication_backend": "rccl_ag_rs", + "communication_overlap": "disabled", + "compute_communication": "decoupled", + "compute_order": [ + 0, + 1 + ], + "compute_schedule": "rlkernel.attention.strict_ring_state.v1", + "cp_comm_accum_dtype": "fp32", + "cp_comm_attention_numeric_reduction": false, + "cp_comm_backend": "rccl_ag_rs", + "cp_comm_compute_communication": "decoupled", + "cp_comm_contract": "partial_out_lse_global_block_index", + "cp_comm_expected_blocks": [ + { + "global_block_index": 0, + "kv_block_end": 4, + "kv_block_start": 0, + "owner_cp_rank": 0, + "owner_tp_rank": 1 + }, + { + "global_block_index": 1, + "kv_block_end": 8, + "kv_block_start": 4, + "owner_cp_rank": 0, + "owner_tp_rank": 1 + }, + { + "global_block_index": 2, + "kv_block_end": 12, + "kv_block_start": 8, + "owner_cp_rank": 1, + "owner_tp_rank": 1 + }, + { + "global_block_index": 3, + "kv_block_end": 16, + "kv_block_start": 12, + "owner_cp_rank": 1, + "owner_tp_rank": 1 + } + ], + "cp_comm_expected_kv_token_range": [ + 0, + 16 + ], + "cp_comm_merge_order": "global_block_index", + "cp_comm_merge_root_cp_rank": 0, + "cp_comm_pattern": "ag_rs", + "cp_comm_query_token_ranges": [ + [ + 0, + 8 + ], + [ + 8, + 16 + ] + ], + "cp_comm_required": true, + "cp_comm_return_lse": true, + "cp_comm_runtime": "rccl", + "cp_comm_status": "implemented", + "cp_comm_strict_backward": "rs_out_backward_ag_then_ag_qkv_backward_rs", + "cp_comm_strict_contract": "ag_qkv_positions_shared_core_rs_out_lse", + "cp_comm_strict_kv_communication": "all_gather", + "cp_comm_strict_position_communication": "all_gather", + "cp_rank": 0, + "cp_world_size": 2, + "deterministic_backward": true, + "downcast_at": "final_write", + "fa_api_source": null, + "fa_package_version": null, + "fallback": false, + "fallback_reason": null, + "k_cache_rope_state": "post_rope", + "lse_domain": "attention", + "lse_dtype": "fp32", + "lse_exported": true, + "materialization": "ag_qkv_positions_shared_core_rs", + "merge_order_indices": [ + 0, + 1 + ], + "native_attention_arithmetic": true, + "num_splits": 1, + "platform": "rocm", + "production_ready": true, + "q_rope_state": "post_rope", + "reference_only": false, + "requested_backend": "flashinfer_layout_adapter", + "ring_partial_arithmetic": false, + "ring_schedule_default": true, + "rope_backend": "rlkernel.rocm.deterministic_rope", + "rope_fusion": false, + "rope_fusion_boundary": "rlkernel_rope_then_attention", + "rope_theta": 1000000.0, + "rotary_dim": null, + "softmax_scale": null, + "split_kv_control": "dense_non_split_api", + "strict_comm_autograd": true, + "strict_core_id": "rlkernel.attention.rocm.aiter_ck_dense_mha.v1", + "strict_core_row_plans": [ + { + "actual_split_boundaries": [ + [ + 0, + 16 + ] + ], + "actual_split_kv_count": 1, + "actual_split_kv_policy": "disabled", + "actual_split_kv_size": null, + "requested_split_kv_policy": "disabled", + "requested_split_kv_size": null, + "split_kv_accum_dtype": "fp32", + "split_kv_backend": "aiter.rocm.ck_dense_mha", + "split_kv_downcast_at": "final_write", + "split_kv_fallback": false, + "split_kv_fallback_reason": null, + "split_kv_merge_order": "global_block_index", + "split_kv_plan_source": "contract_exact" + }, + { + "actual_split_boundaries": [ + [ + 0, + 16 + ] + ], + "actual_split_kv_count": 1, + "actual_split_kv_policy": "disabled", + "actual_split_kv_size": null, + "requested_split_kv_policy": "disabled", + "requested_split_kv_size": null, + "split_kv_accum_dtype": "fp32", + "split_kv_backend": "aiter.rocm.ck_dense_mha", + "split_kv_downcast_at": "final_write", + "split_kv_fallback": false, + "split_kv_fallback_reason": null, + "split_kv_merge_order": "global_block_index", + "split_kv_plan_source": "contract_exact" + } + ], + "strict_full_qkv_all_gather": true, + "strict_local_kv_range": [ + 0, + 8 + ], + "strict_local_query_range": [ + 0, + 8 + ], + "strict_mode": true, + "strict_position_ids_all_gather": true, + "strict_schedule": "single_batch_aiter_ck_dense_mha_no_splitkv", + "strict_split_kv": "disabled", + "tp_rank": 1, + "tp_world_size": 2 + }, + "strict_schedule": "single_batch_aiter_ck_dense_mha_no_splitkv" + }, + "tp_rank": 1, + "tp_world_size": 2, + "transport": "rccl_ag_rs" + }, + { + "accum_dtype": "fp32", + "atol": 0.0002, + "cp_rank": 1, + "cp_world_size": 2, + "device": "cuda:7", + "downcast_at": "final_write", + "dtype": "bf16", + "expected_block_manifest": [ + { + "global_block_index": 0, + "kv_block_end": 4, + "kv_block_start": 0, + "owner_cp_rank": 0, + "owner_tp_rank": 1 + }, + { + "global_block_index": 1, + "kv_block_end": 8, + "kv_block_start": 4, + "owner_cp_rank": 0, + "owner_tp_rank": 1 + }, + { + "global_block_index": 2, + "kv_block_end": 12, + "kv_block_start": 8, + "owner_cp_rank": 1, + "owner_tp_rank": 1 + }, + { + "global_block_index": 3, + "kv_block_end": 16, + "kv_block_start": 12, + "owner_cp_rank": 1, + "owner_tp_rank": 1 + } + ], + "final_out_max_abs": 0.001953125, + "final_output_dtype": "bfloat16", + "final_write_atol": 0.02, + "gathered_block_indices": [ + 0, + 1, + 2, + 3 + ], + "global_failure_count": 0, + "global_world_size": 8, + "local_block_indices": [ + 2, + 3 + ], + "lse_max_abs": 4.76837158203125e-07, + "out_max_abs": 7.152557373046875e-07, + "passed": true, + "protocol": "ag_query_local_kv_rs_out_lse", + "query_ag": "rccl_ag_rs", + "query_ag_max_abs": 0.0, + "query_range": [ + 8, + 16 + ], + "rank": 7, + "repeat_count": 3, + "repeat_lse_bitwise": true, + "repeat_manifest_bitwise": true, + "repeat_out_bitwise": true, + "repeat_query_bitwise": true, + "replica_count": 2, + "replica_index": 1, + "strict_protocol": "ag_qkv_positions_shared_core_rs_out_lse", + "strict_shared_core": { + "actual_backend": "aiter.rocm.ck_dense_mha", + "bitwise": { + "dk": true, + "dq": true, + "dv": true, + "lse": true, + "out": true + }, + "communication_autograd": true, + "communication_backend": "rccl_ag_rs", + "executed": true, + "fallback": false, + "identity_errors": [], + "max_abs": { + "dk": 0.0, + "dq": 0.0, + "dv": 0.0, + "lse": 0.0, + "out": 0.0 + }, + "native_attention_arithmetic": true, + "passed": true, + "production_ready": true, + "repeat_lse_bitwise": true, + "repeat_out_bitwise": true, + "split_kv_policy": "disabled", + "strict_core_id": "rlkernel.attention.rocm.aiter_ck_dense_mha.v1", + "strict_mode": true, + "strict_provenance": { + "accum_dtype": "fp32", + "actual_backend": "aiter.rocm.ck_dense_mha", + "adapter_backend": "flashinfer", + "aiter_api_source": "aiter.ops.mha", + "aiter_source_sha256": "db61d7ce62a907c5079830622723f0abcc7fd35816d88006e381140cb6566ff4", + "arithmetic_plan_source": "aiter.ops.mha", + "arithmetic_semantics_verified": true, + "attention_backend": "aiter.rocm.ck_dense_mha", + "attention_mode": "prefill", + "batch_invariant_claim": "strict_runtime_verified", + "causal": true, + "communication_backend": "rccl_ag_rs", + "communication_overlap": "disabled", + "compute_communication": "decoupled", + "compute_order": [ + 0, + 1 + ], + "compute_schedule": "rlkernel.attention.strict_ring_state.v1", + "cp_comm_accum_dtype": "fp32", + "cp_comm_attention_numeric_reduction": false, + "cp_comm_backend": "rccl_ag_rs", + "cp_comm_compute_communication": "decoupled", + "cp_comm_contract": "partial_out_lse_global_block_index", + "cp_comm_expected_blocks": [ + { + "global_block_index": 0, + "kv_block_end": 4, + "kv_block_start": 0, + "owner_cp_rank": 0, + "owner_tp_rank": 1 + }, + { + "global_block_index": 1, + "kv_block_end": 8, + "kv_block_start": 4, + "owner_cp_rank": 0, + "owner_tp_rank": 1 + }, + { + "global_block_index": 2, + "kv_block_end": 12, + "kv_block_start": 8, + "owner_cp_rank": 1, + "owner_tp_rank": 1 + }, + { + "global_block_index": 3, + "kv_block_end": 16, + "kv_block_start": 12, + "owner_cp_rank": 1, + "owner_tp_rank": 1 + } + ], + "cp_comm_expected_kv_token_range": [ + 0, + 16 + ], + "cp_comm_merge_order": "global_block_index", + "cp_comm_merge_root_cp_rank": 0, + "cp_comm_pattern": "ag_rs", + "cp_comm_query_token_ranges": [ + [ + 0, + 8 + ], + [ + 8, + 16 + ] + ], + "cp_comm_required": true, + "cp_comm_return_lse": true, + "cp_comm_runtime": "rccl", + "cp_comm_status": "implemented", + "cp_comm_strict_backward": "rs_out_backward_ag_then_ag_qkv_backward_rs", + "cp_comm_strict_contract": "ag_qkv_positions_shared_core_rs_out_lse", + "cp_comm_strict_kv_communication": "all_gather", + "cp_comm_strict_position_communication": "all_gather", + "cp_rank": 1, + "cp_world_size": 2, + "deterministic_backward": true, + "downcast_at": "final_write", + "fa_api_source": null, + "fa_package_version": null, + "fallback": false, + "fallback_reason": null, + "k_cache_rope_state": "post_rope", + "lse_domain": "attention", + "lse_dtype": "fp32", + "lse_exported": true, + "materialization": "ag_qkv_positions_shared_core_rs", + "merge_order_indices": [ + 0, + 1 + ], + "native_attention_arithmetic": true, + "num_splits": 1, + "platform": "rocm", + "production_ready": true, + "q_rope_state": "post_rope", + "reference_only": false, + "requested_backend": "flashinfer_layout_adapter", + "ring_partial_arithmetic": false, + "ring_schedule_default": true, + "rope_backend": "rlkernel.rocm.deterministic_rope", + "rope_fusion": false, + "rope_fusion_boundary": "rlkernel_rope_then_attention", + "rope_theta": 1000000.0, + "rotary_dim": null, + "softmax_scale": null, + "split_kv_control": "dense_non_split_api", + "strict_comm_autograd": true, + "strict_core_id": "rlkernel.attention.rocm.aiter_ck_dense_mha.v1", + "strict_core_row_plans": [ + { + "actual_split_boundaries": [ + [ + 0, + 16 + ] + ], + "actual_split_kv_count": 1, + "actual_split_kv_policy": "disabled", + "actual_split_kv_size": null, + "requested_split_kv_policy": "disabled", + "requested_split_kv_size": null, + "split_kv_accum_dtype": "fp32", + "split_kv_backend": "aiter.rocm.ck_dense_mha", + "split_kv_downcast_at": "final_write", + "split_kv_fallback": false, + "split_kv_fallback_reason": null, + "split_kv_merge_order": "global_block_index", + "split_kv_plan_source": "contract_exact" + }, + { + "actual_split_boundaries": [ + [ + 0, + 16 + ] + ], + "actual_split_kv_count": 1, + "actual_split_kv_policy": "disabled", + "actual_split_kv_size": null, + "requested_split_kv_policy": "disabled", + "requested_split_kv_size": null, + "split_kv_accum_dtype": "fp32", + "split_kv_backend": "aiter.rocm.ck_dense_mha", + "split_kv_downcast_at": "final_write", + "split_kv_fallback": false, + "split_kv_fallback_reason": null, + "split_kv_merge_order": "global_block_index", + "split_kv_plan_source": "contract_exact" + } + ], + "strict_full_qkv_all_gather": true, + "strict_local_kv_range": [ + 8, + 16 + ], + "strict_local_query_range": [ + 8, + 16 + ], + "strict_mode": true, + "strict_position_ids_all_gather": true, + "strict_schedule": "single_batch_aiter_ck_dense_mha_no_splitkv", + "strict_split_kv": "disabled", + "tp_rank": 1, + "tp_world_size": 2 + }, + "strict_schedule": "single_batch_aiter_ck_dense_mha_no_splitkv" + }, + "tp_rank": 1, + "tp_world_size": 2, + "transport": "rccl_ag_rs" + } + ], + "replica_count": 2, + "runtime_version": "7.14.60850", + "schema_version": "ws2_rccl_ag_rs_attention/v2", + "torch_version": "2.12.0+rocm7.14.0a20260608", + "tp_world_size": 2, + "transport": "rccl_ag_rs", + "world_size": 8 +} diff --git a/benchmarks/results/pr319_rocm_mi300x/report.md b/benchmarks/results/pr319_rocm_mi300x/report.md index e5cbe016..e0577d94 100644 --- a/benchmarks/results/pr319_rocm_mi300x/report.md +++ b/benchmarks/results/pr319_rocm_mi300x/report.md @@ -21,21 +21,21 @@ Triton backend; `strict` = `aiter.rocm.ck_dense_mha` through WS2 contract dispat | B | S | Backend | Fwd median (ms) | Fwd p95 (ms) | Fwd peak MiB | Fwd+bwd median (ms) | Fwd+bwd peak MiB | |---:|---:|---|---:|---:|---:|---:|---:| -| 1 | 1024 | native | 0.1438 | 0.1668 | 36.1 | 0.4741 | 84.3 | -| 1 | 1024 | triton | 0.3069 | 0.3300 | 32.1 | 1.7754 | 52.2 | -| 1 | 1024 | strict | 0.4045 | 0.4261 | 40.1 | 1.0480 | 1108.5 | -| 1 | 2048 | native | 0.2963 | 0.3018 | 72.3 | 1.0880 | 168.8 | -| 1 | 2048 | triton | 0.6404 | 0.6551 | 64.2 | 2.2759 | 104.5 | -| 1 | 2048 | strict | 0.4327 | 0.4475 | 80.3 | 2.0983 | 4265.0 | -| 1 | 4096 | native | 0.7014 | 0.7219 | 144.5 | 3.2456 | 337.5 | -| 1 | 4096 | triton | 1.4337 | 1.4886 | 128.5 | 4.6235 | 209.0 | -| 1 | 4096 | strict | 0.7415 | 0.7529 | 160.5 | 5.8933 | 16722.0 | -| 2 | 2048 | native | 0.4437 | 0.4696 | 144.5 | 1.9135 | 337.5 | -| 2 | 2048 | triton | 0.9821 | 0.9917 | 128.5 | 2.6656 | 209.0 | -| 2 | 2048 | strict | 0.7912 | 0.8101 | 120.5 | 3.9666 | 4369.3 | -| 4 | 2048 | native | 0.8211 | 0.8666 | 289.0 | 3.5754 | 675.0 | -| 4 | 2048 | triton | 1.7571 | 1.7957 | 257.0 | 5.1310 | 418.0 | -| 4 | 2048 | strict | 1.3856 | 1.4119 | 226.1 | 7.7189 | 4610.3 | +| 1 | 1024 | native | 0.1449 | 0.1503 | 36.1 | 0.4758 | 84.3 | +| 1 | 1024 | triton | 0.3021 | 0.3315 | 32.1 | 1.2487 | 52.2 | +| 1 | 1024 | strict | 0.3980 | 0.4205 | 40.1 | 1.0305 | 1108.5 | +| 1 | 2048 | native | 0.2972 | 0.3010 | 72.3 | 1.0875 | 168.8 | +| 1 | 2048 | triton | 0.6379 | 0.6535 | 64.2 | 2.5059 | 104.5 | +| 1 | 2048 | strict | 0.4265 | 0.4365 | 80.3 | 2.1164 | 4265.0 | +| 1 | 4096 | native | 0.7020 | 0.7232 | 144.5 | 3.2428 | 337.5 | +| 1 | 4096 | triton | 1.4280 | 1.4877 | 128.5 | 4.6182 | 209.0 | +| 1 | 4096 | strict | 0.7343 | 0.7502 | 160.5 | 5.9652 | 16722.0 | +| 2 | 2048 | native | 0.4499 | 0.4715 | 144.5 | 1.9132 | 337.5 | +| 2 | 2048 | triton | 0.9764 | 0.9864 | 128.5 | 2.6696 | 209.0 | +| 2 | 2048 | strict | 0.7826 | 0.8113 | 120.5 | 3.9917 | 4369.3 | +| 4 | 2048 | native | 0.8225 | 0.8474 | 289.0 | 3.5825 | 675.0 | +| 4 | 2048 | triton | 1.7539 | 1.7674 | 257.0 | 5.1151 | 418.0 | +| 4 | 2048 | strict | 1.3809 | 1.4118 | 226.1 | 7.6638 | 4610.3 | ## Cost of the deterministic backward @@ -44,9 +44,9 @@ already-resident forward tensors, so read the absolute det=on column for scaling | S | det=on median (ms) | det=off median (ms) | Time | det=on peak MiB | det=off peak MiB | |---:|---:|---:|---:|---:|---:| -| 1024 | 0.4455 | 0.1917 | 2.32x | 1188.3 | 180.3 | -| 2048 | 1.3399 | 0.5687 | 2.36x | 4328.5 | 264.5 | -| 4096 | 5.0313 | 2.0607 | 2.44x | 16753.0 | 433.0 | +| 1024 | 0.4490 | 0.1903 | 2.36x | 1188.3 | 180.3 | +| 2048 | 1.3386 | 0.5687 | 2.35x | 4328.5 | 264.5 | +| 4096 | 5.0450 | 2.0415 | 2.47x | 16753.0 | 433.0 | Deterministic backward peak memory scales as O(S^2): each doubling of S quadruples it. @@ -72,6 +72,32 @@ core materializes one logical row per call. Through the provider, every shape above is bitwise identical (max abs `0`). +## TP-degree invariance of raw AITER + +A head shard computed under TP=N vs the same slice of an unsharded run. TP performs no +cross-rank reduction in attention, so any nonzero value here means the kernel's result +depends on how many heads shared the launch - i.e. changing TP degree changes the numbers. + +| S | TP | Local Hq | Local Hkv | out max abs | lse max abs | Invariant | +|---:|---:|---:|---:|---:|---:|---| +| 512 | 2 | 16 | 4 | 0.000000e+00 | 0.000000e+00 | yes | +| 512 | 4 | 8 | 2 | 0.000000e+00 | 0.000000e+00 | yes | +| 512 | 8 | 4 | 1 | 0.000000e+00 | 0.000000e+00 | yes | +| 1024 | 2 | 16 | 4 | 3.906250e-03 | 1.430511e-06 | **no** | +| 1024 | 4 | 8 | 2 | 3.906250e-03 | 1.430511e-06 | **no** | +| 1024 | 8 | 4 | 1 | 3.906250e-03 | 1.430511e-06 | **no** | +| 2048 | 2 | 16 | 4 | 0.000000e+00 | 0.000000e+00 | yes | +| 2048 | 4 | 8 | 2 | 7.812500e-03 | 2.861023e-06 | **no** | +| 2048 | 8 | 4 | 1 | 7.812500e-03 | 2.861023e-06 | **no** | +| 4096 | 2 | 16 | 4 | 0.000000e+00 | 0.000000e+00 | yes | +| 4096 | 4 | 8 | 2 | 0.000000e+00 | 0.000000e+00 | yes | +| 4096 | 8 | 4 | 1 | 7.812500e-03 | 4.768372e-06 | **no** | + +RL-Kernel does not remove this dependence (per-KV-group execution would, at 2.7-3.9x +forward time). It binds the degree instead: `AttentionContract.cross_rank_fingerprint` +includes `tp_world_size`, and `validate_cross_config_alignment` fails closed when training +and rollout disagree. + ## Reproduce ```bash diff --git a/benchmarks/results/pr319_rocm_mi300x/results.json b/benchmarks/results/pr319_rocm_mi300x/results.json index 7aec1877..3d9f93fe 100644 --- a/benchmarks/results/pr319_rocm_mi300x/results.json +++ b/benchmarks/results/pr319_rocm_mi300x/results.json @@ -16,13 +16,13 @@ "seq_len": 1024, "backend": "native", "forward": { - "median_ms": 0.1438, - "p95_ms": 0.1668, + "median_ms": 0.1449, + "p95_ms": 0.1503, "peak_mib": 36.1 }, "forward_backward": { - "median_ms": 0.4741, - "p95_ms": 0.4915, + "median_ms": 0.4758, + "p95_ms": 0.4814, "peak_mib": 84.3 } }, @@ -31,13 +31,13 @@ "seq_len": 1024, "backend": "triton", "forward": { - "median_ms": 0.3069, - "p95_ms": 0.33, + "median_ms": 0.3021, + "p95_ms": 0.3315, "peak_mib": 32.1 }, "forward_backward": { - "median_ms": 1.7754, - "p95_ms": 1.864, + "median_ms": 1.2487, + "p95_ms": 1.3242, "peak_mib": 52.2 } }, @@ -46,13 +46,13 @@ "seq_len": 1024, "backend": "strict", "forward": { - "median_ms": 0.4045, - "p95_ms": 0.4261, + "median_ms": 0.398, + "p95_ms": 0.4205, "peak_mib": 40.1 }, "forward_backward": { - "median_ms": 1.048, - "p95_ms": 1.0706, + "median_ms": 1.0305, + "p95_ms": 1.0536, "peak_mib": 1108.5 } }, @@ -61,13 +61,13 @@ "seq_len": 2048, "backend": "native", "forward": { - "median_ms": 0.2963, - "p95_ms": 0.3018, + "median_ms": 0.2972, + "p95_ms": 0.301, "peak_mib": 72.3 }, "forward_backward": { - "median_ms": 1.088, - "p95_ms": 1.0986, + "median_ms": 1.0875, + "p95_ms": 1.1026, "peak_mib": 168.8 } }, @@ -76,13 +76,13 @@ "seq_len": 2048, "backend": "triton", "forward": { - "median_ms": 0.6404, - "p95_ms": 0.6551, + "median_ms": 0.6379, + "p95_ms": 0.6535, "peak_mib": 64.2 }, "forward_backward": { - "median_ms": 2.2759, - "p95_ms": 2.2853, + "median_ms": 2.5059, + "p95_ms": 2.5251, "peak_mib": 104.5 } }, @@ -91,13 +91,13 @@ "seq_len": 2048, "backend": "strict", "forward": { - "median_ms": 0.4327, - "p95_ms": 0.4475, + "median_ms": 0.4265, + "p95_ms": 0.4365, "peak_mib": 80.3 }, "forward_backward": { - "median_ms": 2.0983, - "p95_ms": 2.2269, + "median_ms": 2.1164, + "p95_ms": 2.2624, "peak_mib": 4265.0 } }, @@ -106,13 +106,13 @@ "seq_len": 4096, "backend": "native", "forward": { - "median_ms": 0.7014, - "p95_ms": 0.7219, + "median_ms": 0.702, + "p95_ms": 0.7232, "peak_mib": 144.5 }, "forward_backward": { - "median_ms": 3.2456, - "p95_ms": 3.2945, + "median_ms": 3.2428, + "p95_ms": 3.2862, "peak_mib": 337.5 } }, @@ -121,13 +121,13 @@ "seq_len": 4096, "backend": "triton", "forward": { - "median_ms": 1.4337, - "p95_ms": 1.4886, + "median_ms": 1.428, + "p95_ms": 1.4877, "peak_mib": 128.5 }, "forward_backward": { - "median_ms": 4.6235, - "p95_ms": 4.672, + "median_ms": 4.6182, + "p95_ms": 4.6719, "peak_mib": 209.0 } }, @@ -136,13 +136,13 @@ "seq_len": 4096, "backend": "strict", "forward": { - "median_ms": 0.7415, - "p95_ms": 0.7529, + "median_ms": 0.7343, + "p95_ms": 0.7502, "peak_mib": 160.5 }, "forward_backward": { - "median_ms": 5.8933, - "p95_ms": 5.9776, + "median_ms": 5.9652, + "p95_ms": 6.0334, "peak_mib": 16722.0 } }, @@ -151,13 +151,13 @@ "seq_len": 2048, "backend": "native", "forward": { - "median_ms": 0.4437, - "p95_ms": 0.4696, + "median_ms": 0.4499, + "p95_ms": 0.4715, "peak_mib": 144.5 }, "forward_backward": { - "median_ms": 1.9135, - "p95_ms": 1.9372, + "median_ms": 1.9132, + "p95_ms": 1.943, "peak_mib": 337.5 } }, @@ -166,13 +166,13 @@ "seq_len": 2048, "backend": "triton", "forward": { - "median_ms": 0.9821, - "p95_ms": 0.9917, + "median_ms": 0.9764, + "p95_ms": 0.9864, "peak_mib": 128.5 }, "forward_backward": { - "median_ms": 2.6656, - "p95_ms": 2.6806, + "median_ms": 2.6696, + "p95_ms": 2.6816, "peak_mib": 209.0 } }, @@ -181,13 +181,13 @@ "seq_len": 2048, "backend": "strict", "forward": { - "median_ms": 0.7912, - "p95_ms": 0.8101, + "median_ms": 0.7826, + "p95_ms": 0.8113, "peak_mib": 120.5 }, "forward_backward": { - "median_ms": 3.9666, - "p95_ms": 4.0203, + "median_ms": 3.9917, + "p95_ms": 4.0312, "peak_mib": 4369.3 } }, @@ -196,13 +196,13 @@ "seq_len": 2048, "backend": "native", "forward": { - "median_ms": 0.8211, - "p95_ms": 0.8666, + "median_ms": 0.8225, + "p95_ms": 0.8474, "peak_mib": 289.0 }, "forward_backward": { - "median_ms": 3.5754, - "p95_ms": 3.6514, + "median_ms": 3.5825, + "p95_ms": 3.6547, "peak_mib": 675.0 } }, @@ -211,13 +211,13 @@ "seq_len": 2048, "backend": "triton", "forward": { - "median_ms": 1.7571, - "p95_ms": 1.7957, + "median_ms": 1.7539, + "p95_ms": 1.7674, "peak_mib": 257.0 }, "forward_backward": { - "median_ms": 5.131, - "p95_ms": 5.1587, + "median_ms": 5.1151, + "p95_ms": 5.1429, "peak_mib": 418.0 } }, @@ -226,13 +226,13 @@ "seq_len": 2048, "backend": "strict", "forward": { - "median_ms": 1.3856, - "p95_ms": 1.4119, + "median_ms": 1.3809, + "p95_ms": 1.4118, "peak_mib": 226.1 }, "forward_backward": { - "median_ms": 7.7189, - "p95_ms": 7.8018, + "median_ms": 7.6638, + "p95_ms": 7.7376, "peak_mib": 4610.3 } } @@ -241,46 +241,46 @@ { "seq_len": 1024, "deterministic": { - "median_ms": 0.4455, - "p95_ms": 0.4746, + "median_ms": 0.449, + "p95_ms": 0.4681, "peak_mib": 1188.3 }, "non_deterministic": { - "median_ms": 0.1917, - "p95_ms": 0.1971, + "median_ms": 0.1903, + "p95_ms": 0.197, "peak_mib": 180.3 }, - "time_ratio": 2.32, + "time_ratio": 2.36, "memory_ratio": 6.6 }, { "seq_len": 2048, "deterministic": { - "median_ms": 1.3399, - "p95_ms": 1.3742, + "median_ms": 1.3386, + "p95_ms": 1.379, "peak_mib": 4328.5 }, "non_deterministic": { "median_ms": 0.5687, - "p95_ms": 0.5762, + "p95_ms": 0.5761, "peak_mib": 264.5 }, - "time_ratio": 2.36, + "time_ratio": 2.35, "memory_ratio": 16.4 }, { "seq_len": 4096, "deterministic": { - "median_ms": 5.0313, - "p95_ms": 5.1068, + "median_ms": 5.045, + "p95_ms": 5.0906, "peak_mib": 16753.0 }, "non_deterministic": { - "median_ms": 2.0607, - "p95_ms": 2.089, + "median_ms": 2.0415, + "p95_ms": 2.072, "peak_mib": 433.0 }, - "time_ratio": 2.44, + "time_ratio": 2.47, "memory_ratio": 38.7 } ], @@ -369,5 +369,115 @@ "raw_aiter_lse_max_abs": 0.0, "raw_aiter_is_batch_invariant": true } + ], + "tp_head_count_sensitivity": [ + { + "seq_len": 512, + "tp": 2, + "local_q_heads": 16, + "local_kv_heads": 4, + "out_max_abs": 0.0, + "lse_max_abs": 0.0, + "tp_degree_invariant": true + }, + { + "seq_len": 512, + "tp": 4, + "local_q_heads": 8, + "local_kv_heads": 2, + "out_max_abs": 0.0, + "lse_max_abs": 0.0, + "tp_degree_invariant": true + }, + { + "seq_len": 512, + "tp": 8, + "local_q_heads": 4, + "local_kv_heads": 1, + "out_max_abs": 0.0, + "lse_max_abs": 0.0, + "tp_degree_invariant": true + }, + { + "seq_len": 1024, + "tp": 2, + "local_q_heads": 16, + "local_kv_heads": 4, + "out_max_abs": 0.00390625, + "lse_max_abs": 1.430511474609375e-06, + "tp_degree_invariant": false + }, + { + "seq_len": 1024, + "tp": 4, + "local_q_heads": 8, + "local_kv_heads": 2, + "out_max_abs": 0.00390625, + "lse_max_abs": 1.430511474609375e-06, + "tp_degree_invariant": false + }, + { + "seq_len": 1024, + "tp": 8, + "local_q_heads": 4, + "local_kv_heads": 1, + "out_max_abs": 0.00390625, + "lse_max_abs": 1.430511474609375e-06, + "tp_degree_invariant": false + }, + { + "seq_len": 2048, + "tp": 2, + "local_q_heads": 16, + "local_kv_heads": 4, + "out_max_abs": 0.0, + "lse_max_abs": 0.0, + "tp_degree_invariant": true + }, + { + "seq_len": 2048, + "tp": 4, + "local_q_heads": 8, + "local_kv_heads": 2, + "out_max_abs": 0.0078125, + "lse_max_abs": 2.86102294921875e-06, + "tp_degree_invariant": false + }, + { + "seq_len": 2048, + "tp": 8, + "local_q_heads": 4, + "local_kv_heads": 1, + "out_max_abs": 0.0078125, + "lse_max_abs": 2.86102294921875e-06, + "tp_degree_invariant": false + }, + { + "seq_len": 4096, + "tp": 2, + "local_q_heads": 16, + "local_kv_heads": 4, + "out_max_abs": 0.0, + "lse_max_abs": 0.0, + "tp_degree_invariant": true + }, + { + "seq_len": 4096, + "tp": 4, + "local_q_heads": 8, + "local_kv_heads": 2, + "out_max_abs": 0.0, + "lse_max_abs": 0.0, + "tp_degree_invariant": true + }, + { + "seq_len": 4096, + "tp": 8, + "local_q_heads": 4, + "local_kv_heads": 1, + "out_max_abs": 0.0078125, + "lse_max_abs": 4.76837158203125e-06, + "tp_degree_invariant": false + } ] } \ No newline at end of file diff --git a/rl_engine/integrations/vime/attention.py b/rl_engine/integrations/vime/attention.py index c848b2d7..f3f29e6f 100644 --- a/rl_engine/integrations/vime/attention.py +++ b/rl_engine/integrations/vime/attention.py @@ -30,6 +30,7 @@ import torch from rl_engine.kernels.attention_contract import ( + CROSS_CONFIG_BOUND_DEGREES, AttentionContract, AttentionDType, AttentionMode, @@ -412,6 +413,23 @@ def attention_provider(request: Any) -> AttentionProviderResult: "cp_is_merge_axis": False, } provenance["lse_domain"] = "attention" + # The qualified ROCm core's reduction order depends on the launch head + # count, so a head shard computed under TP=4 is not bit-identical to the + # same shard under TP=8 at some shapes. RL-Kernel binds the degree rather + # than paying ~3x forward time to remove the dependence: training and + # rollout must run the same TP/CP degree. ``contract_id`` encodes both, so + # comparing it across the two sides is the preflight that enforces this. + provenance["cross_config_binding"] = { + "bound_degrees": list(CROSS_CONFIG_BOUND_DEGREES), + "tp_world_size": contract.sharding.tp_world_size, + "cp_world_size": contract.sharding.cp_world_size, + "binding_token": "contract_id", + "tp_degree_invariant": False, + "reason": ( + "AITER/CK dense MHA reduction order is launch-head-count dependent; " + "train and rollout must use the same TP degree" + ), + } return AttentionProviderResult( out=out, lse=lse, diff --git a/rl_engine/kernels/attention_contract.py b/rl_engine/kernels/attention_contract.py index 96acdeba..81802d77 100644 --- a/rl_engine/kernels/attention_contract.py +++ b/rl_engine/kernels/attention_contract.py @@ -579,6 +579,81 @@ def validate_split_kv_alignment( ) +# Parallel degrees that must be identical on both sides of a cross-config +# comparison. This is not a portability preference: the qualified ROCm vendor +# core (AITER/CK dense MHA) produces launch-shape-dependent reduction order, so +# a head shard computed under TP=4 is not bit-identical to the same head shard +# computed under TP=8 at some shapes. Measured on MI300X, BF16, Hq=32/Hkv=8/ +# D=128, causal, TP=1 versus the sharded run: +# +# 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) +# +# Executing one KV group per launch removes the dependence and costs 2.7-3.9x +# forward time. RL-Kernel instead binds the degree: training and rollout must +# run the same TP (and CP) degree, and a mismatch fails closed here rather than +# silently producing different logprobs. +CROSS_CONFIG_BOUND_DEGREES = ("tp_world_size", "cp_world_size") + + +def validate_cross_config_alignment( + training: "AttentionContract", + rollout: "AttentionContract", +) -> None: + """Fail closed unless both sides describe one comparable attention invocation. + + ``AttentionContract.cross_rank_fingerprint`` is the single token that + encodes this; comparing fingerprints is the cheap distributed preflight. + This function exists to explain *which* field diverged when they disagree, + so a cross-config drift investigation does not start from one opaque hash. + """ + + if not isinstance(training, AttentionContract) or not isinstance(rollout, AttentionContract): + raise AttentionContractError("both sides must be AttentionContract instances") + + degree_mismatches = [ + f"{name}: train={getattr(training.sharding, name)} " + f"rollout={getattr(rollout.sharding, name)}" + for name in CROSS_CONFIG_BOUND_DEGREES + if getattr(training.sharding, name) != getattr(rollout.sharding, name) + ] + if degree_mismatches: + raise AttentionContractError( + "training and rollout must use the same parallel degrees for a bitwise " + "attention comparison; the qualified ROCm core's reduction order depends on " + "the launch head count, so a differing degree changes the numbers: " + + "; ".join(degree_mismatches) + ) + + layout_mismatches = [ + name + for name in ("global_q_heads", "global_kv_heads") + if getattr(training.sharding, name) != getattr(rollout.sharding, name) + ] + if layout_mismatches: + raise AttentionContractError( + "training and rollout describe different global head layouts: " + + ", ".join(layout_mismatches) + ) + + scalar_mismatches = [ + name + for name in ("dtype", "head_dim", "causal", "export_lse") + if getattr(training, name) != getattr(rollout, name) + ] + if scalar_mismatches: + raise AttentionContractError( + "training and rollout attention contracts differ: " + ", ".join(scalar_mismatches) + ) + + if training.split_kv != rollout.split_kv: + raise AttentionContractError("training and rollout Split-KV policies differ") + if training.reduction != rollout.reduction: + raise AttentionContractError("training and rollout reduction specs differ") + + @dataclass(frozen=True, order=True) class SplitKVRuntimeCoordinate: """Identity of one batch/rank/owner Split-KV runtime plan.""" @@ -1506,6 +1581,8 @@ class AttentionDispatchResult: "STRICT_ATTENTION_ROCM_PRODUCTION_CORE_ID", "STRICT_ATTENTION_ROCM_SCHEDULE_ID", "STRICT_ATTENTION_SCHEDULE_ID", + "CROSS_CONFIG_BOUND_DEGREES", + "validate_cross_config_alignment", "validate_split_kv_alignment", "validate_split_kv_plan_set_alignment", ] diff --git a/tests/test_attention_dispatch.py b/tests/test_attention_dispatch.py index 4c174557..a4d9dac9 100644 --- a/tests/test_attention_dispatch.py +++ b/tests/test_attention_dispatch.py @@ -23,6 +23,7 @@ ReductionSpec, ShardingSpec, SplitKVSpec, + validate_cross_config_alignment, ) from rl_engine.kernels.registry import KernelRegistry, OpBackend, _rocm_strict_attention_available @@ -305,3 +306,131 @@ def test_contract_fingerprint_is_rank_independent(): # Both TP ranks of one logical invocation agree; a different TP degree does not. assert left_tp2.cross_rank_fingerprint() == right_tp2.cross_rank_fingerprint() assert left.cross_rank_fingerprint() != left_tp2.cross_rank_fingerprint() + + +# --------------------------------------------------------------------------- +# Cross-config binding: train and rollout must use the same parallel degrees +# --------------------------------------------------------------------------- + + +def _tp_contract(tp_world_size: int, tp_rank: int = 0, seq_len: int = 128) -> AttentionContract: + """A contract for one TP rank of a 32Q/8KV layout.""" + + local_q = 32 // tp_world_size + local_kv = 8 // tp_world_size + sharding = ShardingSpec( + tp_rank=tp_rank, + tp_world_size=tp_world_size, + cp_rank=0, + cp_world_size=1, + global_q_heads=32, + global_kv_heads=8, + local_q_head_start=tp_rank * local_q, + local_q_heads=local_q, + local_kv_head_start=tp_rank * local_kv, + local_kv_heads=local_kv, + global_sequence_length=seq_len, + local_sequence_length=seq_len, + global_block_indices=(0,), + global_block_token_starts=(0,), + local_block_offsets=(0, seq_len), + ) + return AttentionContract( + role=AttentionRole.TRAIN, + mode=AttentionMode.PREFILL, + dtype=AttentionDType.BF16, + batch_size=1, + query_sequence_length=seq_len, + head_dim=128, + causal=True, + causal_offsets=(0,), + sharding=sharding, + reduction=ReductionSpec(), + split_kv=SplitKVSpec.disabled(), + export_lse=True, + ) + + +def test_matching_degrees_pass_cross_config_alignment(): + validate_cross_config_alignment(_tp_contract(4, tp_rank=0), _tp_contract(4, tp_rank=3)) + + +def test_tp_degree_mismatch_fails_closed(): + """A differing TP degree changes the bits, so it must not be comparable. + + The qualified ROCm core's reduction order depends on the launch head count: + measured on MI300X the same head shard differs by up to 7.8125e-03 between + TP degrees at some shapes. Binding the degree is what makes the strict + claim survive, so the mismatch has to fail rather than warn. + """ + + with pytest.raises(AttentionContractError, match="same parallel degrees"): + validate_cross_config_alignment(_tp_contract(4), _tp_contract(8)) + + +def test_cp_degree_mismatch_fails_closed(): + train = _tp_contract(2) + rollout_sharding = ShardingSpec( + tp_rank=0, + tp_world_size=2, + cp_rank=0, + cp_world_size=2, + global_q_heads=32, + global_kv_heads=8, + local_q_head_start=0, + local_q_heads=16, + local_kv_head_start=0, + local_kv_heads=4, + global_sequence_length=256, + local_sequence_length=128, + global_block_indices=(0,), + global_block_token_starts=(0,), + local_block_offsets=(0, 128), + ) + rollout = AttentionContract( + role=AttentionRole.INFER, + mode=AttentionMode.PREFILL, + dtype=AttentionDType.BF16, + batch_size=1, + query_sequence_length=128, + head_dim=128, + causal=True, + causal_offsets=(0,), + sharding=rollout_sharding, + reduction=ReductionSpec(), + split_kv=SplitKVSpec.disabled(), + export_lse=True, + ) + with pytest.raises(AttentionContractError, match="same parallel degrees"): + validate_cross_config_alignment(train, rollout) + + +def test_contract_fingerprint_separates_tp_degrees(): + """The cheap preflight must also catch it, not only the explaining validator.""" + + assert _tp_contract(4).cross_rank_fingerprint() != _tp_contract(8).cross_rank_fingerprint() + # ...while ranks of one TP degree still agree. + assert ( + _tp_contract(4, tp_rank=0).cross_rank_fingerprint() + == _tp_contract(4, tp_rank=3).cross_rank_fingerprint() + ) + + +def test_dtype_and_split_kv_mismatches_are_explained(): + train = _tp_contract(2) + rollout = AttentionContract( + role=train.role, + mode=train.mode, + dtype=AttentionDType.FP16, + batch_size=train.batch_size, + query_sequence_length=train.query_sequence_length, + head_dim=train.head_dim, + causal=train.causal, + causal_offsets=train.causal_offsets, + sharding=train.sharding, + reduction=train.reduction, + split_kv=train.split_kv, + export_lse=True, + ) + with pytest.raises(AttentionContractError, match="dtype"): + validate_cross_config_alignment(train, rollout) diff --git a/tests/test_vime_attention_provider.py b/tests/test_vime_attention_provider.py index 3a8afc35..26d04dac 100644 --- a/tests/test_vime_attention_provider.py +++ b/tests/test_vime_attention_provider.py @@ -270,3 +270,51 @@ def test_explicit_scale_is_honored(): explicit = attention_provider(_request(seq_len=128, seed=8, softmax_scale=scale)) assert torch.equal(default.out, explicit.out) + + +@requires_strict_rocm +def test_provenance_records_the_cross_config_degree_binding(): + """The binding must be auditable per call, not only documented. + + RL-Kernel does not make the strict path TP-degree invariant: the qualified + vendor core's reduction order depends on the launch head count, and + removing that dependence costs ~3x forward time. The path instead binds + the degree, so every result has to carry what it was bound to. + """ + + result = attention_provider(_request(seq_len=256)) + binding = result.provenance["cross_config_binding"] + + assert binding["bound_degrees"] == ["tp_world_size", "cp_world_size"] + assert binding["tp_degree_invariant"] is False + assert binding["binding_token"] == "contract_id" + assert binding["tp_world_size"] == 1 + assert binding["cp_world_size"] == 1 + + +@requires_strict_rocm +def test_tp_degree_change_changes_the_contract_id(): + """Train and rollout on different TP degrees must not compare as equal.""" + + class _Group: + def __init__(self, rank, size): + self._rank, self._size = rank, size + + def rank(self): + return self._rank + + def size(self): + return self._size + + ids = {} + for tp in (1, 2, 4): + request = _request(seq_len=256, seed=4) + local_q, local_kv = GLOBAL_Q_HEADS // tp, GLOBAL_KV_HEADS // tp + request.query = request.query[:, :local_q] + request.key = request.key[:, :local_kv] + request.value = request.value[:, :local_kv] + request.metadata = _metadata(tp_rank=0, tp_world_size=tp) + request.tensor_parallel_group = _Group(0, tp) + ids[tp] = attention_provider(request).contract_id + + assert len({ids[1], ids[2], ids[4]}) == 3 From 5cf23ecc5882537edddf1c7d7282f4c64dddf521 Mon Sep 17 00:00:00 2001 From: vensen Date: Fri, 28 Aug 2026 16:42:19 +0000 Subject: [PATCH 16/35] feat(distributed): add deterministic ROCm collectives --- benchmarks/benchmark_rocm_collectives.py | 290 ++++++++++++ csrc/ops.cpp | 14 +- docs/design/rocm-deterministic-collectives.md | 112 +++++ rl_engine/distributed/__init__.py | 12 +- .../distributed/transport_collectives.py | 419 ++++++++++++++++++ .../kernels/ops/cuda/attention/__init__.py | 2 + .../kernels/ops/cuda/attention/cp_comm.py | 183 +++++++- rl_engine/kernels/ops/pytorch/ffn/ffn.py | 39 +- setup.py | 9 +- .../test_rocm_attention_transport.py | 95 ++++ ...test_transport_deterministic_collective.py | 375 ++++++++++++++++ tests/test_build_platform_collectives.py | 54 +++ tests/test_qwen_ffn.py | 19 + tests/test_rocm_collective_benchmark.py | 73 +++ 14 files changed, 1676 insertions(+), 20 deletions(-) create mode 100644 benchmarks/benchmark_rocm_collectives.py create mode 100644 docs/design/rocm-deterministic-collectives.md create mode 100644 rl_engine/distributed/transport_collectives.py create mode 100644 tests/distributed/test_rocm_attention_transport.py create mode 100644 tests/distributed/test_transport_deterministic_collective.py create mode 100644 tests/test_build_platform_collectives.py create mode 100644 tests/test_rocm_collective_benchmark.py diff --git a/benchmarks/benchmark_rocm_collectives.py b/benchmarks/benchmark_rocm_collectives.py new file mode 100644 index 00000000..6202e2e5 --- /dev/null +++ b/benchmarks/benchmark_rocm_collectives.py @@ -0,0 +1,290 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2026 RL-Kernel Contributors + +"""Benchmark deterministic ROCm collectives against native RCCL. + +Example: + + torchrun --standalone --nproc-per-node=8 \ + benchmarks/benchmark_rocm_collectives.py \ + --size-bytes 4096 65536 1048576 16777216 \ + --output benchmarks/results/rocm_collectives_mi300x.json + +The native RCCL rows are performance references only. They are not used as a +bitwise correctness oracle because their floating-point reduction order is not +part of the strict deterministic contract. +""" + +from __future__ import annotations + +import argparse +import json +import os +import statistics +import time +from pathlib import Path +from typing import Callable, Sequence + +import torch +import torch.distributed as dist + +from rl_engine.distributed import RCCLDeterministicCollective + +_DTYPES = { + "bf16": torch.bfloat16, + "fp16": torch.float16, + "fp32": torch.float32, +} +_OPERATIONS = ("all_reduce", "all_gather", "reduce_scatter") + + +def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--size-bytes", + type=int, + nargs="+", + default=[4 * 1024, 64 * 1024, 1024 * 1024, 16 * 1024 * 1024], + ) + parser.add_argument("--dtype", choices=tuple(_DTYPES), default="bf16") + parser.add_argument("--operations", nargs="+", choices=_OPERATIONS, default=_OPERATIONS) + parser.add_argument("--warmup", type=int, default=10) + parser.add_argument("--iterations", type=int, default=50) + parser.add_argument("--samples", type=int, default=5) + parser.add_argument("--output", type=Path) + return parser.parse_args(argv) + + +def _validate_args(args: argparse.Namespace) -> None: + if any(size <= 0 for size in args.size_bytes): + raise ValueError("every --size-bytes value must be positive") + if args.warmup < 0: + raise ValueError("--warmup must be non-negative") + if args.iterations <= 0 or args.samples <= 0: + raise ValueError("--iterations and --samples must be positive") + + +def _timed_sample(operation: Callable[[], None], *, warmup: int, iterations: int) -> float: + for _ in range(warmup): + operation() + torch.cuda.synchronize() + dist.barrier() + start = time.perf_counter() + for _ in range(iterations): + operation() + torch.cuda.synchronize() + elapsed = (time.perf_counter() - start) / iterations + + # Report the slowest rank, which is the end-to-end collective latency. + elapsed_tensor = torch.tensor([elapsed], dtype=torch.float64, device="cuda") + dist.all_reduce(elapsed_tensor, op=dist.ReduceOp.MAX) + return float(elapsed_tensor.item()) + + +def _benchmark( + operation: Callable[[], None], + *, + warmup: int, + iterations: int, + samples: int, +) -> dict[str, object]: + timings = [ + _timed_sample(operation, warmup=warmup if index == 0 else 0, iterations=iterations) + for index in range(samples) + ] + median = statistics.median(timings) + return { + "median_us": median * 1.0e6, + "min_us": min(timings) * 1.0e6, + "max_us": max(timings) * 1.0e6, + "samples_us": [value * 1.0e6 for value in timings], + } + + +def _make_inputs( + *, + size_bytes: int, + dtype: torch.dtype, + world_size: int, + rank: int, + device: torch.device, +) -> tuple[torch.Tensor, int]: + element_size = torch.empty((), dtype=dtype).element_size() + elements = max(world_size, size_bytes // element_size) + elements -= elements % world_size + generator = torch.Generator(device="cpu").manual_seed(942 + rank) + tensor = torch.randn(elements, generator=generator, dtype=torch.float32).to( + device=device, + dtype=dtype, + ) + return tensor.contiguous(), elements * element_size + + +def _operation_pair( + name: str, + input_tensor: torch.Tensor, + collective: RCCLDeterministicCollective, + world_size: int, +) -> tuple[Callable[[], None], Callable[[], None], torch.Tensor, torch.Tensor]: + if name == "all_reduce": + deterministic_out = torch.empty_like(input_tensor) + native_out = torch.empty_like(input_tensor) + + def deterministic() -> None: + collective.all_reduce(input_tensor, out=deterministic_out) + + def native() -> None: + native_out.copy_(input_tensor) + dist.all_reduce(native_out) + + elif name == "all_gather": + output_shape = (input_tensor.numel() * world_size,) + deterministic_out = torch.empty(output_shape, dtype=input_tensor.dtype, device="cuda") + native_out = torch.empty_like(deterministic_out) + + def deterministic() -> None: + collective.all_gather(input_tensor, out=deterministic_out) + + def native() -> None: + dist.all_gather_into_tensor(native_out, input_tensor) + + elif name == "reduce_scatter": + output_shape = (input_tensor.numel() // world_size,) + deterministic_out = torch.empty(output_shape, dtype=input_tensor.dtype, device="cuda") + native_out = torch.empty_like(deterministic_out) + + def deterministic() -> None: + collective.reduce_scatter(input_tensor, out=deterministic_out) + + def native() -> None: + dist.reduce_scatter_tensor(native_out, input_tensor) + + else: # pragma: no cover - argparse constrains this value + raise ValueError(f"unsupported operation: {name}") + + return deterministic, native, deterministic_out, native_out + + +def run(args: argparse.Namespace) -> dict[str, object] | None: + _validate_args(args) + if torch.version.hip is None or not torch.cuda.is_available(): + raise RuntimeError("the ROCm collective benchmark requires an available AMD GPU") + + local_rank = int(os.environ.get("LOCAL_RANK", "0")) + torch.cuda.set_device(local_rank) + dist.init_process_group("nccl", init_method="env://") + rank = dist.get_rank() + world_size = dist.get_world_size() + if world_size not in (2, 4, 8): + raise RuntimeError(f"the benchmark requires 2, 4, or 8 ranks, got {world_size}") + device = torch.device("cuda", local_rank) + dtype = _DTYPES[args.dtype] + max_size_bytes = max(args.size_bytes) + dtype.itemsize * world_size + + rows: list[dict[str, object]] = [] + try: + with RCCLDeterministicCollective( + device=device, + max_size_bytes=max_size_bytes, + ) as collective: + for requested_size in args.size_bytes: + input_tensor, actual_size = _make_inputs( + size_bytes=requested_size, + dtype=dtype, + world_size=world_size, + rank=rank, + device=device, + ) + for name in args.operations: + deterministic, native, deterministic_out, native_out = _operation_pair( + name, + input_tensor, + collective, + world_size, + ) + deterministic() + deterministic_repeat = deterministic_out.clone() + deterministic() + repeat_bitwise = bool(torch.equal(deterministic_out, deterministic_repeat)) + native() + max_abs_vs_native = float( + (deterministic_out.float() - native_out.float()).abs().max().item() + ) + + torch.cuda.reset_peak_memory_stats(device) + deterministic_timing = _benchmark( + deterministic, + warmup=args.warmup, + iterations=args.iterations, + samples=args.samples, + ) + deterministic_peak = int(torch.cuda.max_memory_allocated(device)) + torch.cuda.reset_peak_memory_stats(device) + native_timing = _benchmark( + native, + warmup=args.warmup, + iterations=args.iterations, + samples=args.samples, + ) + native_peak = int(torch.cuda.max_memory_allocated(device)) + deterministic_us = float(deterministic_timing["median_us"]) + native_us = float(native_timing["median_us"]) + rows.append( + { + "operation": name, + "requested_size_bytes": requested_size, + "actual_input_bytes": actual_size, + "dtype": args.dtype, + "deterministic": deterministic_timing, + "native_rccl": native_timing, + "latency_ratio_vs_native": deterministic_us / native_us, + "deterministic_input_gbps": actual_size / (deterministic_us * 1.0e3), + "native_input_gbps": actual_size / (native_us * 1.0e3), + "repeat_bitwise": repeat_bitwise, + "max_abs_vs_native": max_abs_vs_native, + "deterministic_workspace_bytes": collective.workspace_size_bytes, + "deterministic_peak_allocated_bytes": deterministic_peak, + "native_peak_allocated_bytes": native_peak, + } + ) + + reports: list[list[dict[str, object]] | None] = [None] * world_size + dist.all_gather_object(reports, rows) + if rank != 0: + return None + payload = { + "schema_version": "rlkernel.rocm_collective_benchmark.v1", + "world_size": world_size, + "device": torch.cuda.get_device_name(device), + "hip_version": torch.version.hip, + "collective_backend": RCCLDeterministicCollective.backend_id, + "reduction_order": RCCLDeterministicCollective.reduction_order, + "supports_compute_communication_fusion": False, + "warmup": args.warmup, + "iterations": args.iterations, + "samples": args.samples, + "rows": rows, + "all_rank_repeat_bitwise": all( + bool(row["repeat_bitwise"]) + for rank_rows in reports + if rank_rows is not None + for row in rank_rows + ), + } + if args.output is not None: + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(json.dumps(payload, indent=2), encoding="utf-8") + return payload + finally: + dist.destroy_process_group() + + +def main(argv: Sequence[str] | None = None) -> int: + payload = run(parse_args(argv)) + if payload is not None: + print(json.dumps(payload, indent=2)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/csrc/ops.cpp b/csrc/ops.cpp index f1e66dba..fb84dc64 100644 --- a/csrc/ops.cpp +++ b/csrc/ops.cpp @@ -78,7 +78,7 @@ torch::Tensor lm_head_sm90_forward_fp32(torch::Tensor hidden, torch::optional bias); #endif -#if defined(__CUDACC__) || defined(KERNEL_ALIGN_WITH_CUDA) +#if defined(__CUDACC__) || defined(KERNEL_ALIGN_WITH_CUDA) || defined(KERNEL_ALIGN_WITH_ROCM) torch::Tensor fused_logp_forward_out(torch::Tensor logits, torch::Tensor token_ids, torch::Tensor output); torch::Tensor fused_logp_forward_fp32(torch::Tensor logits, torch::Tensor token_ids); torch::Tensor fused_logp_forward_indexed_out(torch::Tensor logits, torch::Tensor token_ids, torch::Tensor row_indices, torch::Tensor output); @@ -93,7 +93,9 @@ torch::Tensor deterministic_logp_forward_fp32(torch::Tensor logits, torch::Tenso torch::Tensor deterministic_logp_forward_indexed_out(torch::Tensor logits, torch::Tensor token_ids, torch::Tensor row_indices, torch::Tensor output); torch::Tensor deterministic_logp_forward_indexed_fp32(torch::Tensor logits, torch::Tensor token_ids, torch::Tensor row_indices); -// Single-node TP=8 deterministic collectives. +#if !defined(USE_ROCM) && !defined(KERNEL_ALIGN_WITH_ROCM) +// Single-node TP=8 deterministic CUDA IPC collectives. ROCm uses the +// rank-ordered RCCL transport in rl_engine.distributed.transport_collectives. std::tuple, int64_t> deterministic_collective_ipc_meta( torch::Tensor& tensor); int64_t deterministic_collective_create( @@ -106,6 +108,7 @@ void deterministic_collective_stage(int64_t handle, torch::Tensor& input); void deterministic_collective_all_reduce(int64_t handle, torch::Tensor& output); void deterministic_collective_reduce_scatter(int64_t handle, torch::Tensor& output); void deterministic_collective_all_gather(int64_t handle, torch::Tensor& output); +#endif // Batch-Invariant Deterministic GEMM Declarations torch::Tensor det_gemm_fwd(torch::Tensor a, torch::Tensor b); @@ -391,7 +394,7 @@ PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) { "Single-card SM90 batch-invariant LM-head forward with fp32 output"); #endif -#if defined(__CUDACC__) || defined(KERNEL_ALIGN_WITH_CUDA) +#if defined(__CUDACC__) || defined(KERNEL_ALIGN_WITH_CUDA) || defined(KERNEL_ALIGN_WITH_ROCM) m.def("fused_logp_forward_out", &fused_logp_forward_out, "Fused logp out"); m.def("fused_logp_forward_fp32", &fused_logp_forward_fp32, "Fused logp fp32"); m.def("fused_logp_forward_indexed_out", &fused_logp_forward_indexed_out, "Fused logp indexed out"); @@ -406,7 +409,9 @@ PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) { m.def("deterministic_logp_forward_indexed_out", &deterministic_logp_forward_indexed_out, "Batch-invariant deterministic logp indexed out"); m.def("deterministic_logp_forward_indexed_fp32", &deterministic_logp_forward_indexed_fp32, "Batch-invariant deterministic logp indexed fp32"); - // Single-node TP=8 fixed-tree collectives. +#if !defined(USE_ROCM) && !defined(KERNEL_ALIGN_WITH_ROCM) + // Single-node TP=8 fixed-tree CUDA IPC collectives. ROCm dispatches to + // the Python RCCL transport implementation instead. m.def( "deterministic_collective_ipc_meta", &deterministic_collective_ipc_meta, @@ -435,6 +440,7 @@ PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) { "deterministic_collective_all_gather", &deterministic_collective_all_gather, "Run the TP=8 deterministic rank-ordered all-gather kernel"); +#endif // Prefix-shared attention uses NVIDIA PTX and falls back to PyTorch SDPA on ROCm. #if !defined(USE_ROCM) diff --git a/docs/design/rocm-deterministic-collectives.md b/docs/design/rocm-deterministic-collectives.md new file mode 100644 index 00000000..ce104332 --- /dev/null +++ b/docs/design/rocm-deterministic-collectives.md @@ -0,0 +1,112 @@ +# ROCm deterministic collectives + +This document defines the ROCm communication boundary used by deterministic +TP/CP kernels. The implementation is intentionally separate from the CUDA IPC +collective in `csrc/cuda/distributed/deterministic_collective.cu`. + +## Contract + +`RCCLDeterministicCollective` implements `all_reduce`, `all_gather`, and +`reduce_scatter` with the same Python call shape as the CUDA collective. It +supports process-group sizes 1, 2, 4, and 8 and FP32/FP16/BF16 reductions. +Inputs and outputs must be contiguous and all ranks must call operations in the +same order with matching shapes, dtypes, and capacity. + +RCCL is used only for rank-ordered tensor transport: + +1. `all_gather_into_tensor` gathers every rank's bit patterns. +2. Each rank evaluates the same balanced tree locally: + `((rank0 + rank1) + (rank2 + rank3)) + ...`. +3. `reduce_scatter` slices the rank-owned rows after that fixed reduction. + +RCCL's `all_reduce` and `reduce_scatter` are not used for strict reductions. +They guarantee a mathematical reduction but do not expose a stable +floating-point operand order. Delegating the arithmetic to them would weaken +the cross-TP bitwise contract. + +`create_deterministic_collective` is the platform boundary. It selects the +existing CUDA IPC implementation on NVIDIA and the RCCL transport +implementation when `torch.version.hip` is set. The FFN path calls this factory +instead of importing the CUDA class directly. + +The ROCm extension build also excludes the CUDA IPC source and does not link +`libcuda`; the Python transport has no CUDA-driver dependency. + +## Relationship to vLLM + +The backend split follows the useful parts of vLLM's device communicator +design: + +- PyTorch exposes RCCL through the `nccl` process-group API. +- ROCm AllGather uses `torch.distributed.all_gather_into_tensor` rather than a + manually allocated PyNccl path. +- Backend, topology, world-size, dtype, layout, and capacity checks fail closed + before entering an optimized path. + +vLLM QuickReduce, custom all-reduce, and AITER all-reduce are not used in the +strict path. Those are valuable performance implementations, but their +reduction order is not the fixed balanced tree required here. They can be +added later as an explicitly non-strict performance mode with separate +provenance and toleranced correctness tests. + +## Compute/communication fusion + +The current ROCm collective is synchronous and reports +`supports_async_overlap = False` and +`supports_compute_communication_fusion = False`. FFN and Attention keep the +dependency boundaries explicit: + +```text +sequence-parallel FFN: AllGather(input) -> GEMM -> ReduceScatter(output) +strict CP Attention: AllGather(Q/K/V/positions) -> Attention -> Scatter(output/LSE) +``` + +This is neither a fused GEMM+collective kernel nor two-stream overlap. vLLM's +generic AsyncTP GEMM/communication fusion is currently a CUDA path; ROCm AITER +has narrower fusions such as all-reduce plus RMSNorm, which do not replace this +contract. + +Future overlap must preserve collective issue order, the local reduction tree, +and stage boundaries. It also needs repeat-bitwise tests before being marked +strict. A useful first candidate is backward work that is independent of a +pending reduction; the forward SP AllGather and final row-parallel reduction +are data dependencies and cannot simply be overlapped with their adjacent +GEMMs. + +The “fill rank slots as messages arrive and merge ready contiguous blocks” +scheme needs a lower-level P2P or HIP/XGMI transport. PyTorch/RCCL +`all_gather_into_tensor` exposes completion of the whole collective, not +per-rank arrival events. Such a pipeline may merge only canonical sibling +subtrees when both are ready; merging arbitrary contiguous arrivals would +change floating-point parenthesization. It is therefore a follow-up transport, +not an optimization silently hidden inside this baseline. + +## Performance acceptance + +The transport-only baseline favors correctness and portability. AllGather +writes directly to its final output; reductions reuse one lazily grown +`world_size * input_bytes` byte workspace until `close()`. It still moves more +data than a native RCCL AllReduce. A ROCm GPU PR should therefore report, for +world sizes 2/4/8 and representative FFN tensors: + +- latency and effective bandwidth for all three collectives; +- peak temporary memory; +- comparison with RCCL and vLLM's available ROCm communicator; +- repeat-bitwise and cross-TP results; +- end-to-end TP/CP/SP FFN timing, not only isolated transport timing. + +A later fixed-tree HIP/XGMI implementation may replace the transport behind the +same factory after it satisfies those checks. + +Run the included native-RCCL comparison on a single node, for example: + +```bash +torchrun --standalone --nproc-per-node=8 \ + benchmarks/benchmark_rocm_collectives.py \ + --size-bytes 4096 65536 1048576 16777216 \ + --output benchmarks/results/rocm_collectives_mi300x.json +``` + +The benchmark records slowest-rank latency, temporary allocation, repeat +bitwise status, and the ratio to native RCCL. Native RCCL remains a performance +reference only, not the strict arithmetic reference. diff --git a/rl_engine/distributed/__init__.py b/rl_engine/distributed/__init__.py index 37698f1a..f83f2520 100644 --- a/rl_engine/distributed/__init__.py +++ b/rl_engine/distributed/__init__.py @@ -2,5 +2,15 @@ # Copyright (c) 2026 RL-Kernel Contributors from rl_engine.distributed.collectives import DeterministicCollective +from rl_engine.distributed.transport_collectives import ( + RCCLDeterministicCollective, + TorchDistributedDeterministicCollective, + create_deterministic_collective, +) -__all__ = ["DeterministicCollective"] +__all__ = [ + "DeterministicCollective", + "RCCLDeterministicCollective", + "TorchDistributedDeterministicCollective", + "create_deterministic_collective", +] diff --git a/rl_engine/distributed/transport_collectives.py b/rl_engine/distributed/transport_collectives.py new file mode 100644 index 00000000..fea577bc --- /dev/null +++ b/rl_engine/distributed/transport_collectives.py @@ -0,0 +1,419 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2026 RL-Kernel Contributors +"""Deterministic reductions built on rank-ordered tensor transport. + +The transport in this module never performs a floating-point reduction. It +only gathers every rank's input, after which each rank evaluates the same +balanced reduction tree locally. In a ROCm build, PyTorch's ``nccl`` backend +is RCCL and therefore ``all_gather_into_tensor`` provides the transport. +""" + +from __future__ import annotations + +import threading +from types import TracebackType +from typing import Any + +import torch +import torch.distributed as dist + +_SUPPORTED_WORLD_SIZES = (1, 2, 4, 8) +_DEFAULT_MAX_SIZE_BYTES = 64 * 1024 * 1024 +_REDUCTION_DTYPES = (torch.float32, torch.float16, torch.bfloat16) + + +class TorchDistributedDeterministicCollective: + """Correctness-first collectives using AllGather as transport only. + + Rank inputs are gathered without arithmetic and reduced locally as the + balanced tree ``((rank0 + rank1) + (rank2 + rank3)) + ...``. Consequently, + all ranks execute the exact same floating-point expression. TP sizes 1, + 2, 4, and 8 are nested prefixes of that expression and match the existing + CUDA IPC collective's ordering. + + The generic class also supports a CPU/Gloo process group, which is useful + as an executable reference. Production ROCm callers should use + :class:`RCCLDeterministicCollective` or + :func:`create_deterministic_collective` so backend validation fails closed. + All ranks must call methods in the same order with matching input shapes + and dtypes, and construct the instance with the same ``max_size_bytes``. + """ + + backend_id = "torch_distributed_balanced_tree" + transport_only = True + reduction_order = "balanced_rank_tree" + supports_async_overlap = False + supports_compute_communication_fusion = False + + def __init__( + self, + group: dist.ProcessGroup | None = None, + device: torch.device | str | int | None = None, + *, + max_size_bytes: int = _DEFAULT_MAX_SIZE_BYTES, + ) -> None: + if not dist.is_available() or not dist.is_initialized(): + raise RuntimeError("torch.distributed must be initialized before collectives") + if max_size_bytes <= 0: + raise ValueError("max_size_bytes must be positive") + + self.group = group if group is not None else dist.group.WORLD + self.rank = int(dist.get_rank(group=self.group)) + self.world_size = int(dist.get_world_size(group=self.group)) + if self.world_size not in _SUPPORTED_WORLD_SIZES: + raise ValueError( + "deterministic collectives require world_size in " + f"{_SUPPORTED_WORLD_SIZES}, got {self.world_size}" + ) + + self.device = self._normalize_device(device) + self.max_size_bytes = int(max_size_bytes) + self._backend = str(dist.get_backend(self.group)).lower() + self._lock = threading.Lock() + self._closed = False + # One dtype-agnostic byte workspace is grown on demand and reused by + # reduction collectives. AllGather writes directly into its output. + self._workspace: torch.Tensor | None = None + self._validate_matching_capacity() + + @staticmethod + def _normalize_device( + device: torch.device | str | int | None, + ) -> torch.device: + if device is None: + if torch.cuda.is_available(): + normalized = torch.device("cuda", torch.cuda.current_device()) + else: + normalized = torch.device("cpu") + elif isinstance(device, int): + normalized = torch.device("cuda", device) + else: + normalized = torch.device(device) + + if normalized.type == "cuda": + if not torch.cuda.is_available(): + raise RuntimeError("a CUDA/ROCm device was requested but none is available") + current_device = torch.cuda.current_device() + if normalized.index is None: + normalized = torch.device("cuda", current_device) + if normalized.index != current_device: + raise ValueError( + "the collective device must be the current CUDA/ROCm device; call " + f"torch.cuda.set_device({normalized.index}) first" + ) + return normalized + + @property + def closed(self) -> bool: + """Whether this instance rejects further collective calls.""" + + return self._closed + + @property + def workspace_size_bytes(self) -> int: + """Currently retained reduction workspace size in bytes.""" + + workspace = self._workspace + return 0 if workspace is None else int(workspace.numel()) + + def all_reduce( + self, + input: torch.Tensor, + *, + out: torch.Tensor | None = None, + ) -> torch.Tensor: + """Return the fixed balanced-tree sum on every rank.""" + + self._check_open() + self._validate_reduction_input(input) + if out is None: + out = torch.empty_like(input) + self._validate_output(out, input, tuple(input.shape)) + + with self._lock: + self._check_open() + self._validate_matching_signature("all_reduce", input) + rank_inputs = self._all_gather_transport(input) + reduced = self._balanced_tree_sum(rank_inputs) + out.copy_(reduced) + return out + + def all_gather( + self, + input: torch.Tensor, + *, + out: torch.Tensor | None = None, + ) -> torch.Tensor: + """Gather rank-ordered input bit patterns along dimension 0.""" + + self._check_open() + self._validate_gather_input(input) + output_shape = (input.size(0) * self.world_size, *input.shape[1:]) + if out is None: + out = torch.empty(output_shape, dtype=input.dtype, device=input.device) + self._validate_output(out, input, output_shape) + + with self._lock: + self._check_open() + self._validate_matching_signature("all_gather", input) + self._all_gather_transport(input, gathered_flat=out.view(-1)) + return out + + def reduce_scatter( + self, + input: torch.Tensor, + *, + out: torch.Tensor | None = None, + ) -> torch.Tensor: + """Fixed-tree sum followed by rank-ordered dimension-0 slicing.""" + + self._check_open() + self._validate_reduction_input(input) + if input.dim() == 0: + raise ValueError("reduce_scatter input must have at least one dimension") + if input.size(0) % self.world_size != 0: + raise ValueError( + "reduce_scatter input.size(0) must be divisible by " + f"world_size={self.world_size}; got {input.size(0)}" + ) + rows_per_rank = input.size(0) // self.world_size + output_shape = (rows_per_rank, *input.shape[1:]) + if out is None: + out = torch.empty(output_shape, dtype=input.dtype, device=input.device) + self._validate_output(out, input, output_shape) + + with self._lock: + self._check_open() + self._validate_matching_signature("reduce_scatter", input) + rank_inputs = self._all_gather_transport(input) + reduced = self._balanced_tree_sum(rank_inputs) + begin = self.rank * rows_per_rank + out.copy_(reduced.narrow(0, begin, rows_per_rank)) + return out + + def close(self) -> None: + """Close the instance. + + Closing releases the lazily allocated reduction workspace and marks + the lifecycle boundary. Collective calls are blocking at this API. + """ + + with self._lock: + self._workspace = None + self._closed = True + + def __enter__(self) -> TorchDistributedDeterministicCollective: + self._check_open() + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + self.close() + + def __del__(self) -> None: + try: + self.close() + except Exception: + pass + + def _check_open(self) -> None: + if getattr(self, "_closed", True): + raise RuntimeError("deterministic collective is closed") + + def _validate_tensor(self, input: torch.Tensor) -> None: + if not isinstance(input, torch.Tensor): + raise TypeError(f"input must be a torch.Tensor, got {type(input)!r}") + if input.device != self.device: + raise ValueError(f"input must be on {self.device}, got {input.device}") + if not input.is_contiguous(): + raise ValueError("input must be contiguous") + input_bytes = input.numel() * input.element_size() + if input_bytes > self.max_size_bytes: + raise ValueError( + f"input requires {input_bytes} bytes but max_size_bytes={self.max_size_bytes}" + ) + + def _validate_reduction_input(self, input: torch.Tensor) -> None: + self._validate_tensor(input) + if input.dtype not in _REDUCTION_DTYPES: + raise TypeError( + "deterministic reductions support float32, float16, and bfloat16; " + f"got {input.dtype}" + ) + + def _validate_gather_input(self, input: torch.Tensor) -> None: + self._validate_tensor(input) + if input.dim() == 0: + raise ValueError("all_gather input must have at least one dimension") + + @staticmethod + def _validate_output( + output: torch.Tensor, + input: torch.Tensor, + output_shape: tuple[int, ...], + ) -> None: + if not isinstance(output, torch.Tensor): + raise TypeError(f"out must be a torch.Tensor, got {type(output)!r}") + if output.device != input.device: + raise ValueError("out must be on the same device as input") + if output.dtype != input.dtype: + raise TypeError("out must have the same dtype as input") + if tuple(output.shape) != output_shape: + raise ValueError(f"out must have shape {output_shape}, got {tuple(output.shape)}") + if not output.is_contiguous(): + raise ValueError("out must be contiguous") + + def _validate_matching_signature(self, op_name: str, input: torch.Tensor) -> None: + if self.world_size == 1: + return + signature = (op_name, tuple(input.shape), str(input.dtype), input.numel()) + signatures: list[tuple[Any, ...] | None] = [None] * self.world_size + dist.all_gather_object(signatures, signature, group=self.group) + if any(peer_signature != signature for peer_signature in signatures): + raise ValueError( + f"all ranks must call {op_name} with matching shapes and dtypes; got {signatures}" + ) + + def _validate_matching_capacity(self) -> None: + if self.world_size == 1: + return + capacities: list[int | None] = [None] * self.world_size + dist.all_gather_object(capacities, self.max_size_bytes, group=self.group) + if any(peer_capacity != self.max_size_bytes for peer_capacity in capacities): + raise ValueError(f"all ranks must use the same max_size_bytes; got {capacities}") + + def _all_gather_transport( + self, + input: torch.Tensor, + *, + gathered_flat: torch.Tensor | None = None, + ) -> torch.Tensor: + # Flattening makes the output contract independent of whether a given + # ProcessGroup implements the concatenation or stacking form of AG. + if self.world_size == 1: + if gathered_flat is None: + gathered_flat = input.clone().view(-1) + else: + gathered_flat.copy_(input.view(-1)) + return gathered_flat.reshape((1, *input.shape)) + input_flat = input.view(-1) + required_elements = self.world_size * input_flat.numel() + if gathered_flat is None: + gathered_flat = self._workspace_for(input, required_elements) + elif ( + gathered_flat.numel() != required_elements + or gathered_flat.dtype != input.dtype + or gathered_flat.device != input.device + or not gathered_flat.is_contiguous() + ): + raise ValueError("gathered transport output has an invalid layout") + if "nccl" in self._backend: + # PyTorch exposes RCCL through the NCCL ProcessGroup API. Keep + # this as a tensor-only transport; reduction happens below. + dist.all_gather_into_tensor(gathered_flat, input_flat, group=self.group) + else: + # Some reference backends (notably Gloo versions without + # all_gather_into_tensor) only implement the list API. + gathered_chunks = list( + gathered_flat.reshape(self.world_size, input_flat.numel()).unbind(0) + ) + dist.all_gather(gathered_chunks, input_flat, group=self.group) + return gathered_flat.reshape((self.world_size, *input.shape)) + + def _workspace_for(self, input: torch.Tensor, required_elements: int) -> torch.Tensor: + required_bytes = required_elements * input.element_size() + workspace = self._workspace + if workspace is None or workspace.numel() < required_bytes: + workspace = torch.empty(required_bytes, dtype=torch.uint8, device=self.device) + self._workspace = workspace + # Tensor.view(dtype) reinterprets the aligned byte allocation without + # an allocation or copy. Restrict the view to the current operation. + return workspace[:required_bytes].view(input.dtype) + + @staticmethod + def _balanced_tree_sum(rank_inputs: torch.Tensor) -> torch.Tensor: + level = list(rank_inputs.unbind(0)) + if len(level) not in _SUPPORTED_WORLD_SIZES: + raise ValueError( + "balanced reduction requires rank inputs for world_size in " + f"{_SUPPORTED_WORLD_SIZES}, got {len(level)}" + ) + while len(level) > 1: + level = [torch.add(level[index], level[index + 1]) for index in range(0, len(level), 2)] + return level[0] + + +class RCCLDeterministicCollective(TorchDistributedDeterministicCollective): + """ROCm collective using RCCL AllGather strictly as tensor transport.""" + + backend_id = "rccl_all_gather_balanced_tree" + + def __init__( + self, + group: dist.ProcessGroup | None = None, + device: torch.device | str | int | None = None, + *, + max_size_bytes: int = _DEFAULT_MAX_SIZE_BYTES, + ) -> None: + if getattr(torch.version, "hip", None) is None: + raise RuntimeError("RCCL deterministic collectives require a ROCm PyTorch build") + if not torch.cuda.is_available(): + raise RuntimeError("RCCL deterministic collectives require an available ROCm device") + if device is not None and not isinstance(device, int): + requested_device = torch.device(device) + if requested_device.type != "cuda": + raise ValueError( + f"RCCL deterministic collectives require a ROCm device, got {device!r}" + ) + super().__init__(group=group, device=device, max_size_bytes=max_size_bytes) + if self.device.type != "cuda": + raise ValueError( + f"RCCL deterministic collectives require a ROCm device, got {device!r}" + ) + if "nccl" not in self._backend: + raise RuntimeError( + "RCCL deterministic collectives require PyTorch's NCCL process-group API" + ) + + +def create_deterministic_collective( + group: dist.ProcessGroup | None = None, + device: torch.device | str | int | None = None, + *, + max_size_bytes: int = _DEFAULT_MAX_SIZE_BYTES, +) -> Any: + """Create the platform-appropriate deterministic collective. + + CUDA keeps using the existing IPC implementation. ROCm uses RCCL only to + gather rank inputs, followed by the shared local balanced-tree reduction. + The returned object has independent ownership; callers may cache it by + process-group/rank/device and must close an entry before replacing it. + """ + + if getattr(torch.version, "hip", None) is not None: + return RCCLDeterministicCollective( + group=group, + device=device, + max_size_bytes=max_size_bytes, + ) + + # Import lazily to keep the existing CUDA implementation and its extension + # checks independent of the generic transport reference above. + from rl_engine.distributed.collectives import DeterministicCollective + + return DeterministicCollective( + group=group, + device=device, + max_size_bytes=max_size_bytes, + ) + + +__all__ = [ + "RCCLDeterministicCollective", + "TorchDistributedDeterministicCollective", + "create_deterministic_collective", +] diff --git a/rl_engine/kernels/ops/cuda/attention/__init__.py b/rl_engine/kernels/ops/cuda/attention/__init__.py index 91fa6f99..d5e45b96 100644 --- a/rl_engine/kernels/ops/cuda/attention/__init__.py +++ b/rl_engine/kernels/ops/cuda/attention/__init__.py @@ -27,6 +27,7 @@ CPCommunicationStatus, CUDAAGRSAttentionCPCommunication, P2PNCCLAttentionCPCommunication, + RCCLAGRSAttentionCPCommunication, sort_attention_cp_partial_states, ) except ModuleNotFoundError as exc: @@ -46,6 +47,7 @@ "CPCommunicationStatus", "CUDAAGRSAttentionCPCommunication", "P2PNCCLAttentionCPCommunication", + "RCCLAGRSAttentionCPCommunication", "sort_attention_cp_partial_states", ] diff --git a/rl_engine/kernels/ops/cuda/attention/cp_comm.py b/rl_engine/kernels/ops/cuda/attention/cp_comm.py index d1470bfd..4f945f14 100644 --- a/rl_engine/kernels/ops/cuda/attention/cp_comm.py +++ b/rl_engine/kernels/ops/cuda/attention/cp_comm.py @@ -40,7 +40,12 @@ import torch -CPCommunicationBackend = Literal["cuda_ag_rs", "p2p_nccl_reference", "local_debug"] +CPCommunicationBackend = Literal[ + "cuda_ag_rs", + "rccl_ag_rs", + "p2p_nccl_reference", + "local_debug", +] CPCommunicationStatus = Literal["interface_only", "implemented"] @@ -198,12 +203,17 @@ class AttentionCPCommunicationPlan: def validate(self) -> None: self.parallel.validate() - if self.backend not in {"cuda_ag_rs", "p2p_nccl_reference", "local_debug"}: + if self.backend not in { + "cuda_ag_rs", + "rccl_ag_rs", + "p2p_nccl_reference", + "local_debug", + }: raise ValueError(f"unsupported CP communication backend: {self.backend}") if self.status not in {"interface_only", "implemented"}: raise ValueError(f"unsupported CP communication status: {self.status}") if self.pattern != "ag_rs": - raise ValueError("PR7 CP communication must use the custom CUDA AG/RS interface") + raise ValueError("PR7 CP communication must use the self-owned AG/RS interface") if self.compute_communication != "decoupled": raise ValueError("PR7 CP communication must keep compute and communication decoupled") if self.merge_order != "global_block_index": @@ -242,6 +252,15 @@ def provenance(self) -> dict[str, object]: "cp_comm_strict_kv_communication": "all_gather", "cp_comm_strict_position_communication": "all_gather", "cp_comm_strict_backward": "rs_out_backward_ag_then_ag_qkv_backward_rs", + "cp_comm_runtime": ( + "rccl" + if self.backend == "rccl_ag_rs" + else "nccl" if self.backend in {"cuda_ag_rs", "p2p_nccl_reference"} else "local" + ), + # The ROCm path intentionally transports tensors and performs the + # arithmetic in the deterministic core; only the CUDA IPC path + # owns a numeric collective reduction kernel. + "cp_comm_attention_numeric_reduction": self.backend == "cuda_ag_rs", "cp_comm_expected_kv_token_range": ( None if self.expected_kv_token_range is None else list(self.expected_kv_token_range) ), @@ -338,7 +357,10 @@ def forward( packed = full.movedim(ctx.sequence_dim, 0).contiguous() if ctx.rank != ctx.root: packed = torch.zeros_like(packed) - local = collective.reduce_scatter(packed) + # The ROCm transport adapter exposes an explicit root-owned scatter; + # CUDA's IPC collective keeps the historical reduce_scatter entrypoint. + scatter = getattr(collective, "scatter", None) + local = scatter(packed) if callable(scatter) else collective.reduce_scatter(packed) return local.movedim(0, ctx.sequence_dim).contiguous() @staticmethod @@ -557,6 +579,158 @@ def _validate_cuda_plan(self, plan: AttentionCPCommunicationPlan) -> None: raise AttentionCPCommunicationUnavailable("self-owned CUDA AG/RS requires CUDA") +class _RCCLRankOrderedTransport: + """RCCL tensor transport for the ROCm AG/RS attention contract. + + RCCL is deliberately used only for AllGather/Scatter transport. The + reduction half first gathers every source tensor and then evaluates the + same fixed balanced rank tree on every device, which avoids relying on RCCL's + implementation-defined floating-point reduction order. + """ + + def __init__(self, *, process_group: Any = None, root: int = 0) -> None: + import torch.distributed as dist + + if not dist.is_available() or not dist.is_initialized(): + raise AttentionCPCommunicationUnavailable( + "self-owned RCCL AG/RS requires initialized torch.distributed" + ) + backend = str(dist.get_backend(process_group)).lower() + if "nccl" not in backend or torch.version.hip is None: + raise AttentionCPCommunicationUnavailable( + "self-owned RCCL AG/RS requires the PyTorch NCCL API on ROCm" + ) + self.group = process_group + self.rank = int(dist.get_rank(process_group)) + self.world_size = int(dist.get_world_size(process_group)) + self.root = int(root) + if self.world_size not in (1, 2, 4, 8): + raise AttentionCPCommunicationUnavailable( + "self-owned RCCL AG/RS supports CP world sizes 1, 2, 4, and 8" + ) + if self.root < 0 or self.root >= self.world_size: + raise AttentionCPCommunicationUnavailable("RCCL scatter root is outside the group") + + def all_gather(self, local: torch.Tensor) -> torch.Tensor: + import torch.distributed as dist + + if local.ndim == 0 or not local.is_cuda or not local.is_contiguous(): + raise AttentionCPCommunicationUnavailable( + "RCCL AllGather requires a contiguous ROCm tensor with a leading dimension" + ) + shape = (self.world_size * local.size(0), *local.shape[1:]) + gathered = torch.empty(shape, dtype=local.dtype, device=local.device) + if self.world_size == 1: + gathered.copy_(local) + else: + dist.all_gather_into_tensor(gathered, local, group=self.group) + return gathered + + def scatter(self, full: torch.Tensor) -> torch.Tensor: + import torch.distributed as dist + + if full.ndim == 0 or not full.is_cuda or not full.is_contiguous(): + raise AttentionCPCommunicationUnavailable( + "RCCL Scatter requires a contiguous ROCm tensor with a leading dimension" + ) + if full.size(0) % self.world_size: + raise AttentionCPCommunicationUnavailable( + "RCCL Scatter leading dimension must divide the CP world size" + ) + chunks = tuple(chunk.contiguous() for chunk in full.chunk(self.world_size, dim=0)) + local = torch.empty_like(chunks[self.rank]) + if self.world_size == 1: + local.copy_(chunks[0]) + return local + + # ``src`` is a global rank even when a subgroup is supplied. + global_root = self.root + if self.group is not None: + get_global_rank = getattr(dist, "get_global_rank", None) + if callable(get_global_rank): + global_root = int(get_global_rank(self.group, self.root)) + else: + get_group_ranks = getattr(dist, "get_process_group_ranks", None) + if not callable(get_group_ranks): + raise AttentionCPCommunicationUnavailable( + "PyTorch cannot map the RCCL subgroup root to a global rank" + ) + global_root = int(get_group_ranks(self.group)[self.root]) + dist.scatter( + local, + scatter_list=list(chunks) if self.rank == self.root else None, + src=global_root, + group=self.group, + ) + return local + + def reduce_scatter(self, full: torch.Tensor) -> torch.Tensor: + """Deterministic source-rank sum followed by local sequence slicing.""" + + if full.ndim == 0 or not full.is_cuda or not full.is_contiguous(): + raise AttentionCPCommunicationUnavailable( + "RCCL ReduceScatter requires a contiguous ROCm tensor with a leading dimension" + ) + if full.size(0) % self.world_size: + raise AttentionCPCommunicationUnavailable( + "RCCL ReduceScatter leading dimension must divide the CP world size" + ) + gathered = self.all_gather(full) + rows_per_rank = full.size(0) // self.world_size + begin = self.rank * rows_per_rank + # Gather layout is [source_rank, full_sequence, ...]. Evaluate the + # same balanced rank tree as the general deterministic collective. + level = [] + for source in range(self.world_size): + source_begin = source * full.size(0) + begin + level.append(gathered[source_begin : source_begin + rows_per_rank]) + while len(level) > 1: + level = [torch.add(level[index], level[index + 1]) for index in range(0, len(level), 2)] + return level[0] + + +class RCCLAGRSAttentionCPCommunication(CUDAAGRSAttentionCPCommunication): + """ROCm AG/RS adapter using RCCL only as rank-ordered tensor transport.""" + + backend_id = "rccl_ag_rs" + supports_autograd = True + transport_only = True + supports_async_overlap = False + supports_compute_communication_fusion = False + + def _get_collective(self, plan: AttentionCPCommunicationPlan): + if self._collective is None: + self._collective = _RCCLRankOrderedTransport( + process_group=self._process_group, + root=plan.merge_root_cp_rank, + ) + if self._collective.world_size != plan.parallel.cp_world_size: + raise AttentionCPCommunicationUnavailable( + "self-owned RCCL world size does not match the CP plan" + ) + return self._collective + + def _dist(self): + import torch.distributed as dist + + if not dist.is_available() or not dist.is_initialized(): + raise AttentionCPCommunicationUnavailable( + "self-owned RCCL AG/RS requires initialized torch.distributed" + ) + return dist + + def _validate_cuda_plan(self, plan: AttentionCPCommunicationPlan) -> None: + plan.validate() + if plan.backend != "rccl_ag_rs" or plan.status != "implemented": + raise AttentionCPCommunicationUnavailable( + "self-owned RCCL AG/RS requires an implemented rccl_ag_rs plan" + ) + if torch.version.hip is None or not torch.cuda.is_available(): + raise AttentionCPCommunicationUnavailable( + "self-owned RCCL AG/RS requires an available ROCm device" + ) + + class P2PNCCLAttentionCPCommunication: """Correctness-first P2P NCCL implementation of the CP protocol. @@ -1288,6 +1462,7 @@ def _rank_in_world(rank: int, world_size: int, name: str) -> None: "CPCommunicationBackend", "CPCommunicationStatus", "CUDAAGRSAttentionCPCommunication", + "RCCLAGRSAttentionCPCommunication", "P2PNCCLAttentionCPCommunication", "sort_attention_cp_partial_states", ] diff --git a/rl_engine/kernels/ops/pytorch/ffn/ffn.py b/rl_engine/kernels/ops/pytorch/ffn/ffn.py index bc0ddcef..fe17486b 100644 --- a/rl_engine/kernels/ops/pytorch/ffn/ffn.py +++ b/rl_engine/kernels/ops/pytorch/ffn/ffn.py @@ -1,6 +1,6 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright (c) 2026 RL-Kernel Contributors -"""Bias-free gated FFN assembled from deterministic CUDA kernels.""" +"""Bias-free gated FFN assembled from deterministic GPU kernels.""" from __future__ import annotations @@ -35,9 +35,9 @@ def _require_ffn_kernels(*, disable_split_k: bool) -> None: if not _EXT_AVAILABLE or _C is None or missing: suffix = f" Missing symbols: {', '.join(missing)}." if missing else "" needed = ( - "compiled deterministic GEMM and SwiGLU CUDA kernels" + "compiled deterministic GEMM and SwiGLU GPU kernels" if disable_split_k - else "compiled SwiGLU CUDA kernels" + else "compiled SwiGLU GPU kernels" ) raise RuntimeError(f"qwen3_ffn requires the {needed}.{suffix}") @@ -135,7 +135,8 @@ def _validate_ffn_inputs( if tensor.dtype != torch.bfloat16: raise TypeError(f"{name} must have dtype bfloat16, got {tensor.dtype}.") if not tensor.is_cuda: - raise RuntimeError(f"{name} must be on a CUDA device, got '{tensor.device}'.") + # PyTorch exposes AMD GPU tensors through the torch.cuda API too. + raise RuntimeError(f"{name} must be on a CUDA/ROCm GPU device, got '{tensor.device}'.") if tensor.device != rmsnorm_output.device: raise RuntimeError( f"all FFN inputs must be on {rmsnorm_output.device}, " @@ -143,14 +144,35 @@ def _validate_ffn_inputs( ) +def _create_collective(*, group: Any, max_size_bytes: int): + """Create the platform-specific collective lazily. + + NVIDIA keeps the existing CUDA IPC implementation. ROCm selects the + RCCL transport-only implementation, which performs the floating-point + reduction in the shared deterministic local tree. Keeping this boundary + small also makes the backend choice explicit and easy to inject in tests. + """ + + try: + from rl_engine.distributed import create_deterministic_collective + except ImportError as exc: + raise RuntimeError( + "parallel qwen3_ffn requires the platform deterministic collective " + "factory; single-device qwen3_ffn remains available" + ) from exc + + return create_deterministic_collective( + group=group, + max_size_bytes=max_size_bytes, + ) + + def _collective_for_group(group: Any, *, min_size_bytes: int): if group is None: return None import torch.distributed as dist - from rl_engine.distributed import DeterministicCollective - rank = dist.get_rank(group=group) world_size = dist.get_world_size(group=group) device_index = torch.cuda.current_device() @@ -161,7 +183,7 @@ def _collective_for_group(group: Any, *, min_size_bytes: int): if cached is not None: cached.close() - collective = DeterministicCollective( + collective = _create_collective( group=group, max_size_bytes=max(_COLLECTIVE_MIN_CAPACITY_BYTES, min_size_bytes), ) @@ -393,7 +415,8 @@ def qwen3_ffn( ``[H, I_local]``. tp_group: Optional tensor-parallel process group. Gate and Up are column-parallel; Down is row-parallel. Reductions use the - deterministic fixed-tree collectives rather than NCCL. + platform deterministic fixed-tree collectives. On ROCm, RCCL only + transports rank inputs and the reduction tree executes locally. cp_group: Optional context-parallel process group. Each rank owns different token rows and the same local weight shards. Weight gradients AllGather tokens along CP and run the full-token diff --git a/setup.py b/setup.py index 79f882d9..3f0b3d5a 100644 --- a/setup.py +++ b/setup.py @@ -139,9 +139,11 @@ def get_extensions(): "csrc/cuda/rmsnorm.cu", "csrc/cuda/activation.cu", "csrc/cuda/attention/deterministic_attention.cu", - "csrc/cuda/distributed/deterministic_collective.cu", ] if not is_rocm: + # The CUDA collective owns CUDA IPC handles and driver API calls; + # ROCm uses the Python RCCL transport implementation instead. + cuda_sources.append("csrc/cuda/distributed/deterministic_collective.cu") # This source contains NVIDIA PTX (cp.async, ldmatrix, and mma.sync). # The ROCm dispatcher falls back to PyTorch SDPA for this operator. cuda_sources.append("csrc/cuda/attention/prefix_shared_attention.cu") @@ -211,9 +213,10 @@ def get_extensions(): nvcc_flags.append("-allow-unsupported-compiler") nvcc_flags.append("-D_ALLOW_COMPILER_AND_STL_VERSION_MISMATCH") - cxx_flags = ["-O3", "-std=c++17", "-DKERNEL_ALIGN_WITH_CUDA"] + platform_define = "-DKERNEL_ALIGN_WITH_ROCM" if is_rocm else "-DKERNEL_ALIGN_WITH_CUDA" + cxx_flags = ["-O3", "-std=c++17", platform_define] extra_link_args = list(torch_rpath) - if os.name != "nt": + if os.name != "nt" and not is_rocm: # CUDA IPC metadata queries use the driver API (cuPointerGetAttribute). extra_link_args.append("-lcuda") diff --git a/tests/distributed/test_rocm_attention_transport.py b/tests/distributed/test_rocm_attention_transport.py new file mode 100644 index 00000000..327bd1f1 --- /dev/null +++ b/tests/distributed/test_rocm_attention_transport.py @@ -0,0 +1,95 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2026 RL-Kernel Contributors + +from __future__ import annotations + +import pytest +import torch + +from rl_engine.kernels.ops.cuda.attention.cp_comm import ( + AttentionCPBlockMetadata, + AttentionCPCommunicationPlan, + AttentionCPCommunicationUnavailable, + AttentionParallelSpec, + RCCLAGRSAttentionCPCommunication, +) + + +def _plan(*, backend: str = "rccl_ag_rs") -> AttentionCPCommunicationPlan: + return AttentionCPCommunicationPlan( + parallel=AttentionParallelSpec(tp_world_size=2, cp_world_size=2), + backend=backend, # type: ignore[arg-type] + status="implemented", + expected_blocks=( + AttentionCPBlockMetadata(0, 0, 2, 0, 0), + AttentionCPBlockMetadata(1, 2, 4, 1, 0), + ), + expected_kv_token_range=(0, 4), + query_token_ranges=((0, 1), (1, 2)), + ) + + +class _FakeRCCLTransport: + world_size = 2 + + def __init__(self) -> None: + self.gather_calls = 0 + self.scatter_calls = 0 + + def all_gather(self, tensor: torch.Tensor) -> torch.Tensor: + self.gather_calls += 1 + return torch.cat((tensor, tensor), dim=0) + + def scatter(self, tensor: torch.Tensor) -> torch.Tensor: + self.scatter_calls += 1 + return tensor.chunk(self.world_size, dim=0)[0].contiguous() + + +def test_rccl_plan_reports_transport_only_runtime() -> None: + provenance = _plan().provenance() + + assert provenance["cp_comm_runtime"] == "rccl" + assert provenance["cp_comm_attention_numeric_reduction"] is False + + +def test_rccl_adapter_uses_root_scatter_and_reports_no_fusion( + monkeypatch: pytest.MonkeyPatch, +) -> None: + transport = _FakeRCCLTransport() + communication = RCCLAGRSAttentionCPCommunication(collective=transport) + monkeypatch.setattr(torch.version, "hip", "test", raising=False) + monkeypatch.setattr(torch.cuda, "is_available", lambda: True) + plan = _plan() + + local_q = torch.zeros(1, 2, 1, 4, dtype=torch.bfloat16) + assert communication.all_gather_query(local_q, plan).shape == (1, 2, 2, 4) + + full_out = torch.zeros(1, 2, 2, 4, dtype=torch.bfloat16) + full_lse = torch.zeros(1, 2, 2, dtype=torch.float32) + shard = communication.reduce_scatter_strict_result(full_out, full_lse, plan) + + assert shard.out.shape == (1, 2, 1, 4) + assert shard.lse.shape == (1, 2, 1) + assert transport.gather_calls == 1 + assert transport.scatter_calls == 2 + assert communication.transport_only is True + assert communication.supports_async_overlap is False + assert communication.supports_compute_communication_fusion is False + + +def test_rccl_adapter_fails_closed_outside_rocm(monkeypatch: pytest.MonkeyPatch) -> None: + communication = RCCLAGRSAttentionCPCommunication(collective=_FakeRCCLTransport()) + monkeypatch.setattr(torch.version, "hip", None, raising=False) + monkeypatch.setattr(torch.cuda, "is_available", lambda: True) + + with pytest.raises(AttentionCPCommunicationUnavailable, match="ROCm device"): + communication.all_gather_query(torch.zeros(1, 2, 1, 4), _plan()) + + +def test_rccl_adapter_rejects_cuda_plan(monkeypatch: pytest.MonkeyPatch) -> None: + communication = RCCLAGRSAttentionCPCommunication(collective=_FakeRCCLTransport()) + monkeypatch.setattr(torch.version, "hip", "test", raising=False) + monkeypatch.setattr(torch.cuda, "is_available", lambda: True) + + with pytest.raises(AttentionCPCommunicationUnavailable, match="rccl_ag_rs plan"): + communication.all_gather_query(torch.zeros(1, 2, 1, 4), _plan(backend="cuda_ag_rs")) diff --git a/tests/distributed/test_transport_deterministic_collective.py b/tests/distributed/test_transport_deterministic_collective.py new file mode 100644 index 00000000..00c987e1 --- /dev/null +++ b/tests/distributed/test_transport_deterministic_collective.py @@ -0,0 +1,375 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2026 RL-Kernel Contributors + +from __future__ import annotations + +from typing import Any + +import pytest +import torch + +import rl_engine.distributed.collectives as cuda_collectives +import rl_engine.distributed.transport_collectives as transport_collectives +from rl_engine.distributed import ( + RCCLDeterministicCollective, + TorchDistributedDeterministicCollective, +) + + +class _FakeDistributed: + """Single-process model of rank-ordered AllGather transport.""" + + def __init__( + self, + peer_inputs: list[torch.Tensor], + *, + rank: int = 0, + backend: str = "gloo", + peer_signatures: list[tuple[Any, ...]] | None = None, + peer_capacities: list[int] | None = None, + ) -> None: + self.peer_inputs = peer_inputs + self.rank = rank + self.backend = backend + self.peer_signatures = peer_signatures + self.peer_capacities = peer_capacities + self.into_tensor_calls = 0 + self.list_transport_calls = 0 + self.last_transport_output: torch.Tensor | None = None + + @property + def tensor_transport_calls(self) -> int: + return self.into_tensor_calls + self.list_transport_calls + + @staticmethod + def is_available() -> bool: + return True + + @staticmethod + def is_initialized() -> bool: + return True + + def get_rank(self, *, group: Any) -> int: + return self.rank + + def get_world_size(self, *, group: Any) -> int: + return len(self.peer_inputs) + + def get_backend(self, group: Any) -> str: + return self.backend + + def all_gather_object(self, output: list[Any], value: Any, *, group: Any) -> None: + if isinstance(value, int): + values = self.peer_capacities or [value] * len(self.peer_inputs) + else: + values = self.peer_signatures or [value] * len(self.peer_inputs) + output[:] = values + + def all_gather_into_tensor( + self, + output: torch.Tensor, + input: torch.Tensor, + *, + group: Any, + ) -> None: + self.into_tensor_calls += 1 + self.last_transport_output = output + gathered = torch.cat([peer.reshape(-1) for peer in self.peer_inputs]) + output.copy_(gathered) + + def all_gather( + self, + output: list[torch.Tensor], + input: torch.Tensor, + *, + group: Any, + ) -> None: + self.list_transport_calls += 1 + self.last_transport_output = output[0]._base + for destination, peer in zip(output, self.peer_inputs, strict=True): + destination.copy_(peer.reshape(-1)) + + +def _make_collective( + monkeypatch: pytest.MonkeyPatch, + peer_inputs: list[torch.Tensor], + *, + rank: int = 0, + backend: str = "gloo", + peer_signatures: list[tuple[Any, ...]] | None = None, + peer_capacities: list[int] | None = None, + max_size_bytes: int = 1024, +) -> tuple[TorchDistributedDeterministicCollective, _FakeDistributed]: + fake_dist = _FakeDistributed( + peer_inputs, + rank=rank, + backend=backend, + peer_signatures=peer_signatures, + peer_capacities=peer_capacities, + ) + monkeypatch.setattr(transport_collectives, "dist", fake_dist) + collective = TorchDistributedDeterministicCollective( + group=object(), + device="cpu", + max_size_bytes=max_size_bytes, + ) + return collective, fake_dist + + +@pytest.mark.parametrize("world_size", [1, 2, 4, 8]) +@pytest.mark.parametrize("dtype", [torch.float32, torch.float16, torch.bfloat16]) +def test_all_reduce_supports_fixed_world_sizes_and_dtypes( + monkeypatch: pytest.MonkeyPatch, + world_size: int, + dtype: torch.dtype, +) -> None: + peers = [torch.full((2, 3), rank + 1, dtype=dtype) for rank in range(world_size)] + collective, fake_dist = _make_collective(monkeypatch, peers) + + provided = torch.empty_like(peers[0]) + returned = collective.all_reduce(peers[0], out=provided) + + assert returned is provided + assert torch.equal(provided, torch.full_like(provided, world_size * (world_size + 1) // 2)) + assert fake_dist.tensor_transport_calls == (0 if world_size == 1 else 1) + + +def test_all_reduce_uses_balanced_not_rank_ordered_left_fold( + monkeypatch: pytest.MonkeyPatch, +) -> None: + # Balanced: (1e20 + 1) + (-1e20 + 1) == 0 in FP32. + # Left fold: ((1e20 + 1) + -1e20) + 1 == 1 in FP32. + peers = [torch.tensor([value], dtype=torch.float32) for value in (1.0e20, 1.0, -1.0e20, 1.0)] + collective, _ = _make_collective(monkeypatch, peers) + + output = collective.all_reduce(peers[0]) + + assert torch.equal(output, torch.zeros_like(output)) + + +def test_all_reduce_stages_before_writing_in_place(monkeypatch: pytest.MonkeyPatch) -> None: + peers = [torch.full((2, 3), rank + 1, dtype=torch.float32) for rank in range(4)] + collective, _ = _make_collective(monkeypatch, peers) + local = peers[0].clone() + + returned = collective.all_reduce(local, out=local) + + assert returned is local + assert torch.equal(local, torch.full_like(local, 10)) + + +def test_all_gather_is_rank_ordered_and_transport_only( + monkeypatch: pytest.MonkeyPatch, +) -> None: + peers = [torch.arange(rank * 6, (rank + 1) * 6).reshape(2, 3) for rank in range(4)] + collective, fake_dist = _make_collective(monkeypatch, peers, rank=2) + + output = collective.all_gather(peers[2]) + + assert torch.equal(output, torch.cat(peers, dim=0)) + assert fake_dist.tensor_transport_calls == 1 + + +def test_nccl_backend_uses_all_gather_into_tensor_transport( + monkeypatch: pytest.MonkeyPatch, +) -> None: + peers = [torch.full((2, 3), rank) for rank in range(2)] + collective, fake_dist = _make_collective(monkeypatch, peers, backend="nccl") + + output = collective.all_gather(peers[0]) + + assert torch.equal(output, torch.cat(peers, dim=0)) + assert fake_dist.into_tensor_calls == 1 + assert fake_dist.list_transport_calls == 0 + + +def test_all_gather_writes_directly_to_provided_output( + monkeypatch: pytest.MonkeyPatch, +) -> None: + peers = [torch.full((2, 3), rank) for rank in range(2)] + collective, fake_dist = _make_collective(monkeypatch, peers, backend="nccl") + provided = torch.empty(4, 3, dtype=peers[0].dtype) + + returned = collective.all_gather(peers[0], out=provided) + + assert returned is provided + assert fake_dist.last_transport_output is not None + assert fake_dist.last_transport_output.data_ptr() == provided.data_ptr() + assert collective._workspace is None + + +def test_reduction_workspace_grows_once_and_is_reused( + monkeypatch: pytest.MonkeyPatch, +) -> None: + peers = [torch.full((2, 3), rank + 1, dtype=torch.float32) for rank in range(2)] + collective, _ = _make_collective(monkeypatch, peers, backend="nccl") + + collective.all_reduce(peers[0]) + assert collective._workspace is not None + assert collective.workspace_size_bytes == sum(peer.numel() for peer in peers) * 4 + first_pointer = collective._workspace.data_ptr() + collective.all_reduce(peers[0]) + + assert collective._workspace.data_ptr() == first_pointer + collective.close() + assert collective._workspace is None + assert collective.workspace_size_bytes == 0 + + +def test_reduce_scatter_reduces_then_selects_local_leading_shard( + monkeypatch: pytest.MonkeyPatch, +) -> None: + peers = [torch.full((8, 3), rank + 1, dtype=torch.bfloat16) for rank in range(4)] + collective, _ = _make_collective(monkeypatch, peers, rank=2) + + output = collective.reduce_scatter(peers[2]) + + expected_full = torch.full_like(peers[0], 10) + assert torch.equal(output, expected_full.chunk(4, dim=0)[2]) + + +def test_matching_signature_is_checked_before_tensor_transport( + monkeypatch: pytest.MonkeyPatch, +) -> None: + peers = [torch.ones(2, 3), torch.ones(2, 3)] + signatures = [ + ("all_reduce", (2, 3), "torch.float32", 6), + ("reduce_scatter", (2, 3), "torch.float32", 6), + ] + collective, fake_dist = _make_collective( + monkeypatch, + peers, + peer_signatures=signatures, + ) + + with pytest.raises(ValueError, match="matching shapes and dtypes"): + collective.all_reduce(peers[0]) + assert fake_dist.tensor_transport_calls == 0 + + +def test_capacity_must_match_on_every_rank(monkeypatch: pytest.MonkeyPatch) -> None: + peers = [torch.ones(2, 3), torch.ones(2, 3)] + + with pytest.raises(ValueError, match="same max_size_bytes"): + _make_collective( + monkeypatch, + peers, + max_size_bytes=1024, + peer_capacities=[1024, 2048], + ) + + +def test_validation_fails_closed_before_transport(monkeypatch: pytest.MonkeyPatch) -> None: + peers = [torch.ones(4, 2), torch.ones(4, 2)] + collective, fake_dist = _make_collective(monkeypatch, peers, max_size_bytes=31) + + with pytest.raises(TypeError, match="float32, float16, and bfloat16"): + collective.all_reduce(torch.ones(1, dtype=torch.int32)) + with pytest.raises(ValueError, match="max_size_bytes"): + collective.all_reduce(peers[0]) + with pytest.raises(ValueError, match=r"input.size\(0\).+divisible"): + collective.reduce_scatter(torch.ones(3, 2)) + with pytest.raises(ValueError, match="at least one dimension"): + collective.all_gather(torch.tensor(1.0)) + assert fake_dist.tensor_transport_calls == 0 + + +def test_lifecycle_is_idempotent_and_context_manager_closes( + monkeypatch: pytest.MonkeyPatch, +) -> None: + peers = [torch.ones(2, 3)] + collective, _ = _make_collective(monkeypatch, peers) + + with collective as entered: + assert entered is collective + assert not collective.closed + assert torch.equal(collective.all_reduce(peers[0]), peers[0]) + + assert collective.closed + collective.close() + with pytest.raises(RuntimeError, match="closed"): + collective.all_reduce(peers[0]) + + +@pytest.mark.parametrize("world_size", [3, 16]) +def test_unsupported_world_size_is_rejected( + monkeypatch: pytest.MonkeyPatch, + world_size: int, +) -> None: + fake_dist = _FakeDistributed([torch.ones(1)] * world_size) + monkeypatch.setattr(transport_collectives, "dist", fake_dist) + + with pytest.raises(ValueError, match="world_size in"): + TorchDistributedDeterministicCollective(group=object(), device="cpu") + + +def test_rccl_class_requires_rocm_build(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(transport_collectives.torch.version, "hip", None, raising=False) + + with pytest.raises(RuntimeError, match="ROCm PyTorch build"): + RCCLDeterministicCollective(group=object(), device="cuda:0") + + +def test_rccl_class_requires_nccl_process_group(monkeypatch: pytest.MonkeyPatch) -> None: + fake_dist = _FakeDistributed([torch.ones(1)], backend="gloo") + monkeypatch.setattr(transport_collectives, "dist", fake_dist) + monkeypatch.setattr(transport_collectives.torch.version, "hip", "6.3", raising=False) + monkeypatch.setattr(transport_collectives.torch.cuda, "is_available", lambda: True) + monkeypatch.setattr(transport_collectives.torch.cuda, "current_device", lambda: 0) + + with pytest.raises(RuntimeError, match="NCCL process-group API"): + RCCLDeterministicCollective(group=object(), device="cuda:0") + + +def test_rccl_class_rejects_cpu_before_process_group_exchange( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr(transport_collectives.torch.version, "hip", "6.3", raising=False) + monkeypatch.setattr(transport_collectives.torch.cuda, "is_available", lambda: True) + + with pytest.raises(ValueError, match="ROCm device"): + RCCLDeterministicCollective(group=object(), device="cpu") + + +def test_factory_dispatches_rocm_to_rccl(monkeypatch: pytest.MonkeyPatch) -> None: + sentinel = object() + calls: list[dict[str, Any]] = [] + + def fake_rccl(**kwargs: Any) -> object: + calls.append(kwargs) + return sentinel + + monkeypatch.setattr(transport_collectives.torch.version, "hip", "6.3", raising=False) + monkeypatch.setattr(transport_collectives, "RCCLDeterministicCollective", fake_rccl) + + group = object() + result = transport_collectives.create_deterministic_collective( + group=group, + device="cuda:3", + max_size_bytes=1234, + ) + + assert result is sentinel + assert calls == [{"group": group, "device": "cuda:3", "max_size_bytes": 1234}] + + +def test_factory_preserves_existing_cuda_collective(monkeypatch: pytest.MonkeyPatch) -> None: + sentinel = object() + calls: list[dict[str, Any]] = [] + + def fake_cuda(**kwargs: Any) -> object: + calls.append(kwargs) + return sentinel + + monkeypatch.setattr(transport_collectives.torch.version, "hip", None, raising=False) + monkeypatch.setattr(cuda_collectives, "DeterministicCollective", fake_cuda) + + group = object() + result = transport_collectives.create_deterministic_collective( + group=group, + device="cuda:1", + max_size_bytes=4321, + ) + + assert result is sentinel + assert calls == [{"group": group, "device": "cuda:1", "max_size_bytes": 4321}] diff --git a/tests/test_build_platform_collectives.py b/tests/test_build_platform_collectives.py new file mode 100644 index 00000000..f094a5af --- /dev/null +++ b/tests/test_build_platform_collectives.py @@ -0,0 +1,54 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2026 RL-Kernel Contributors + +from __future__ import annotations + +import runpy +from typing import Any + +import setuptools +import torch +from torch.utils import cpp_extension + + +def _load_extension_config(monkeypatch, *, hip: str | None) -> dict[str, Any]: + captured: dict[str, Any] = {} + + def fake_setup(**kwargs: Any) -> None: + captured.update(kwargs) + + def fake_extension(**kwargs: Any) -> dict[str, Any]: + return kwargs + + monkeypatch.setattr(setuptools, "setup", fake_setup) + monkeypatch.setattr(cpp_extension, "CUDAExtension", fake_extension) + monkeypatch.setattr(torch.version, "hip", hip, raising=False) + monkeypatch.delenv("KERNEL_ALIGN_FORCE_SM90", raising=False) + monkeypatch.delenv("KERNEL_ALIGN_DET_GEMM_SM90", raising=False) + if hip is None: + monkeypatch.delenv("PYTORCH_ROCM_ARCH", raising=False) + monkeypatch.setattr(torch.cuda, "is_available", lambda: True) + monkeypatch.setattr(torch.cuda, "get_device_capability", lambda: (8, 0)) + else: + monkeypatch.setenv("PYTORCH_ROCM_ARCH", "gfx942") + + runpy.run_path("setup.py", run_name=f"rl_kernel_setup_probe_{hip or 'cuda'}") + return captured["ext_modules"][0] + + +def test_rocm_build_excludes_cuda_ipc_collective_and_driver(monkeypatch) -> None: + extension = _load_extension_config(monkeypatch, hip="test") + + assert "csrc/cuda/distributed/deterministic_collective.cu" not in extension["sources"] + assert "-DKERNEL_ALIGN_WITH_ROCM" in extension["extra_compile_args"]["cxx"] + assert "-DKERNEL_ALIGN_WITH_CUDA" not in extension["extra_compile_args"]["cxx"] + assert "-lcuda" not in extension["extra_link_args"] + + +def test_cuda_build_keeps_existing_ipc_collective(monkeypatch) -> None: + extension = _load_extension_config(monkeypatch, hip=None) + + assert "csrc/cuda/distributed/deterministic_collective.cu" in extension["sources"] + assert "-DKERNEL_ALIGN_WITH_CUDA" in extension["extra_compile_args"]["cxx"] + assert "-DKERNEL_ALIGN_WITH_ROCM" not in extension["extra_compile_args"]["cxx"] + assert "-lcuda" in extension["extra_link_args"] diff --git a/tests/test_qwen_ffn.py b/tests/test_qwen_ffn.py index 515abc7a..fddfa9f4 100644 --- a/tests/test_qwen_ffn.py +++ b/tests/test_qwen_ffn.py @@ -137,6 +137,25 @@ def _close_ffn_collectives() -> None: ffn_module._COLLECTIVES.clear() +def test_ffn_collective_creation_uses_platform_factory(monkeypatch): + import rl_engine.distributed as distributed + + sentinel = object() + calls = [] + + def fake_factory(**kwargs): + calls.append(kwargs) + return sentinel + + monkeypatch.setattr(distributed, "create_deterministic_collective", fake_factory) + group = object() + + result = ffn_module._create_collective(group=group, max_size_bytes=1234) + + assert result is sentinel + assert calls == [{"group": group, "max_size_bytes": 1234}] + + def _shard_ranges( rank: int, *, diff --git a/tests/test_rocm_collective_benchmark.py b/tests/test_rocm_collective_benchmark.py new file mode 100644 index 00000000..e4eb8643 --- /dev/null +++ b/tests/test_rocm_collective_benchmark.py @@ -0,0 +1,73 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2026 RL-Kernel Contributors + +from __future__ import annotations + +import importlib.util +from pathlib import Path + +import pytest +import torch + +_BENCHMARK_PATH = Path(__file__).parents[1] / "benchmarks" / "benchmark_rocm_collectives.py" +_SPEC = importlib.util.spec_from_file_location( + "rlkernel_rocm_collective_benchmark", _BENCHMARK_PATH +) +assert _SPEC is not None and _SPEC.loader is not None +benchmark = importlib.util.module_from_spec(_SPEC) +_SPEC.loader.exec_module(benchmark) + + +def test_rocm_collective_benchmark_parser() -> None: + args = benchmark.parse_args( + [ + "--size-bytes", + "1024", + "4096", + "--dtype", + "fp32", + "--operations", + "all_reduce", + "reduce_scatter", + "--warmup", + "2", + "--iterations", + "3", + "--samples", + "4", + ] + ) + + benchmark._validate_args(args) + assert args.size_bytes == [1024, 4096] + assert args.dtype == "fp32" + assert args.operations == ["all_reduce", "reduce_scatter"] + assert (args.warmup, args.iterations, args.samples) == (2, 3, 4) + + +@pytest.mark.parametrize( + "argv", + ( + ["--size-bytes", "0"], + ["--warmup", "-1"], + ["--iterations", "0"], + ["--samples", "0"], + ), +) +def test_rocm_collective_benchmark_rejects_invalid_counts(argv: list[str]) -> None: + with pytest.raises(ValueError): + benchmark._validate_args(benchmark.parse_args(argv)) + + +def test_rocm_collective_benchmark_aligns_reduce_scatter_input() -> None: + tensor, actual_bytes = benchmark._make_inputs( + size_bytes=35, + dtype=torch.float32, + world_size=4, + rank=2, + device=torch.device("cpu"), + ) + + assert tensor.is_contiguous() + assert tensor.numel() % 4 == 0 + assert actual_bytes == tensor.numel() * tensor.element_size() From eb16e79bbfecdb975786adf95c1015f5e60f4ff7 Mon Sep 17 00:00:00 2001 From: vensen Date: Fri, 28 Aug 2026 16:48:51 +0000 Subject: [PATCH 17/35] perf(distributed): remove ROCm collective hot-path allocations --- .../distributed/transport_collectives.py | 28 +++++++++++++++---- .../kernels/ops/cuda/attention/cp_comm.py | 7 +++-- ...test_transport_deterministic_collective.py | 15 ++++++++++ 3 files changed, 42 insertions(+), 8 deletions(-) diff --git a/rl_engine/distributed/transport_collectives.py b/rl_engine/distributed/transport_collectives.py index fea577bc..ff9a4dcf 100644 --- a/rl_engine/distributed/transport_collectives.py +++ b/rl_engine/distributed/transport_collectives.py @@ -74,6 +74,12 @@ def __init__( # One dtype-agnostic byte workspace is grown on demand and reused by # reduction collectives. AllGather writes directly into its output. self._workspace: torch.Tensor | None = None + # A Python-object collective is useful for catching a mismatched new + # signature, but running one on every hot-path call dominates small + # message latency. Validate each local signature once and then rely on + # the standard collective contract that ranks call operations in the + # same order. + self._validated_signatures: set[tuple[Any, ...]] = set() self._validate_matching_capacity() @staticmethod @@ -200,6 +206,7 @@ def close(self) -> None: with self._lock: self._workspace = None + self._validated_signatures.clear() self._closed = True def __enter__(self) -> TorchDistributedDeterministicCollective: @@ -271,12 +278,15 @@ def _validate_matching_signature(self, op_name: str, input: torch.Tensor) -> Non if self.world_size == 1: return signature = (op_name, tuple(input.shape), str(input.dtype), input.numel()) + if signature in self._validated_signatures: + return signatures: list[tuple[Any, ...] | None] = [None] * self.world_size dist.all_gather_object(signatures, signature, group=self.group) if any(peer_signature != signature for peer_signature in signatures): raise ValueError( f"all ranks must call {op_name} with matching shapes and dtypes; got {signatures}" ) + self._validated_signatures.add(signature) def _validate_matching_capacity(self) -> None: if self.world_size == 1: @@ -336,15 +346,21 @@ def _workspace_for(self, input: torch.Tensor, required_elements: int) -> torch.T @staticmethod def _balanced_tree_sum(rank_inputs: torch.Tensor) -> torch.Tensor: - level = list(rank_inputs.unbind(0)) - if len(level) not in _SUPPORTED_WORLD_SIZES: + world_size = rank_inputs.size(0) + if world_size not in _SUPPORTED_WORLD_SIZES: raise ValueError( "balanced reduction requires rank inputs for world_size in " - f"{_SUPPORTED_WORLD_SIZES}, got {len(level)}" + f"{_SUPPORTED_WORLD_SIZES}, got {world_size}" ) - while len(level) > 1: - level = [torch.add(level[index], level[index + 1]) for index in range(0, len(level), 2)] - return level[0] + # ``rank_inputs`` is the private transport workspace for reductions, + # so fixed-tree nodes can be accumulated in place. This preserves the + # exact pairings while avoiding one temporary allocation per tree node. + stride = 1 + while stride < world_size: + for index in range(0, world_size, 2 * stride): + rank_inputs[index].add_(rank_inputs[index + stride]) + stride *= 2 + return rank_inputs[0] class RCCLDeterministicCollective(TorchDistributedDeterministicCollective): diff --git a/rl_engine/kernels/ops/cuda/attention/cp_comm.py b/rl_engine/kernels/ops/cuda/attention/cp_comm.py index 4f945f14..9f805fe5 100644 --- a/rl_engine/kernels/ops/cuda/attention/cp_comm.py +++ b/rl_engine/kernels/ops/cuda/attention/cp_comm.py @@ -684,8 +684,11 @@ def reduce_scatter(self, full: torch.Tensor) -> torch.Tensor: for source in range(self.world_size): source_begin = source * full.size(0) + begin level.append(gathered[source_begin : source_begin + rows_per_rank]) - while len(level) > 1: - level = [torch.add(level[index], level[index + 1]) for index in range(0, len(level), 2)] + stride = 1 + while stride < self.world_size: + for index in range(0, self.world_size, 2 * stride): + level[index].add_(level[index + stride]) + stride *= 2 return level[0] diff --git a/tests/distributed/test_transport_deterministic_collective.py b/tests/distributed/test_transport_deterministic_collective.py index 00c987e1..ed1da194 100644 --- a/tests/distributed/test_transport_deterministic_collective.py +++ b/tests/distributed/test_transport_deterministic_collective.py @@ -35,6 +35,7 @@ def __init__( self.peer_capacities = peer_capacities self.into_tensor_calls = 0 self.list_transport_calls = 0 + self.object_gather_calls = 0 self.last_transport_output: torch.Tensor | None = None @property @@ -59,6 +60,7 @@ def get_backend(self, group: Any) -> str: return self.backend def all_gather_object(self, output: list[Any], value: Any, *, group: Any) -> None: + self.object_gather_calls += 1 if isinstance(value, int): values = self.peer_capacities or [value] * len(self.peer_inputs) else: @@ -216,6 +218,19 @@ def test_reduction_workspace_grows_once_and_is_reused( assert collective.workspace_size_bytes == 0 +def test_matching_signature_is_validated_once_per_hot_path( + monkeypatch: pytest.MonkeyPatch, +) -> None: + peers = [torch.full((2, 3), rank + 1, dtype=torch.float32) for rank in range(2)] + collective, fake_dist = _make_collective(monkeypatch, peers, backend="nccl") + constructor_object_gathers = fake_dist.object_gather_calls + + collective.all_reduce(peers[0]) + collective.all_reduce(peers[0]) + + assert fake_dist.object_gather_calls == constructor_object_gathers + 1 + + def test_reduce_scatter_reduces_then_selects_local_leading_shard( monkeypatch: pytest.MonkeyPatch, ) -> None: From d36d70bb0c5ed1ec72ad563aff5ef834af85a38f Mon Sep 17 00:00:00 2001 From: vensen Date: Fri, 28 Aug 2026 17:37:49 +0000 Subject: [PATCH 18/35] refactor(distributed): unify platform collectives module --- csrc/ops.cpp | 2 +- rl_engine/distributed/__init__.py | 4 +- rl_engine/distributed/collectives.py | 416 +++++++++++++++++ .../distributed/transport_collectives.py | 435 ------------------ ...test_transport_deterministic_collective.py | 46 +- 5 files changed, 448 insertions(+), 455 deletions(-) delete mode 100644 rl_engine/distributed/transport_collectives.py diff --git a/csrc/ops.cpp b/csrc/ops.cpp index fb84dc64..8936c9fb 100644 --- a/csrc/ops.cpp +++ b/csrc/ops.cpp @@ -95,7 +95,7 @@ torch::Tensor deterministic_logp_forward_indexed_fp32(torch::Tensor logits, torc #if !defined(USE_ROCM) && !defined(KERNEL_ALIGN_WITH_ROCM) // Single-node TP=8 deterministic CUDA IPC collectives. ROCm uses the -// rank-ordered RCCL transport in rl_engine.distributed.transport_collectives. +// rank-ordered RCCL transport in rl_engine.distributed.collectives. std::tuple, int64_t> deterministic_collective_ipc_meta( torch::Tensor& tensor); int64_t deterministic_collective_create( diff --git a/rl_engine/distributed/__init__.py b/rl_engine/distributed/__init__.py index f83f2520..9010a534 100644 --- a/rl_engine/distributed/__init__.py +++ b/rl_engine/distributed/__init__.py @@ -1,8 +1,8 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright (c) 2026 RL-Kernel Contributors -from rl_engine.distributed.collectives import DeterministicCollective -from rl_engine.distributed.transport_collectives import ( +from rl_engine.distributed.collectives import ( + DeterministicCollective, RCCLDeterministicCollective, TorchDistributedDeterministicCollective, create_deterministic_collective, diff --git a/rl_engine/distributed/collectives.py b/rl_engine/distributed/collectives.py index 54b00fa5..db3ac70f 100644 --- a/rl_engine/distributed/collectives.py +++ b/rl_engine/distributed/collectives.py @@ -1,5 +1,11 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright (c) 2026 RL-Kernel Contributors +"""Deterministic collectives for native CUDA IPC and rank-ordered transport. + +The ROCm transport never performs a floating-point reduction. It gathers every +rank's input through RCCL, after which each rank evaluates the same balanced +reduction tree locally. +""" from __future__ import annotations @@ -323,3 +329,413 @@ def _synchronize_ranks(self) -> None: dist.barrier(group=self.group, device_ids=[self.device.index]) else: dist.barrier(group=self.group) + + +class TorchDistributedDeterministicCollective: + """Correctness-first collectives using AllGather as transport only. + + Rank inputs are gathered without arithmetic and reduced locally as the + balanced tree ``((rank0 + rank1) + (rank2 + rank3)) + ...``. Consequently, + all ranks execute the exact same floating-point expression. TP sizes 1, + 2, 4, and 8 are nested prefixes of that expression and match the existing + CUDA IPC collective's ordering. + + The generic class also supports a CPU/Gloo process group, which is useful + as an executable reference. Production ROCm callers should use + :class:`RCCLDeterministicCollective` or + :func:`create_deterministic_collective` so backend validation fails closed. + All ranks must call methods in the same order with matching input shapes + and dtypes, and construct the instance with the same ``max_size_bytes``. + """ + + backend_id = "torch_distributed_balanced_tree" + transport_only = True + reduction_order = "balanced_rank_tree" + supports_async_overlap = False + supports_compute_communication_fusion = False + + def __init__( + self, + group: dist.ProcessGroup | None = None, + device: torch.device | str | int | None = None, + *, + max_size_bytes: int = _DEFAULT_MAX_SIZE_BYTES, + ) -> None: + if not dist.is_available() or not dist.is_initialized(): + raise RuntimeError("torch.distributed must be initialized before collectives") + if max_size_bytes <= 0: + raise ValueError("max_size_bytes must be positive") + + self.group = group if group is not None else dist.group.WORLD + self.rank = int(dist.get_rank(group=self.group)) + self.world_size = int(dist.get_world_size(group=self.group)) + if self.world_size not in _SUPPORTED_WORLD_SIZES: + raise ValueError( + "deterministic collectives require world_size in " + f"{_SUPPORTED_WORLD_SIZES}, got {self.world_size}" + ) + + self.device = self._normalize_device(device) + self.max_size_bytes = int(max_size_bytes) + self._backend = str(dist.get_backend(self.group)).lower() + self._lock = threading.Lock() + self._closed = False + # One dtype-agnostic byte workspace is grown on demand and reused by + # reduction collectives. AllGather writes directly into its output. + self._workspace: torch.Tensor | None = None + # A Python-object collective is useful for catching a mismatched new + # signature, but running one on every hot-path call dominates small + # message latency. Validate each local signature once and then rely on + # the standard collective contract that ranks call operations in the + # same order. + self._validated_signatures: set[tuple[Any, ...]] = set() + self._validate_matching_capacity() + + @staticmethod + def _normalize_device( + device: torch.device | str | int | None, + ) -> torch.device: + if device is None: + if torch.cuda.is_available(): + normalized = torch.device("cuda", torch.cuda.current_device()) + else: + normalized = torch.device("cpu") + elif isinstance(device, int): + normalized = torch.device("cuda", device) + else: + normalized = torch.device(device) + + if normalized.type == "cuda": + if not torch.cuda.is_available(): + raise RuntimeError("a CUDA/ROCm device was requested but none is available") + current_device = torch.cuda.current_device() + if normalized.index is None: + normalized = torch.device("cuda", current_device) + if normalized.index != current_device: + raise ValueError( + "the collective device must be the current CUDA/ROCm device; call " + f"torch.cuda.set_device({normalized.index}) first" + ) + return normalized + + @property + def closed(self) -> bool: + """Whether this instance rejects further collective calls.""" + + return self._closed + + @property + def workspace_size_bytes(self) -> int: + """Currently retained reduction workspace size in bytes.""" + + workspace = self._workspace + return 0 if workspace is None else int(workspace.numel()) + + def all_reduce( + self, + input: torch.Tensor, + *, + out: torch.Tensor | None = None, + ) -> torch.Tensor: + """Return the fixed balanced-tree sum on every rank.""" + + self._check_open() + self._validate_reduction_input(input) + if out is None: + out = torch.empty_like(input) + self._validate_output(out, input, tuple(input.shape)) + + with self._lock: + self._check_open() + self._validate_matching_signature("all_reduce", input) + rank_inputs = self._all_gather_transport(input) + reduced = self._balanced_tree_sum(rank_inputs) + out.copy_(reduced) + return out + + def all_gather( + self, + input: torch.Tensor, + *, + out: torch.Tensor | None = None, + ) -> torch.Tensor: + """Gather rank-ordered input bit patterns along dimension 0.""" + + self._check_open() + self._validate_gather_input(input) + output_shape = (input.size(0) * self.world_size, *input.shape[1:]) + if out is None: + out = torch.empty(output_shape, dtype=input.dtype, device=input.device) + self._validate_output(out, input, output_shape) + + with self._lock: + self._check_open() + self._validate_matching_signature("all_gather", input) + self._all_gather_transport(input, gathered_flat=out.view(-1)) + return out + + def reduce_scatter( + self, + input: torch.Tensor, + *, + out: torch.Tensor | None = None, + ) -> torch.Tensor: + """Fixed-tree sum followed by rank-ordered dimension-0 slicing.""" + + self._check_open() + self._validate_reduction_input(input) + if input.dim() == 0: + raise ValueError("reduce_scatter input must have at least one dimension") + if input.size(0) % self.world_size != 0: + raise ValueError( + "reduce_scatter input.size(0) must be divisible by " + f"world_size={self.world_size}; got {input.size(0)}" + ) + rows_per_rank = input.size(0) // self.world_size + output_shape = (rows_per_rank, *input.shape[1:]) + if out is None: + out = torch.empty(output_shape, dtype=input.dtype, device=input.device) + self._validate_output(out, input, output_shape) + + with self._lock: + self._check_open() + self._validate_matching_signature("reduce_scatter", input) + rank_inputs = self._all_gather_transport(input) + reduced = self._balanced_tree_sum(rank_inputs) + begin = self.rank * rows_per_rank + out.copy_(reduced.narrow(0, begin, rows_per_rank)) + return out + + def close(self) -> None: + """Close the instance. + + Closing releases the lazily allocated reduction workspace and marks + the lifecycle boundary. Collective calls are blocking at this API. + """ + + with self._lock: + self._workspace = None + self._validated_signatures.clear() + self._closed = True + + def __enter__(self) -> TorchDistributedDeterministicCollective: + self._check_open() + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + self.close() + + def __del__(self) -> None: + try: + self.close() + except Exception: + pass + + def _check_open(self) -> None: + if getattr(self, "_closed", True): + raise RuntimeError("deterministic collective is closed") + + def _validate_tensor(self, input: torch.Tensor) -> None: + if not isinstance(input, torch.Tensor): + raise TypeError(f"input must be a torch.Tensor, got {type(input)!r}") + if input.device != self.device: + raise ValueError(f"input must be on {self.device}, got {input.device}") + if not input.is_contiguous(): + raise ValueError("input must be contiguous") + input_bytes = input.numel() * input.element_size() + if input_bytes > self.max_size_bytes: + raise ValueError( + f"input requires {input_bytes} bytes but max_size_bytes={self.max_size_bytes}" + ) + + def _validate_reduction_input(self, input: torch.Tensor) -> None: + self._validate_tensor(input) + if input.dtype not in _REDUCTION_DTYPES: + raise TypeError( + "deterministic reductions support float32, float16, and bfloat16; " + f"got {input.dtype}" + ) + + def _validate_gather_input(self, input: torch.Tensor) -> None: + self._validate_tensor(input) + if input.dim() == 0: + raise ValueError("all_gather input must have at least one dimension") + + @staticmethod + def _validate_output( + output: torch.Tensor, + input: torch.Tensor, + output_shape: tuple[int, ...], + ) -> None: + if not isinstance(output, torch.Tensor): + raise TypeError(f"out must be a torch.Tensor, got {type(output)!r}") + if output.device != input.device: + raise ValueError("out must be on the same device as input") + if output.dtype != input.dtype: + raise TypeError("out must have the same dtype as input") + if tuple(output.shape) != output_shape: + raise ValueError(f"out must have shape {output_shape}, got {tuple(output.shape)}") + if not output.is_contiguous(): + raise ValueError("out must be contiguous") + + def _validate_matching_signature(self, op_name: str, input: torch.Tensor) -> None: + if self.world_size == 1: + return + signature = (op_name, tuple(input.shape), str(input.dtype), input.numel()) + if signature in self._validated_signatures: + return + signatures: list[tuple[Any, ...] | None] = [None] * self.world_size + dist.all_gather_object(signatures, signature, group=self.group) + if any(peer_signature != signature for peer_signature in signatures): + raise ValueError( + f"all ranks must call {op_name} with matching shapes and dtypes; got {signatures}" + ) + self._validated_signatures.add(signature) + + def _validate_matching_capacity(self) -> None: + if self.world_size == 1: + return + capacities: list[int | None] = [None] * self.world_size + dist.all_gather_object(capacities, self.max_size_bytes, group=self.group) + if any(peer_capacity != self.max_size_bytes for peer_capacity in capacities): + raise ValueError(f"all ranks must use the same max_size_bytes; got {capacities}") + + def _all_gather_transport( + self, + input: torch.Tensor, + *, + gathered_flat: torch.Tensor | None = None, + ) -> torch.Tensor: + # Flattening makes the output contract independent of whether a given + # ProcessGroup implements the concatenation or stacking form of AG. + if self.world_size == 1: + if gathered_flat is None: + gathered_flat = input.clone().view(-1) + else: + gathered_flat.copy_(input.view(-1)) + return gathered_flat.reshape((1, *input.shape)) + input_flat = input.view(-1) + required_elements = self.world_size * input_flat.numel() + if gathered_flat is None: + gathered_flat = self._workspace_for(input, required_elements) + elif ( + gathered_flat.numel() != required_elements + or gathered_flat.dtype != input.dtype + or gathered_flat.device != input.device + or not gathered_flat.is_contiguous() + ): + raise ValueError("gathered transport output has an invalid layout") + if "nccl" in self._backend: + # PyTorch exposes RCCL through the NCCL ProcessGroup API. Keep this + # as a tensor-only transport; reduction happens below. + dist.all_gather_into_tensor(gathered_flat, input_flat, group=self.group) + else: + # Some reference backends (notably Gloo versions without + # all_gather_into_tensor) only implement the list API. + gathered_chunks = list( + gathered_flat.reshape(self.world_size, input_flat.numel()).unbind(0) + ) + dist.all_gather(gathered_chunks, input_flat, group=self.group) + return gathered_flat.reshape((self.world_size, *input.shape)) + + def _workspace_for(self, input: torch.Tensor, required_elements: int) -> torch.Tensor: + required_bytes = required_elements * input.element_size() + workspace = self._workspace + if workspace is None or workspace.numel() < required_bytes: + workspace = torch.empty(required_bytes, dtype=torch.uint8, device=self.device) + self._workspace = workspace + # Tensor.view(dtype) reinterprets the aligned byte allocation without + # an allocation or copy. Restrict the view to the current operation. + return workspace[:required_bytes].view(input.dtype) + + @staticmethod + def _balanced_tree_sum(rank_inputs: torch.Tensor) -> torch.Tensor: + world_size = rank_inputs.size(0) + if world_size not in _SUPPORTED_WORLD_SIZES: + raise ValueError( + "balanced reduction requires rank inputs for world_size in " + f"{_SUPPORTED_WORLD_SIZES}, got {world_size}" + ) + # ``rank_inputs`` is the private transport workspace for reductions, + # so fixed-tree nodes can be accumulated in place. This preserves the + # exact pairings while avoiding one temporary allocation per tree node. + stride = 1 + while stride < world_size: + for index in range(0, world_size, 2 * stride): + rank_inputs[index].add_(rank_inputs[index + stride]) + stride *= 2 + return rank_inputs[0] + + +class RCCLDeterministicCollective(TorchDistributedDeterministicCollective): + """ROCm collective using RCCL AllGather strictly as tensor transport.""" + + backend_id = "rccl_all_gather_balanced_tree" + + def __init__( + self, + group: dist.ProcessGroup | None = None, + device: torch.device | str | int | None = None, + *, + max_size_bytes: int = _DEFAULT_MAX_SIZE_BYTES, + ) -> None: + if getattr(torch.version, "hip", None) is None: + raise RuntimeError("RCCL deterministic collectives require a ROCm PyTorch build") + if not torch.cuda.is_available(): + raise RuntimeError("RCCL deterministic collectives require an available ROCm device") + if device is not None and not isinstance(device, int): + requested_device = torch.device(device) + if requested_device.type != "cuda": + raise ValueError( + f"RCCL deterministic collectives require a ROCm device, got {device!r}" + ) + super().__init__(group=group, device=device, max_size_bytes=max_size_bytes) + if self.device.type != "cuda": + raise ValueError( + f"RCCL deterministic collectives require a ROCm device, got {device!r}" + ) + if "nccl" not in self._backend: + raise RuntimeError( + "RCCL deterministic collectives require PyTorch's NCCL process-group API" + ) + + +def create_deterministic_collective( + group: dist.ProcessGroup | None = None, + device: torch.device | str | int | None = None, + *, + max_size_bytes: int = _DEFAULT_MAX_SIZE_BYTES, +) -> Any: + """Create the platform-appropriate deterministic collective. + + CUDA uses the native ``DeterministicCollective`` implementation. ROCm uses + RCCL only to gather rank inputs, followed by the shared local balanced-tree + reduction. The returned object has independent ownership; callers may cache + it and must close an entry before replacing it. + """ + + if getattr(torch.version, "hip", None) is not None: + return RCCLDeterministicCollective( + group=group, + device=device, + max_size_bytes=max_size_bytes, + ) + + return DeterministicCollective( + group=group, + device=device, + max_size_bytes=max_size_bytes, + ) + + +__all__ = [ + "DeterministicCollective", + "RCCLDeterministicCollective", + "TorchDistributedDeterministicCollective", + "create_deterministic_collective", +] diff --git a/rl_engine/distributed/transport_collectives.py b/rl_engine/distributed/transport_collectives.py deleted file mode 100644 index ff9a4dcf..00000000 --- a/rl_engine/distributed/transport_collectives.py +++ /dev/null @@ -1,435 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 -# Copyright (c) 2026 RL-Kernel Contributors -"""Deterministic reductions built on rank-ordered tensor transport. - -The transport in this module never performs a floating-point reduction. It -only gathers every rank's input, after which each rank evaluates the same -balanced reduction tree locally. In a ROCm build, PyTorch's ``nccl`` backend -is RCCL and therefore ``all_gather_into_tensor`` provides the transport. -""" - -from __future__ import annotations - -import threading -from types import TracebackType -from typing import Any - -import torch -import torch.distributed as dist - -_SUPPORTED_WORLD_SIZES = (1, 2, 4, 8) -_DEFAULT_MAX_SIZE_BYTES = 64 * 1024 * 1024 -_REDUCTION_DTYPES = (torch.float32, torch.float16, torch.bfloat16) - - -class TorchDistributedDeterministicCollective: - """Correctness-first collectives using AllGather as transport only. - - Rank inputs are gathered without arithmetic and reduced locally as the - balanced tree ``((rank0 + rank1) + (rank2 + rank3)) + ...``. Consequently, - all ranks execute the exact same floating-point expression. TP sizes 1, - 2, 4, and 8 are nested prefixes of that expression and match the existing - CUDA IPC collective's ordering. - - The generic class also supports a CPU/Gloo process group, which is useful - as an executable reference. Production ROCm callers should use - :class:`RCCLDeterministicCollective` or - :func:`create_deterministic_collective` so backend validation fails closed. - All ranks must call methods in the same order with matching input shapes - and dtypes, and construct the instance with the same ``max_size_bytes``. - """ - - backend_id = "torch_distributed_balanced_tree" - transport_only = True - reduction_order = "balanced_rank_tree" - supports_async_overlap = False - supports_compute_communication_fusion = False - - def __init__( - self, - group: dist.ProcessGroup | None = None, - device: torch.device | str | int | None = None, - *, - max_size_bytes: int = _DEFAULT_MAX_SIZE_BYTES, - ) -> None: - if not dist.is_available() or not dist.is_initialized(): - raise RuntimeError("torch.distributed must be initialized before collectives") - if max_size_bytes <= 0: - raise ValueError("max_size_bytes must be positive") - - self.group = group if group is not None else dist.group.WORLD - self.rank = int(dist.get_rank(group=self.group)) - self.world_size = int(dist.get_world_size(group=self.group)) - if self.world_size not in _SUPPORTED_WORLD_SIZES: - raise ValueError( - "deterministic collectives require world_size in " - f"{_SUPPORTED_WORLD_SIZES}, got {self.world_size}" - ) - - self.device = self._normalize_device(device) - self.max_size_bytes = int(max_size_bytes) - self._backend = str(dist.get_backend(self.group)).lower() - self._lock = threading.Lock() - self._closed = False - # One dtype-agnostic byte workspace is grown on demand and reused by - # reduction collectives. AllGather writes directly into its output. - self._workspace: torch.Tensor | None = None - # A Python-object collective is useful for catching a mismatched new - # signature, but running one on every hot-path call dominates small - # message latency. Validate each local signature once and then rely on - # the standard collective contract that ranks call operations in the - # same order. - self._validated_signatures: set[tuple[Any, ...]] = set() - self._validate_matching_capacity() - - @staticmethod - def _normalize_device( - device: torch.device | str | int | None, - ) -> torch.device: - if device is None: - if torch.cuda.is_available(): - normalized = torch.device("cuda", torch.cuda.current_device()) - else: - normalized = torch.device("cpu") - elif isinstance(device, int): - normalized = torch.device("cuda", device) - else: - normalized = torch.device(device) - - if normalized.type == "cuda": - if not torch.cuda.is_available(): - raise RuntimeError("a CUDA/ROCm device was requested but none is available") - current_device = torch.cuda.current_device() - if normalized.index is None: - normalized = torch.device("cuda", current_device) - if normalized.index != current_device: - raise ValueError( - "the collective device must be the current CUDA/ROCm device; call " - f"torch.cuda.set_device({normalized.index}) first" - ) - return normalized - - @property - def closed(self) -> bool: - """Whether this instance rejects further collective calls.""" - - return self._closed - - @property - def workspace_size_bytes(self) -> int: - """Currently retained reduction workspace size in bytes.""" - - workspace = self._workspace - return 0 if workspace is None else int(workspace.numel()) - - def all_reduce( - self, - input: torch.Tensor, - *, - out: torch.Tensor | None = None, - ) -> torch.Tensor: - """Return the fixed balanced-tree sum on every rank.""" - - self._check_open() - self._validate_reduction_input(input) - if out is None: - out = torch.empty_like(input) - self._validate_output(out, input, tuple(input.shape)) - - with self._lock: - self._check_open() - self._validate_matching_signature("all_reduce", input) - rank_inputs = self._all_gather_transport(input) - reduced = self._balanced_tree_sum(rank_inputs) - out.copy_(reduced) - return out - - def all_gather( - self, - input: torch.Tensor, - *, - out: torch.Tensor | None = None, - ) -> torch.Tensor: - """Gather rank-ordered input bit patterns along dimension 0.""" - - self._check_open() - self._validate_gather_input(input) - output_shape = (input.size(0) * self.world_size, *input.shape[1:]) - if out is None: - out = torch.empty(output_shape, dtype=input.dtype, device=input.device) - self._validate_output(out, input, output_shape) - - with self._lock: - self._check_open() - self._validate_matching_signature("all_gather", input) - self._all_gather_transport(input, gathered_flat=out.view(-1)) - return out - - def reduce_scatter( - self, - input: torch.Tensor, - *, - out: torch.Tensor | None = None, - ) -> torch.Tensor: - """Fixed-tree sum followed by rank-ordered dimension-0 slicing.""" - - self._check_open() - self._validate_reduction_input(input) - if input.dim() == 0: - raise ValueError("reduce_scatter input must have at least one dimension") - if input.size(0) % self.world_size != 0: - raise ValueError( - "reduce_scatter input.size(0) must be divisible by " - f"world_size={self.world_size}; got {input.size(0)}" - ) - rows_per_rank = input.size(0) // self.world_size - output_shape = (rows_per_rank, *input.shape[1:]) - if out is None: - out = torch.empty(output_shape, dtype=input.dtype, device=input.device) - self._validate_output(out, input, output_shape) - - with self._lock: - self._check_open() - self._validate_matching_signature("reduce_scatter", input) - rank_inputs = self._all_gather_transport(input) - reduced = self._balanced_tree_sum(rank_inputs) - begin = self.rank * rows_per_rank - out.copy_(reduced.narrow(0, begin, rows_per_rank)) - return out - - def close(self) -> None: - """Close the instance. - - Closing releases the lazily allocated reduction workspace and marks - the lifecycle boundary. Collective calls are blocking at this API. - """ - - with self._lock: - self._workspace = None - self._validated_signatures.clear() - self._closed = True - - def __enter__(self) -> TorchDistributedDeterministicCollective: - self._check_open() - return self - - def __exit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - traceback: TracebackType | None, - ) -> None: - self.close() - - def __del__(self) -> None: - try: - self.close() - except Exception: - pass - - def _check_open(self) -> None: - if getattr(self, "_closed", True): - raise RuntimeError("deterministic collective is closed") - - def _validate_tensor(self, input: torch.Tensor) -> None: - if not isinstance(input, torch.Tensor): - raise TypeError(f"input must be a torch.Tensor, got {type(input)!r}") - if input.device != self.device: - raise ValueError(f"input must be on {self.device}, got {input.device}") - if not input.is_contiguous(): - raise ValueError("input must be contiguous") - input_bytes = input.numel() * input.element_size() - if input_bytes > self.max_size_bytes: - raise ValueError( - f"input requires {input_bytes} bytes but max_size_bytes={self.max_size_bytes}" - ) - - def _validate_reduction_input(self, input: torch.Tensor) -> None: - self._validate_tensor(input) - if input.dtype not in _REDUCTION_DTYPES: - raise TypeError( - "deterministic reductions support float32, float16, and bfloat16; " - f"got {input.dtype}" - ) - - def _validate_gather_input(self, input: torch.Tensor) -> None: - self._validate_tensor(input) - if input.dim() == 0: - raise ValueError("all_gather input must have at least one dimension") - - @staticmethod - def _validate_output( - output: torch.Tensor, - input: torch.Tensor, - output_shape: tuple[int, ...], - ) -> None: - if not isinstance(output, torch.Tensor): - raise TypeError(f"out must be a torch.Tensor, got {type(output)!r}") - if output.device != input.device: - raise ValueError("out must be on the same device as input") - if output.dtype != input.dtype: - raise TypeError("out must have the same dtype as input") - if tuple(output.shape) != output_shape: - raise ValueError(f"out must have shape {output_shape}, got {tuple(output.shape)}") - if not output.is_contiguous(): - raise ValueError("out must be contiguous") - - def _validate_matching_signature(self, op_name: str, input: torch.Tensor) -> None: - if self.world_size == 1: - return - signature = (op_name, tuple(input.shape), str(input.dtype), input.numel()) - if signature in self._validated_signatures: - return - signatures: list[tuple[Any, ...] | None] = [None] * self.world_size - dist.all_gather_object(signatures, signature, group=self.group) - if any(peer_signature != signature for peer_signature in signatures): - raise ValueError( - f"all ranks must call {op_name} with matching shapes and dtypes; got {signatures}" - ) - self._validated_signatures.add(signature) - - def _validate_matching_capacity(self) -> None: - if self.world_size == 1: - return - capacities: list[int | None] = [None] * self.world_size - dist.all_gather_object(capacities, self.max_size_bytes, group=self.group) - if any(peer_capacity != self.max_size_bytes for peer_capacity in capacities): - raise ValueError(f"all ranks must use the same max_size_bytes; got {capacities}") - - def _all_gather_transport( - self, - input: torch.Tensor, - *, - gathered_flat: torch.Tensor | None = None, - ) -> torch.Tensor: - # Flattening makes the output contract independent of whether a given - # ProcessGroup implements the concatenation or stacking form of AG. - if self.world_size == 1: - if gathered_flat is None: - gathered_flat = input.clone().view(-1) - else: - gathered_flat.copy_(input.view(-1)) - return gathered_flat.reshape((1, *input.shape)) - input_flat = input.view(-1) - required_elements = self.world_size * input_flat.numel() - if gathered_flat is None: - gathered_flat = self._workspace_for(input, required_elements) - elif ( - gathered_flat.numel() != required_elements - or gathered_flat.dtype != input.dtype - or gathered_flat.device != input.device - or not gathered_flat.is_contiguous() - ): - raise ValueError("gathered transport output has an invalid layout") - if "nccl" in self._backend: - # PyTorch exposes RCCL through the NCCL ProcessGroup API. Keep - # this as a tensor-only transport; reduction happens below. - dist.all_gather_into_tensor(gathered_flat, input_flat, group=self.group) - else: - # Some reference backends (notably Gloo versions without - # all_gather_into_tensor) only implement the list API. - gathered_chunks = list( - gathered_flat.reshape(self.world_size, input_flat.numel()).unbind(0) - ) - dist.all_gather(gathered_chunks, input_flat, group=self.group) - return gathered_flat.reshape((self.world_size, *input.shape)) - - def _workspace_for(self, input: torch.Tensor, required_elements: int) -> torch.Tensor: - required_bytes = required_elements * input.element_size() - workspace = self._workspace - if workspace is None or workspace.numel() < required_bytes: - workspace = torch.empty(required_bytes, dtype=torch.uint8, device=self.device) - self._workspace = workspace - # Tensor.view(dtype) reinterprets the aligned byte allocation without - # an allocation or copy. Restrict the view to the current operation. - return workspace[:required_bytes].view(input.dtype) - - @staticmethod - def _balanced_tree_sum(rank_inputs: torch.Tensor) -> torch.Tensor: - world_size = rank_inputs.size(0) - if world_size not in _SUPPORTED_WORLD_SIZES: - raise ValueError( - "balanced reduction requires rank inputs for world_size in " - f"{_SUPPORTED_WORLD_SIZES}, got {world_size}" - ) - # ``rank_inputs`` is the private transport workspace for reductions, - # so fixed-tree nodes can be accumulated in place. This preserves the - # exact pairings while avoiding one temporary allocation per tree node. - stride = 1 - while stride < world_size: - for index in range(0, world_size, 2 * stride): - rank_inputs[index].add_(rank_inputs[index + stride]) - stride *= 2 - return rank_inputs[0] - - -class RCCLDeterministicCollective(TorchDistributedDeterministicCollective): - """ROCm collective using RCCL AllGather strictly as tensor transport.""" - - backend_id = "rccl_all_gather_balanced_tree" - - def __init__( - self, - group: dist.ProcessGroup | None = None, - device: torch.device | str | int | None = None, - *, - max_size_bytes: int = _DEFAULT_MAX_SIZE_BYTES, - ) -> None: - if getattr(torch.version, "hip", None) is None: - raise RuntimeError("RCCL deterministic collectives require a ROCm PyTorch build") - if not torch.cuda.is_available(): - raise RuntimeError("RCCL deterministic collectives require an available ROCm device") - if device is not None and not isinstance(device, int): - requested_device = torch.device(device) - if requested_device.type != "cuda": - raise ValueError( - f"RCCL deterministic collectives require a ROCm device, got {device!r}" - ) - super().__init__(group=group, device=device, max_size_bytes=max_size_bytes) - if self.device.type != "cuda": - raise ValueError( - f"RCCL deterministic collectives require a ROCm device, got {device!r}" - ) - if "nccl" not in self._backend: - raise RuntimeError( - "RCCL deterministic collectives require PyTorch's NCCL process-group API" - ) - - -def create_deterministic_collective( - group: dist.ProcessGroup | None = None, - device: torch.device | str | int | None = None, - *, - max_size_bytes: int = _DEFAULT_MAX_SIZE_BYTES, -) -> Any: - """Create the platform-appropriate deterministic collective. - - CUDA keeps using the existing IPC implementation. ROCm uses RCCL only to - gather rank inputs, followed by the shared local balanced-tree reduction. - The returned object has independent ownership; callers may cache it by - process-group/rank/device and must close an entry before replacing it. - """ - - if getattr(torch.version, "hip", None) is not None: - return RCCLDeterministicCollective( - group=group, - device=device, - max_size_bytes=max_size_bytes, - ) - - # Import lazily to keep the existing CUDA implementation and its extension - # checks independent of the generic transport reference above. - from rl_engine.distributed.collectives import DeterministicCollective - - return DeterministicCollective( - group=group, - device=device, - max_size_bytes=max_size_bytes, - ) - - -__all__ = [ - "RCCLDeterministicCollective", - "TorchDistributedDeterministicCollective", - "create_deterministic_collective", -] diff --git a/tests/distributed/test_transport_deterministic_collective.py b/tests/distributed/test_transport_deterministic_collective.py index ed1da194..dddd674b 100644 --- a/tests/distributed/test_transport_deterministic_collective.py +++ b/tests/distributed/test_transport_deterministic_collective.py @@ -8,8 +8,8 @@ import pytest import torch -import rl_engine.distributed.collectives as cuda_collectives -import rl_engine.distributed.transport_collectives as transport_collectives +import rl_engine.distributed as distributed +import rl_engine.distributed.collectives as collectives from rl_engine.distributed import ( RCCLDeterministicCollective, TorchDistributedDeterministicCollective, @@ -92,6 +92,18 @@ def all_gather( destination.copy_(peer.reshape(-1)) +def test_public_exports_use_canonical_collectives_module() -> None: + assert distributed.DeterministicCollective is collectives.DeterministicCollective + assert distributed.RCCLDeterministicCollective is collectives.RCCLDeterministicCollective + assert ( + distributed.TorchDistributedDeterministicCollective + is collectives.TorchDistributedDeterministicCollective + ) + assert ( + distributed.create_deterministic_collective is collectives.create_deterministic_collective + ) + + def _make_collective( monkeypatch: pytest.MonkeyPatch, peer_inputs: list[torch.Tensor], @@ -109,7 +121,7 @@ def _make_collective( peer_signatures=peer_signatures, peer_capacities=peer_capacities, ) - monkeypatch.setattr(transport_collectives, "dist", fake_dist) + monkeypatch.setattr(collectives, "dist", fake_dist) collective = TorchDistributedDeterministicCollective( group=object(), device="cpu", @@ -312,14 +324,14 @@ def test_unsupported_world_size_is_rejected( world_size: int, ) -> None: fake_dist = _FakeDistributed([torch.ones(1)] * world_size) - monkeypatch.setattr(transport_collectives, "dist", fake_dist) + monkeypatch.setattr(collectives, "dist", fake_dist) with pytest.raises(ValueError, match="world_size in"): TorchDistributedDeterministicCollective(group=object(), device="cpu") def test_rccl_class_requires_rocm_build(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setattr(transport_collectives.torch.version, "hip", None, raising=False) + monkeypatch.setattr(collectives.torch.version, "hip", None, raising=False) with pytest.raises(RuntimeError, match="ROCm PyTorch build"): RCCLDeterministicCollective(group=object(), device="cuda:0") @@ -327,10 +339,10 @@ def test_rccl_class_requires_rocm_build(monkeypatch: pytest.MonkeyPatch) -> None def test_rccl_class_requires_nccl_process_group(monkeypatch: pytest.MonkeyPatch) -> None: fake_dist = _FakeDistributed([torch.ones(1)], backend="gloo") - monkeypatch.setattr(transport_collectives, "dist", fake_dist) - monkeypatch.setattr(transport_collectives.torch.version, "hip", "6.3", raising=False) - monkeypatch.setattr(transport_collectives.torch.cuda, "is_available", lambda: True) - monkeypatch.setattr(transport_collectives.torch.cuda, "current_device", lambda: 0) + monkeypatch.setattr(collectives, "dist", fake_dist) + monkeypatch.setattr(collectives.torch.version, "hip", "6.3", raising=False) + monkeypatch.setattr(collectives.torch.cuda, "is_available", lambda: True) + monkeypatch.setattr(collectives.torch.cuda, "current_device", lambda: 0) with pytest.raises(RuntimeError, match="NCCL process-group API"): RCCLDeterministicCollective(group=object(), device="cuda:0") @@ -339,8 +351,8 @@ def test_rccl_class_requires_nccl_process_group(monkeypatch: pytest.MonkeyPatch) def test_rccl_class_rejects_cpu_before_process_group_exchange( monkeypatch: pytest.MonkeyPatch, ) -> None: - monkeypatch.setattr(transport_collectives.torch.version, "hip", "6.3", raising=False) - monkeypatch.setattr(transport_collectives.torch.cuda, "is_available", lambda: True) + monkeypatch.setattr(collectives.torch.version, "hip", "6.3", raising=False) + monkeypatch.setattr(collectives.torch.cuda, "is_available", lambda: True) with pytest.raises(ValueError, match="ROCm device"): RCCLDeterministicCollective(group=object(), device="cpu") @@ -354,11 +366,11 @@ def fake_rccl(**kwargs: Any) -> object: calls.append(kwargs) return sentinel - monkeypatch.setattr(transport_collectives.torch.version, "hip", "6.3", raising=False) - monkeypatch.setattr(transport_collectives, "RCCLDeterministicCollective", fake_rccl) + monkeypatch.setattr(collectives.torch.version, "hip", "6.3", raising=False) + monkeypatch.setattr(collectives, "RCCLDeterministicCollective", fake_rccl) group = object() - result = transport_collectives.create_deterministic_collective( + result = collectives.create_deterministic_collective( group=group, device="cuda:3", max_size_bytes=1234, @@ -376,11 +388,11 @@ def fake_cuda(**kwargs: Any) -> object: calls.append(kwargs) return sentinel - monkeypatch.setattr(transport_collectives.torch.version, "hip", None, raising=False) - monkeypatch.setattr(cuda_collectives, "DeterministicCollective", fake_cuda) + monkeypatch.setattr(collectives.torch.version, "hip", None, raising=False) + monkeypatch.setattr(collectives, "DeterministicCollective", fake_cuda) group = object() - result = transport_collectives.create_deterministic_collective( + result = collectives.create_deterministic_collective( group=group, device="cuda:1", max_size_bytes=4321, From e8e79e7a31ada85bf8114560180581921135479e Mon Sep 17 00:00:00 2001 From: inaniloquentee Date: Sat, 29 Aug 2026 05:54:08 +0000 Subject: [PATCH 19/35] fix(build): repair ops.cpp preprocessor guards mangled by the merge 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) Claude-Session: https://claude.ai/code/session_01824WkgMDxtBKD4Ex2NkfYA --- csrc/ops.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/csrc/ops.cpp b/csrc/ops.cpp index 63f4a65f..11140d08 100644 --- a/csrc/ops.cpp +++ b/csrc/ops.cpp @@ -450,8 +450,10 @@ PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) { "Run the TP=8 deterministic rank-ordered all-gather kernel"); #endif - // Prefix-shared attention uses NVIDIA PTX and falls back to PyTorch SDPA on - // ROCm; it is covered by the same guard opened above. +#if !defined(USE_ROCM) + // Prefix-shared attention uses NVIDIA PTX; the declaration above carries the + // same guard, so the registration must repeat it or a ROCm build fails on an + // undeclared identifier. m.def("prefix_shared_attention", &prefix_shared_attention, "Prefix-Shared Fused Attention for GRPO"); #endif From a8deab9d91176c98693610c1682eceb320415603 Mon Sep 17 00:00:00 2001 From: inaniloquentee Date: Sat, 29 Aug 2026 05:54:20 +0000 Subject: [PATCH 20/35] feat(kernels): add bitwise Triton deterministic Attention core 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) Claude-Session: https://claude.ai/code/session_01824WkgMDxtBKD4Ex2NkfYA --- .../kernels/ops/triton/attention/__init__.py | 16 + .../triton/attention/deterministic_attn.py | 878 ++++++++++++++++++ tests/test_triton_deterministic_attention.py | 284 ++++++ 3 files changed, 1178 insertions(+) create mode 100644 rl_engine/kernels/ops/triton/attention/deterministic_attn.py create mode 100644 tests/test_triton_deterministic_attention.py diff --git a/rl_engine/kernels/ops/triton/attention/__init__.py b/rl_engine/kernels/ops/triton/attention/__init__.py index 220b6c95..7df3d2ba 100644 --- a/rl_engine/kernels/ops/triton/attention/__init__.py +++ b/rl_engine/kernels/ops/triton/attention/__init__.py @@ -1,6 +1,15 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright (c) 2026 RL-Kernel Contributors +from rl_engine.kernels.ops.triton.attention.deterministic_attn import ( + BITWISE_LIBM_PARITY, + TritonDeterministicAttentionOp, + triton_deterministic_attention, + triton_deterministic_attention_backward, + triton_deterministic_attention_forward, + triton_deterministic_attention_fp32, + triton_deterministic_attention_with_lse, +) from rl_engine.kernels.ops.triton.attention.standard_attn import ( TritonBatchInvariantAttentionOp, triton_batch_invariant_attention, @@ -8,7 +17,14 @@ ) __all__ = [ + "BITWISE_LIBM_PARITY", "TritonBatchInvariantAttentionOp", + "TritonDeterministicAttentionOp", "triton_batch_invariant_attention", "triton_batch_invariant_attention_with_lse", + "triton_deterministic_attention", + "triton_deterministic_attention_backward", + "triton_deterministic_attention_forward", + "triton_deterministic_attention_fp32", + "triton_deterministic_attention_with_lse", ] diff --git a/rl_engine/kernels/ops/triton/attention/deterministic_attn.py b/rl_engine/kernels/ops/triton/attention/deterministic_attn.py new file mode 100644 index 00000000..7914342a --- /dev/null +++ b/rl_engine/kernels/ops/triton/attention/deterministic_attn.py @@ -0,0 +1,878 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2026 RL-Kernel Contributors + +"""Triton port of the deterministic standard-softmax attention core (issue #147). + +This is a *bitwise* re-implementation of ``csrc/cuda/attention/deterministic_attention.cu`` +(exposed as ``_C.deterministic_attention_forward`` / ``_C.deterministic_attention_backward``). +Every reduction here reproduces the C++ kernel's floating-point order exactly, so the +Triton path and the native path return bit-identical ``out``/``lse``/``dQ``/``dK``/``dV`` +for the same inputs on the same device. + +The pipeline mirrors the native one 1:1: + + forward : QK -> masked softmax+LSE -> PV + backward: dP -> softmax backward -> dQ -> dK -> dV + +The three arithmetic contracts that have to be honoured for bitwise parity are: + +1. **Dot products are sequential FMA chains.** The C++ kernels accumulate + ``acc += (float)a[i] * (float)b[i]`` over ascending ``i`` in a single thread, which + hipcc/nvcc contract into a chain of FMAs. Every reduction below loops over the + contraction index one element at a time and uses :func:`tl.fma`, so no vector + tree reduction is ever introduced. That is why the contraction index is the *loop* + and the head dim / output tile is the *vector*: the opposite (and much faster) + arrangement would reassociate the sum. +2. **Row softmax uses the 256-lane partial + binary-tree layout.** The C++ softmax + assigns key ``k`` to thread ``k % 256``, sums each lane's keys in ascending order, + and then folds the 256 partials with ``stride = 128, 64, ... 1``. :func:`_tree_sum_256` + reproduces that fold exactly by repeatedly reshaping to ``(2, n)`` and summing axis 0. +3. **Transcendentals reproduce the vendor libm.** Every Triton exp/log intrinsic lowers + to a bare hardware ``v_exp_f32``/``v_log_f32``, which is ~1 ULP away from the + ``expf``/``logf`` the C++ kernel calls. :func:`_expf` and :func:`_logf` below + re-emit the vendor argument reduction instruction for instruction instead. + +Performance note: like the native reference core this materialises the full FP32 +``[B, Hq, Sq, Skv]`` score matrix and runs scalar-order reductions. It is a +correctness/parity core, not a FlashAttention replacement. +""" + +from __future__ import annotations + +import math +from typing import Optional + +import torch +import triton +import triton.language as tl +from torch.autograd import Function +from torch.autograd.function import once_differentiable + +_HEAD_DIM = 128 + +_IS_ROCM = torch.version.hip is not None + +# --------------------------------------------------------------------------- +# Vendor-exact expf / logf +# --------------------------------------------------------------------------- +# The softmax is the only place this core evaluates a transcendental, and it is +# also the only place where "write the obvious Triton code" is not enough: +# ``tl.exp`` / ``tl.math.exp`` / ``libdevice.exp`` all lower to ``llvm.exp.f32``, +# which the AMDGPU backend expands to a bare ``v_exp_f32``. The HIP ``expf`` the +# C++ kernel calls does a two-term argument reduction around that same hardware +# ``v_exp_f32``, so the two differ by ~1 ULP on most inputs. +# +# The sequences below reproduce, instruction for instruction, what hipcc emits +# for ``expf`` / ``logf`` on gfx9. They are verified bitwise against the vendor +# result over 4M+ random and edge-case inputs, including subnormals, +/-inf and NaN. +# +# ``_fp32_barrier`` is load-bearing: without it LLVM folds ``x * L2E_HI`` and the +# following subtract back into a single FMA, which silently changes the reduced +# argument. Inline asm is opaque to that folding. + +if _IS_ROCM: + from triton.language.extra.hip import libdevice as _ocml + + @triton.jit + def _fp32_barrier(x): + """Opaque move: stops LLVM re-associating across this point.""" + return tl.inline_asm_elementwise( + "v_mov_b32 $0, $1", "=v,v", [x], dtype=tl.float32, is_pure=True, pack=1 + ) + + @triton.jit + def _expf(x): + """Bitwise-exact HIP ``expf`` for FP32.""" + t = _fp32_barrier(x * 1.4426950216293335) + err = tl.fma(x, 1.4426950216293335, -t) + n = _ocml.rint(t) + err = tl.fma(x, 1.925962855864327e-08, err) + r = _fp32_barrier(_fp32_barrier(t - n) + err) + y = _ocml.ldexp(_ocml.exp2(r), n.to(tl.int32)) + # Written as "constant compared against x" so an unordered (NaN) compare + # falls through to the NaN result, matching v_cmp_ngt / v_cmp_nlt. + y = tl.where(-103.2789306640625 > x, 0.0, y) + return tl.where(88.72283935546875 < x, float("inf"), y) + + @triton.jit + def _logf(x): + """Bitwise-exact HIP ``logf`` for FP32.""" + small = x < 1.1754943508222875e-38 + log2_x = _ocml.log2(_ocml.ldexp(x, tl.where(small, 32, 0))) + t = _fp32_barrier(log2_x * 0.6931471228599548) + err = tl.fma(log2_x, 0.6931471228599548, -t) + err = tl.fma(log2_x, 5.769998878690785e-08, err) + r = _fp32_barrier(t + err) + r = tl.where(tl.abs(log2_x) < float("inf"), r, log2_x) + return r - tl.where(small, 22.180709838867188, 0.0) + +else: + + @triton.jit + def _expf(x): + return tl.math.exp(x) + + @triton.jit + def _logf(x): + return tl.math.log(x) + + +#: True when :func:`_expf` / :func:`_logf` reproduce this platform's libm bitwise. +#: The nvcc ``expf``/``logf`` sequences have not been ported, so on CUDA the ops +#: below refuse to run unless the caller opts out explicitly. +BITWISE_LIBM_PARITY = _IS_ROCM + +# Mirrors kSoftmaxThreads in csrc/cuda/attention/deterministic_attention.cu. The +# value is part of the arithmetic contract, not a tuning knob: changing it changes +# which keys land in which partial sum and therefore changes the result bitwise. +_SOFTMAX_LANES = 256 + +# Tile shapes. These only affect scheduling, never the reduction order, because +# every reduction is a per-output-element sequential loop. +_QK_BLOCK_Q = 16 +_QK_BLOCK_K = 64 + + +@triton.jit +def _tree_sum_256(vals): + """Fold 256 partials the way the C++ shared-memory tree reduction does. + + The C++ loop is ``for (stride = 128; stride > 0; stride >>= 1) s[i] += s[i + stride]``. + Reshaping to ``(2, n)`` and summing axis 0 is that same pairing: row-major + ``reshape(2, n)[0] == vals[:n]`` and ``[1] == vals[n:]``, so each step is + ``vals[i] + vals[i + n]``. The steps are written out because a Triton loop + cannot carry a value whose shape changes. + """ + total = tl.sum(tl.reshape(vals, (2, 128)), axis=0) + total = tl.sum(tl.reshape(total, (2, 64)), axis=0) + total = tl.sum(tl.reshape(total, (2, 32)), axis=0) + total = tl.sum(tl.reshape(total, (2, 16)), axis=0) + total = tl.sum(tl.reshape(total, (2, 8)), axis=0) + total = tl.sum(tl.reshape(total, (2, 4)), axis=0) + total = tl.sum(tl.reshape(total, (2, 2)), axis=0) + total = tl.sum(tl.reshape(total, (2, 1)), axis=0) + return tl.sum(total, axis=0) + + +# --------------------------------------------------------------------------- +# Forward +# --------------------------------------------------------------------------- + + +@triton.jit +def _qk_kernel( + q_ptr, + k_ptr, + scores_ptr, + scale, + Hq, + Hkv, + Sq, + Skv, + D: tl.constexpr, + BLOCK_Q: tl.constexpr, + BLOCK_K: tl.constexpr, +): + """scores[b, hq, q, k] = scale * sum_{d ascending} Q[b,hq,q,d] * K[b,kv,k,d].""" + pid_k = tl.program_id(0) + pid_q = tl.program_id(1) + bh = tl.program_id(2) + b = bh // Hq + hq = bh % Hq + kv_head = hq // (Hq // Hkv) + + offs_q = pid_q * BLOCK_Q + tl.arange(0, BLOCK_Q) + offs_k = pid_k * BLOCK_K + tl.arange(0, BLOCK_K) + q_in = offs_q < Sq + k_in = offs_k < Skv + + q_rows = q_ptr + (b * Hq + hq).to(tl.int64) * Sq * D + offs_q.to(tl.int64) * D + k_rows = k_ptr + (b * Hkv + kv_head).to(tl.int64) * Skv * D + offs_k.to(tl.int64) * D + + acc = tl.zeros((BLOCK_Q, BLOCK_K), dtype=tl.float32) + for d in range(0, D): + qv = tl.load(q_rows + d, mask=q_in, other=0.0).to(tl.float32) + kv = tl.load(k_rows + d, mask=k_in, other=0.0).to(tl.float32) + acc = tl.fma(qv[:, None], kv[None, :], acc) + + dst = ( + scores_ptr + + (b * Hq + hq).to(tl.int64) * Sq * Skv + + offs_q.to(tl.int64)[:, None] * Skv + + offs_k[None, :] + ) + tl.store(dst, scale * acc, mask=q_in[:, None] & k_in[None, :]) + + +@triton.jit +def _masked_softmax_lse_kernel( + scores_ptr, + lse_ptr, + mask_ptr, + Hq, + Sq, + Skv, + CAUSAL: tl.constexpr, + HAS_MASK: tl.constexpr, + LANES: tl.constexpr, +): + """Mask, softmax and LSE one ``(b, hq, q)`` row in place, C++ reduction order.""" + row = tl.program_id(0) + b = row // (Hq * Sq) + q = row % Sq + row_base = scores_ptr + row.to(tl.int64) * Skv + + if CAUSAL: + causal_limit = Skv - Sq + q + 1 + else: + causal_limit = Skv + + lane = tl.arange(0, LANES) + neg_inf = float("-inf") + minus_inf_vec = tl.full((LANES,), neg_inf, tl.float32) + + # Phase 1: write -inf over masked entries and take the row max. Max is + # associative and commutative in IEEE-754, so the tree shape is irrelevant here. + lane_max = minus_inf_vec + for start in range(0, Skv, LANES): + cols = start + lane + in_range = cols < Skv + valid = in_range & (cols < causal_limit) + if HAS_MASK: + keep = tl.load(mask_ptr + b.to(tl.int64) * Skv + cols, mask=in_range, other=0) + valid = valid & (keep != 0) + scores = tl.load(row_base + cols, mask=in_range, other=neg_inf) + tl.store(row_base + cols, minus_inf_vec, mask=in_range & ~valid) + lane_max = tl.maximum(lane_max, tl.where(valid, scores, neg_inf)) + row_max = tl.max(lane_max, axis=0) + + # Phase 2: exponentiate in place. Lane ``t`` sums keys t, t+LANES, ... ascending, + # exactly like thread ``t`` in the C++ kernel; masked lanes contribute +0.0, which + # is bitwise neutral for this non-negative sum. + lane_sum = tl.zeros((LANES,), dtype=tl.float32) + for start in range(0, Skv, LANES): + cols = start + lane + in_range = cols < Skv + valid = in_range & (cols < causal_limit) + if HAS_MASK: + keep = tl.load(mask_ptr + b.to(tl.int64) * Skv + cols, mask=in_range, other=0) + valid = valid & (keep != 0) + scores = tl.load(row_base + cols, mask=in_range, other=neg_inf) + probs = tl.where(valid, _expf(scores - row_max), 0.0) + tl.store(row_base + cols, probs, mask=in_range) + lane_sum += probs + row_sum = _tree_sum_256(lane_sum) + + # Phase 3: normalise and emit the LSE. A fully masked row already holds zeros + # from phase 2, so dividing it by 1.0 reproduces the C++ zero-fill branch. + is_empty = row_sum == 0.0 + denom = tl.where(is_empty, 1.0, row_sum) + for start in range(0, Skv, LANES): + cols = start + lane + in_range = cols < Skv + probs = tl.load(row_base + cols, mask=in_range, other=0.0) + tl.store(row_base + cols, probs / denom, mask=in_range) + + lse_val = tl.where(is_empty, neg_inf, row_max + _logf(row_sum)) + tl.store(lse_ptr + row, lse_val) + + +@triton.jit +def _pv_kernel( + p_ptr, + v_ptr, + out_ptr, + Hq, + Hkv, + Sq, + Skv, + D: tl.constexpr, + BLOCK_D: tl.constexpr, +): + """out[b, hq, q, d] = sum_{k ascending} P[b,hq,q,k] * V[b,kv,k,d].""" + row = tl.program_id(0) + bh = row // Sq + b = bh // Hq + hq = bh % Hq + kv_head = hq // (Hq // Hkv) + + offs_d = tl.arange(0, BLOCK_D) + d_in = offs_d < D + p_base = p_ptr + row.to(tl.int64) * Skv + v_base = v_ptr + (b * Hkv + kv_head).to(tl.int64) * Skv * D + + acc = tl.zeros((BLOCK_D,), dtype=tl.float32) + for col in range(0, Skv): + p = tl.load(p_base + col) + vv = tl.load(v_base + col.to(tl.int64) * D + offs_d, mask=d_in, other=0.0).to(tl.float32) + acc = tl.fma(p, vv, acc) + + tl.store(out_ptr + row.to(tl.int64) * D + offs_d, acc, mask=d_in) + + +# --------------------------------------------------------------------------- +# Backward +# --------------------------------------------------------------------------- + + +@triton.jit +def _dp_kernel( + do_ptr, + v_ptr, + dp_ptr, + Hq, + Hkv, + Sq, + Skv, + D: tl.constexpr, + BLOCK_Q: tl.constexpr, + BLOCK_K: tl.constexpr, +): + """dP[b, hq, q, k] = sum_{d ascending} dO[b,hq,q,d] * V[b,kv,k,d].""" + pid_k = tl.program_id(0) + pid_q = tl.program_id(1) + bh = tl.program_id(2) + b = bh // Hq + hq = bh % Hq + kv_head = hq // (Hq // Hkv) + + offs_q = pid_q * BLOCK_Q + tl.arange(0, BLOCK_Q) + offs_k = pid_k * BLOCK_K + tl.arange(0, BLOCK_K) + q_in = offs_q < Sq + k_in = offs_k < Skv + + do_rows = do_ptr + (b * Hq + hq).to(tl.int64) * Sq * D + offs_q.to(tl.int64) * D + v_rows = v_ptr + (b * Hkv + kv_head).to(tl.int64) * Skv * D + offs_k.to(tl.int64) * D + + acc = tl.zeros((BLOCK_Q, BLOCK_K), dtype=tl.float32) + for d in range(0, D): + dov = tl.load(do_rows + d, mask=q_in, other=0.0).to(tl.float32) + vv = tl.load(v_rows + d, mask=k_in, other=0.0).to(tl.float32) + acc = tl.fma(dov[:, None], vv[None, :], acc) + + dst = ( + dp_ptr + + (b * Hq + hq).to(tl.int64) * Sq * Skv + + offs_q.to(tl.int64)[:, None] * Skv + + offs_k[None, :] + ) + tl.store(dst, acc, mask=q_in[:, None] & k_in[None, :]) + + +@triton.jit +def _softmax_backward_kernel( + ds_ptr, + p_ptr, + Skv, + LANES: tl.constexpr, +): + """delta = sum_k dP*P (C++ tree order); then dS = P * (dP - delta) in place.""" + row = tl.program_id(0) + ds_base = ds_ptr + row.to(tl.int64) * Skv + p_base = p_ptr + row.to(tl.int64) * Skv + lane = tl.arange(0, LANES) + + lane_delta = tl.zeros((LANES,), dtype=tl.float32) + for start in range(0, Skv, LANES): + cols = start + lane + in_range = cols < Skv + dp = tl.load(ds_base + cols, mask=in_range, other=0.0) + p = tl.load(p_base + cols, mask=in_range, other=0.0) + lane_delta = tl.fma(dp, p, lane_delta) + delta = _tree_sum_256(lane_delta) + + for start in range(0, Skv, LANES): + cols = start + lane + in_range = cols < Skv + dp = tl.load(ds_base + cols, mask=in_range, other=0.0) + p = tl.load(p_base + cols, mask=in_range, other=0.0) + tl.store(ds_base + cols, p * (dp - delta), mask=in_range) + + +@triton.jit +def _dq_kernel( + ds_ptr, + k_ptr, + dq_ptr, + scale, + Hq, + Hkv, + Sq, + Skv, + D: tl.constexpr, + BLOCK_D: tl.constexpr, +): + """dQ[b, hq, q, d] = scale * sum_{k ascending} dS[b,hq,q,k] * K[b,kv,k,d].""" + row = tl.program_id(0) + bh = row // Sq + b = bh // Hq + hq = bh % Hq + kv_head = hq // (Hq // Hkv) + + offs_d = tl.arange(0, BLOCK_D) + d_in = offs_d < D + ds_base = ds_ptr + row.to(tl.int64) * Skv + k_base = k_ptr + (b * Hkv + kv_head).to(tl.int64) * Skv * D + + acc = tl.zeros((BLOCK_D,), dtype=tl.float32) + for col in range(0, Skv): + ds = tl.load(ds_base + col) + kv = tl.load(k_base + col.to(tl.int64) * D + offs_d, mask=d_in, other=0.0).to(tl.float32) + acc = tl.fma(ds, kv, acc) + + tl.store(dq_ptr + row.to(tl.int64) * D + offs_d, scale * acc, mask=d_in) + + +@triton.jit +def _dk_kernel( + ds_ptr, + q_ptr, + dk_ptr, + scale, + Hq, + Hkv, + Sq, + Skv, + D: tl.constexpr, + BLOCK_D: tl.constexpr, +): + """dK[b, hkv, k, d] = scale * sum_{group head, then q, both ascending} dS * Q.""" + k_idx = tl.program_id(0) + b_hkv = tl.program_id(1) + b = b_hkv // Hkv + hkv = b_hkv % Hkv + group = Hq // Hkv + + offs_d = tl.arange(0, BLOCK_D) + d_in = offs_d < D + + acc = tl.zeros((BLOCK_D,), dtype=tl.float32) + for local in range(0, group): + hq = hkv * group + local + ds_head = ds_ptr + (b * Hq + hq).to(tl.int64) * Sq * Skv + k_idx + q_head = q_ptr + (b * Hq + hq).to(tl.int64) * Sq * D + for qi in range(0, Sq): + ds = tl.load(ds_head + qi.to(tl.int64) * Skv) + qv = tl.load(q_head + qi.to(tl.int64) * D + offs_d, mask=d_in, other=0.0).to(tl.float32) + acc = tl.fma(ds, qv, acc) + + dst = dk_ptr + (b * Hkv + hkv).to(tl.int64) * Skv * D + k_idx.to(tl.int64) * D + offs_d + tl.store(dst, scale * acc, mask=d_in) + + +@triton.jit +def _dv_kernel( + p_ptr, + do_ptr, + dv_ptr, + Hq, + Hkv, + Sq, + Skv, + D: tl.constexpr, + BLOCK_D: tl.constexpr, +): + """dV[b, hkv, k, d] = sum_{group head, then q, both ascending} P * dO.""" + k_idx = tl.program_id(0) + b_hkv = tl.program_id(1) + b = b_hkv // Hkv + hkv = b_hkv % Hkv + group = Hq // Hkv + + offs_d = tl.arange(0, BLOCK_D) + d_in = offs_d < D + + acc = tl.zeros((BLOCK_D,), dtype=tl.float32) + for local in range(0, group): + hq = hkv * group + local + p_head = p_ptr + (b * Hq + hq).to(tl.int64) * Sq * Skv + k_idx + do_head = do_ptr + (b * Hq + hq).to(tl.int64) * Sq * D + for qi in range(0, Sq): + p = tl.load(p_head + qi.to(tl.int64) * Skv) + dov = tl.load(do_head + qi.to(tl.int64) * D + offs_d, mask=d_in, other=0.0).to( + tl.float32 + ) + acc = tl.fma(p, dov, acc) + + dst = dv_ptr + (b * Hkv + hkv).to(tl.int64) * Skv * D + k_idx.to(tl.int64) * D + offs_d + tl.store(dst, acc, mask=d_in) + + +# --------------------------------------------------------------------------- +# Launchers +# --------------------------------------------------------------------------- + + +def _dummy_mask(reference: torch.Tensor) -> torch.Tensor: + return reference.new_empty((1,), dtype=torch.bool) + + +def triton_deterministic_attention_forward( + q: torch.Tensor, + k: torch.Tensor, + v: torch.Tensor, + causal: bool, + scale: float, + key_padding_mask: Optional[torch.Tensor], + output_fp32: bool, +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + """Return ``(out, lse, P)`` matching ``_C.deterministic_attention_forward`` bitwise.""" + q = q.contiguous() + k = k.contiguous() + v = v.contiguous() + mask = key_padding_mask.contiguous() if key_padding_mask is not None else None + + b, hq, sq, d = q.shape + hkv, skv = k.shape[1], k.shape[2] + + scores = torch.empty((b, hq, sq, skv), device=q.device, dtype=torch.float32) + lse = torch.empty((b, hq, sq), device=q.device, dtype=torch.float32) + out = torch.empty_like(q, dtype=torch.float32 if output_fp32 else q.dtype) + + _qk_kernel[(triton.cdiv(skv, _QK_BLOCK_K), triton.cdiv(sq, _QK_BLOCK_Q), b * hq)]( + q, + k, + scores, + float(scale), + hq, + hkv, + sq, + skv, + D=d, + BLOCK_Q=_QK_BLOCK_Q, + BLOCK_K=_QK_BLOCK_K, + num_warps=4, + ) + _masked_softmax_lse_kernel[(b * hq * sq,)]( + scores, + lse, + mask if mask is not None else _dummy_mask(q), + hq, + sq, + skv, + CAUSAL=causal, + HAS_MASK=mask is not None, + LANES=_SOFTMAX_LANES, + num_warps=4, + ) + _pv_kernel[(b * hq * sq,)]( + scores, + v, + out, + hq, + hkv, + sq, + skv, + D=d, + BLOCK_D=d, + num_warps=4, + ) + return out, lse, scores + + +def triton_deterministic_attention_backward( + grad_output: torch.Tensor, + q: torch.Tensor, + k: torch.Tensor, + v: torch.Tensor, + p: torch.Tensor, + scale: float, +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + """Return ``(dQ, dK, dV)`` matching ``_C.deterministic_attention_backward`` bitwise.""" + do = grad_output.contiguous() + q = q.contiguous() + k = k.contiguous() + v = v.contiguous() + p = p.contiguous() + + b, hq, sq, d = q.shape + hkv, skv = k.shape[1], k.shape[2] + + # dS reuses the dP buffer exactly like the native backward does. + ds = torch.empty((b, hq, sq, skv), device=q.device, dtype=torch.float32) + dq = torch.empty_like(q) + dk = torch.empty_like(k) + dv = torch.empty_like(v) + + _dp_kernel[(triton.cdiv(skv, _QK_BLOCK_K), triton.cdiv(sq, _QK_BLOCK_Q), b * hq)]( + do, + v, + ds, + hq, + hkv, + sq, + skv, + D=d, + BLOCK_Q=_QK_BLOCK_Q, + BLOCK_K=_QK_BLOCK_K, + num_warps=4, + ) + _softmax_backward_kernel[(b * hq * sq,)]( + ds, + p, + skv, + LANES=_SOFTMAX_LANES, + num_warps=4, + ) + _dq_kernel[(b * hq * sq,)]( + ds, + k, + dq, + float(scale), + hq, + hkv, + sq, + skv, + D=d, + BLOCK_D=d, + num_warps=4, + ) + _dk_kernel[(skv, b * hkv)]( + ds, + q, + dk, + float(scale), + hq, + hkv, + sq, + skv, + D=d, + BLOCK_D=d, + num_warps=4, + ) + _dv_kernel[(skv, b * hkv)]( + p, + do, + dv, + hq, + hkv, + sq, + skv, + D=d, + BLOCK_D=d, + num_warps=4, + ) + return dq, dk, dv + + +class _TritonDeterministicAttentionFn(Function): + @staticmethod + def forward( + ctx, + q: torch.Tensor, + k: torch.Tensor, + v: torch.Tensor, + causal: bool, + scale: float, + key_padding_mask: Optional[torch.Tensor], + output_fp32: bool, + ) -> tuple[torch.Tensor, torch.Tensor]: + q_c = q.contiguous() + k_c = k.contiguous() + v_c = v.contiguous() + mask_c = key_padding_mask.contiguous() if key_padding_mask is not None else None + + out, lse, p = triton_deterministic_attention_forward( + q_c, k_c, v_c, causal, float(scale), mask_c, output_fp32 + ) + + ctx.save_for_backward(q_c, k_c, v_c, p, mask_c) + ctx.causal = causal + ctx.scale = scale + ctx.mark_non_differentiable(lse) + + return out, lse + + @staticmethod + @once_differentiable + def backward(ctx, grad_out: torch.Tensor, grad_lse: torch.Tensor): + q_c, k_c, v_c, p, _mask_c = ctx.saved_tensors + + if grad_out.dtype != q_c.dtype: + grad_out = grad_out.to(q_c.dtype) + dq, dk, dv = triton_deterministic_attention_backward( + grad_out.contiguous(), q_c, k_c, v_c, p, float(ctx.scale) + ) + return dq, dk, dv, None, None, None, None + + +class TritonDeterministicAttentionOp: + """Triton twin of :class:`DeterministicAttentionOp`, bitwise identical to it. + + The public surface matches the native op so either can be dropped into the + strict-attention harness. Validation is duplicated rather than imported so the + Triton path stays usable when the native extension is not built. + """ + + backend_id = "rlkernel.triton.deterministic_attention" + + def __init__(self, *, require_bitwise_libm: bool = True) -> None: + """``require_bitwise_libm=False`` trades bitwise parity for portability. + + The softmax needs a bitwise-exact ``expf``/``logf``; only the HIP sequences + are ported (see :data:`BITWISE_LIBM_PARITY`). Opting out keeps the kernel + deterministic and batch-invariant but no longer bit-identical to the + native core, so it is never the default. + """ + if require_bitwise_libm and not BITWISE_LIBM_PARITY: + raise RuntimeError( + "Triton deterministic attention is bitwise-identical to " + "_C.deterministic_attention_* only on ROCm: the nvcc expf/logf " + "argument reduction has not been ported. Construct with " + "require_bitwise_libm=False to run the non-bitwise fallback." + ) + self.bitwise_libm = BITWISE_LIBM_PARITY + + def __call__( + self, + q: torch.Tensor, + k: torch.Tensor, + v: torch.Tensor, + *, + causal: bool = True, + scale: Optional[float] = None, + key_padding_mask: Optional[torch.Tensor] = None, + ) -> torch.Tensor: + return self.forward(q, k, v, causal=causal, scale=scale, key_padding_mask=key_padding_mask) + + def forward( + self, + q: torch.Tensor, + k: torch.Tensor, + v: torch.Tensor, + *, + causal: bool = True, + scale: Optional[float] = None, + key_padding_mask: Optional[torch.Tensor] = None, + ) -> torch.Tensor: + out, _lse = self.forward_with_lse( + q, k, v, causal=causal, scale=scale, key_padding_mask=key_padding_mask + ) + return out + + def forward_with_lse( + self, + q: torch.Tensor, + k: torch.Tensor, + v: torch.Tensor, + *, + causal: bool = True, + scale: Optional[float] = None, + key_padding_mask: Optional[torch.Tensor] = None, + ) -> tuple[torch.Tensor, torch.Tensor]: + self._validate_inputs(q, k, v, key_padding_mask) + resolved_scale = scale if scale is not None else (1.0 / math.sqrt(q.shape[-1])) + return _TritonDeterministicAttentionFn.apply( + q, k, v, causal, resolved_scale, key_padding_mask, False + ) + + def forward_fp32( + self, + q: torch.Tensor, + k: torch.Tensor, + v: torch.Tensor, + *, + causal: bool = True, + scale: Optional[float] = None, + key_padding_mask: Optional[torch.Tensor] = None, + ) -> torch.Tensor: + self._validate_inputs(q, k, v, key_padding_mask) + resolved_scale = scale if scale is not None else (1.0 / math.sqrt(q.shape[-1])) + out, _lse = _TritonDeterministicAttentionFn.apply( + q, k, v, causal, resolved_scale, key_padding_mask, True + ) + return out + + @staticmethod + def _validate_inputs( + q: torch.Tensor, + k: torch.Tensor, + v: torch.Tensor, + key_padding_mask: Optional[torch.Tensor], + ) -> None: + if q.dim() != 4 or k.dim() != 4 or v.dim() != 4: + raise ValueError( + f"q/k/v must be 4-D [B, H, S, D], got q={tuple(q.shape)}, " + f"k={tuple(k.shape)}, v={tuple(v.shape)}" + ) + b, hq, sq, d = q.shape + hkv, skv = k.shape[1], k.shape[2] + if k.shape[0] != b or v.shape[0] != b: + raise ValueError("batch size mismatch between q/k/v") + if v.shape[1] != hkv or v.shape[2] != skv or k.shape[3] != d or v.shape[3] != d: + raise ValueError( + f"k/v shape mismatch: k={tuple(k.shape)}, v={tuple(v.shape)}, " + f"expected k/v [B={b}, Hkv, Skv, D={d}]" + ) + if d != _HEAD_DIM: + raise ValueError(f"head dim D must be {_HEAD_DIM}, got {d}") + if hq % hkv != 0: + raise ValueError(f"Hq={hq} not divisible by Hkv={hkv} (GQA group)") + if q.dtype not in (torch.float16, torch.bfloat16): + raise ValueError(f"only FP16/BF16 supported, got {q.dtype}") + if k.dtype != q.dtype or v.dtype != q.dtype: + raise ValueError("q, k, v must share the same dtype") + if not (q.is_cuda and k.is_cuda and v.is_cuda): + raise ValueError("q, k, v must be GPU tensors") + if key_padding_mask is not None: + if key_padding_mask.dtype != torch.bool: + raise ValueError("key_padding_mask must be bool") + if key_padding_mask.shape != (b, skv): + raise ValueError( + f"key_padding_mask must be [B, Skv]=[{b}, {skv}], " + f"got {tuple(key_padding_mask.shape)}" + ) + if sq < 1 or skv < 1: + raise ValueError(f"Sq and Skv must be positive, got Sq={sq}, Skv={skv}") + + +def triton_deterministic_attention( + q: torch.Tensor, + k: torch.Tensor, + v: torch.Tensor, + *, + causal: bool = True, + scale: Optional[float] = None, + key_padding_mask: Optional[torch.Tensor] = None, +) -> torch.Tensor: + return TritonDeterministicAttentionOp().forward( + q, k, v, causal=causal, scale=scale, key_padding_mask=key_padding_mask + ) + + +def triton_deterministic_attention_with_lse( + q: torch.Tensor, + k: torch.Tensor, + v: torch.Tensor, + *, + causal: bool = True, + scale: Optional[float] = None, + key_padding_mask: Optional[torch.Tensor] = None, +) -> tuple[torch.Tensor, torch.Tensor]: + return TritonDeterministicAttentionOp().forward_with_lse( + q, k, v, causal=causal, scale=scale, key_padding_mask=key_padding_mask + ) + + +def triton_deterministic_attention_fp32( + q: torch.Tensor, + k: torch.Tensor, + v: torch.Tensor, + *, + causal: bool = True, + scale: Optional[float] = None, + key_padding_mask: Optional[torch.Tensor] = None, +) -> torch.Tensor: + return TritonDeterministicAttentionOp().forward_fp32( + q, k, v, causal=causal, scale=scale, key_padding_mask=key_padding_mask + ) + + +__all__ = [ + "TritonDeterministicAttentionOp", + "triton_deterministic_attention", + "triton_deterministic_attention_backward", + "triton_deterministic_attention_forward", + "triton_deterministic_attention_fp32", + "triton_deterministic_attention_with_lse", +] diff --git a/tests/test_triton_deterministic_attention.py b/tests/test_triton_deterministic_attention.py new file mode 100644 index 00000000..8e87ecf9 --- /dev/null +++ b/tests/test_triton_deterministic_attention.py @@ -0,0 +1,284 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2026 RL-Kernel Contributors + +"""Bitwise parity between the Triton and native deterministic Attention cores. + +The Triton core in ``rl_engine.kernels.ops.triton.attention.deterministic_attn`` is a +port of ``csrc/cuda/attention/deterministic_attention.cu``. Its contract is stronger +than "numerically close": every tensor it returns must be bit-identical to the native +kernel's. These tests pin that contract, plus the two properties the port depends on: +the vendor-exact ``expf``/``logf`` helpers, and batch invariance. +""" + +import math + +import pytest +import torch + +from rl_engine.platforms.device import device_ctx + +IS_ROCM = device_ctx.is_rocm +IS_GPU = device_ctx.device_type == "cuda" or IS_ROCM + +pytestmark = pytest.mark.skipif(not IS_GPU, reason="CUDA/ROCm GPU not available") + +try: + from rl_engine.kernels.ops.base import _C, _EXT_AVAILABLE + from rl_engine.kernels.ops.cuda.attention.deterministic_attn import ( + DeterministicAttentionOp, + ) + from rl_engine.kernels.ops.triton.attention.deterministic_attn import ( + BITWISE_LIBM_PARITY, + TritonDeterministicAttentionOp, + triton_deterministic_attention_backward, + triton_deterministic_attention_forward, + ) + + _IMPORTED = True +except (ImportError, RuntimeError): # pragma: no cover - import guard + _IMPORTED = False + +_NATIVE = _IMPORTED and _EXT_AVAILABLE and hasattr(_C, "deterministic_attention_forward") + +needs_native = pytest.mark.skipif( + not _NATIVE, reason="native deterministic attention kernel not built" +) +needs_bitwise_libm = pytest.mark.skipif( + not (_IMPORTED and BITWISE_LIBM_PARITY), + reason="bitwise expf/logf sequence is only ported for ROCm", +) + +DEVICE = device_ctx.device +D = 128 + +# (B, Hq, Hkv, Sq, Skv, causal, mask_kind, scale) +_CASES = [ + (1, 1, 1, 1, 1, True, None, None), + (1, 2, 2, 1, 64, True, None, None), # decode step + (1, 8, 2, 64, 64, True, None, None), # GQA, group 4 + (2, 4, 1, 128, 128, True, None, None), # MQA + (1, 2, 2, 256, 256, True, None, None), # exactly one softmax lane chunk + (1, 2, 2, 257, 257, True, None, None), # one past the chunk boundary + (1, 2, 2, 100, 512, False, None, None), # two full lane chunks + (2, 4, 2, 64, 700, True, None, None), # ragged multi-chunk + (1, 2, 2, 32, 32, True, "right", None), + (1, 2, 2, 32, 32, False, "left", None), + (2, 2, 2, 16, 300, True, "right", None), + (2, 2, 2, 8, 8, True, "allfalse", None), # fully masked row -> lse == -inf + (1, 2, 2, 16, 16, True, None, 0.0), + (1, 2, 2, 16, 16, True, None, 3.7), + (1, 2, 2, 16, 16, False, None, -1.25), +] + + +def _make_inputs(case, dtype, seed): + b, hq, hkv, sq, skv, causal, mask_kind, scale = case + gen = torch.Generator(device=DEVICE).manual_seed(seed) + q = torch.randn(b, hq, sq, D, device=DEVICE, dtype=dtype, generator=gen) + k = torch.randn(b, hkv, skv, D, device=DEVICE, dtype=dtype, generator=gen) + v = torch.randn(b, hkv, skv, D, device=DEVICE, dtype=dtype, generator=gen) + + mask = None + if mask_kind is not None: + mask = torch.ones(b, skv, device=DEVICE, dtype=torch.bool) + if mask_kind == "right": + mask[:, skv // 2 :] = False + elif mask_kind == "left": + mask[:, : skv // 3] = False + elif mask_kind == "allfalse": + mask[0, :] = False + else: # pragma: no cover - guards the parametrisation itself + raise AssertionError(f"unknown mask kind {mask_kind}") + + resolved_scale = 1.0 / math.sqrt(D) if scale is None else scale + return q, k, v, causal, resolved_scale, mask + + +def _case_id(case): + b, hq, hkv, sq, skv, causal, mask_kind, scale = case + return f"b{b}_hq{hq}_hkv{hkv}_sq{sq}_skv{skv}_causal{int(causal)}_{mask_kind}_{scale}" + + +@needs_native +@needs_bitwise_libm +@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16], ids=["bf16", "fp16"]) +@pytest.mark.parametrize("case", _CASES, ids=[_case_id(c) for c in _CASES]) +def test_triton_forward_is_bitwise_identical_to_native(case, dtype): + q, k, v, causal, scale, mask = _make_inputs(case, dtype, seed=11) + + ref_out, ref_lse, ref_p = _C.deterministic_attention_forward(q, k, v, causal, scale, mask) + out, lse, p = triton_deterministic_attention_forward(q, k, v, causal, scale, mask, False) + + assert torch.equal(out, ref_out) + assert torch.equal(lse, ref_lse) + assert torch.equal(p, ref_p) + + +@needs_native +@needs_bitwise_libm +@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16], ids=["bf16", "fp16"]) +@pytest.mark.parametrize("case", _CASES, ids=[_case_id(c) for c in _CASES]) +def test_triton_backward_is_bitwise_identical_to_native(case, dtype): + q, k, v, causal, scale, mask = _make_inputs(case, dtype, seed=12) + + _ref_out, _ref_lse, ref_p = _C.deterministic_attention_forward(q, k, v, causal, scale, mask) + _out, _lse, p = triton_deterministic_attention_forward(q, k, v, causal, scale, mask, False) + grad_out = torch.randn_like(q) + + ref_dq, ref_dk, ref_dv = _C.deterministic_attention_backward( + grad_out, q, k, v, ref_p, causal, scale, mask + ) + dq, dk, dv = triton_deterministic_attention_backward(grad_out, q, k, v, p, scale) + + assert torch.equal(dq, ref_dq) + assert torch.equal(dk, ref_dk) + assert torch.equal(dv, ref_dv) + + +@needs_native +@needs_bitwise_libm +@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16], ids=["bf16", "fp16"]) +def test_triton_autograd_matches_native_autograd_bitwise(dtype): + b, hq, hkv, sq, skv = 2, 8, 2, 96, 300 + gen = torch.Generator(device=DEVICE).manual_seed(13) + base = ( + torch.randn(b, hq, sq, D, device=DEVICE, dtype=dtype, generator=gen), + torch.randn(b, hkv, skv, D, device=DEVICE, dtype=dtype, generator=gen), + torch.randn(b, hkv, skv, D, device=DEVICE, dtype=dtype, generator=gen), + ) + grad_out = torch.randn(b, hq, sq, D, device=DEVICE, dtype=dtype, generator=gen) + + results = [] + for op in (DeterministicAttentionOp(), TritonDeterministicAttentionOp()): + tensors = [t.clone().requires_grad_(True) for t in base] + out, lse = op.forward_with_lse(*tensors, causal=True) + out.backward(grad_out) + results.append((out.detach(), lse, [t.grad for t in tensors])) + + native, triton_result = results + assert torch.equal(triton_result[0], native[0]) + assert torch.equal(triton_result[1], native[1]) + for triton_grad, native_grad in zip(triton_result[2], native[2]): + assert torch.equal(triton_grad, native_grad) + + +@needs_bitwise_libm +def test_triton_fp32_output_downcasts_to_the_native_dtype_result(): + """``output_fp32=True`` must expose the same accumulator the bf16 path rounds.""" + q, k, v, causal, scale, mask = _make_inputs(_CASES[6], torch.bfloat16, seed=14) + + out, _lse, _p = triton_deterministic_attention_forward(q, k, v, causal, scale, mask, False) + out_fp32, _lse32, _p32 = triton_deterministic_attention_forward( + q, k, v, causal, scale, mask, True + ) + + assert out_fp32.dtype is torch.float32 + assert torch.equal(out_fp32.to(torch.bfloat16), out) + + +@needs_bitwise_libm +def test_triton_fully_masked_row_is_zero_with_neg_inf_lse(): + q, k, v, causal, scale, mask = _make_inputs(_CASES[11], torch.bfloat16, seed=15) + + out, lse, p = triton_deterministic_attention_forward(q, k, v, causal, scale, mask, False) + + assert torch.equal(out[0], torch.zeros_like(out[0])) + assert torch.equal(p[0], torch.zeros_like(p[0])) + assert torch.isneginf(lse[0]).all() + assert torch.isfinite(lse[1]).all() + + +@needs_bitwise_libm +@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16], ids=["bf16", "fp16"]) +def test_triton_batch_slice_is_bitwise_invariant(dtype): + """A row's result must not depend on what else was in the batch.""" + b, hq, hkv, sq, skv = 4, 4, 2, 48, 192 + gen = torch.Generator(device=DEVICE).manual_seed(16) + q = torch.randn(b, hq, sq, D, device=DEVICE, dtype=dtype, generator=gen) + k = torch.randn(b, hkv, skv, D, device=DEVICE, dtype=dtype, generator=gen) + v = torch.randn(b, hkv, skv, D, device=DEVICE, dtype=dtype, generator=gen) + scale = 1.0 / math.sqrt(D) + + batched, batched_lse, _ = triton_deterministic_attention_forward( + q, k, v, True, scale, None, False + ) + single, single_lse, _ = triton_deterministic_attention_forward( + q[2:3], k[2:3], v[2:3], True, scale, None, False + ) + + assert torch.equal(single[0], batched[2]) + assert torch.equal(single_lse[0], batched_lse[2]) + + +@needs_bitwise_libm +def test_expf_and_logf_match_the_vendor_libm_bitwise(): + """The softmax parity rests on these two helpers; pin them independently.""" + import triton + + import triton.language as tl # isort: skip + from rl_engine.kernels.ops.triton.attention import deterministic_attn as mod + + @triton.jit + def _exp_probe(x_ptr, out_ptr, n_elem, EXPF: tl.constexpr): + offs = tl.program_id(0) * 256 + tl.arange(0, 256) + keep = offs < n_elem + value = tl.load(x_ptr + offs, mask=keep, other=0.0) + tl.store(out_ptr + offs, EXPF(value), mask=keep) + + gen = torch.Generator(device=DEVICE).manual_seed(17) + edge = torch.tensor( + [0.0, -0.0, 1.0, -1.0, 88.72283935546875, 88.73, -103.2789306640625, -104.0], + device=DEVICE, + dtype=torch.float32, + ) + xs = torch.cat( + [torch.rand(1 << 18, device=DEVICE, generator=gen) * 240.0 - 130.0, edge] + ).contiguous() + got = torch.empty_like(xs) + _exp_probe[(triton.cdiv(xs.numel(), 256),)](xs, got, xs.numel(), EXPF=mod._expf) + assert torch.equal(got, torch.exp(xs)) + + positives = torch.cat( + [ + torch.rand(1 << 18, device=DEVICE, generator=gen) * 1e3, + torch.rand(1 << 12, device=DEVICE, generator=gen) * 1e-38, # subnormal inputs + torch.tensor([1.0, 1e-45, 3.4e38], device=DEVICE), + ] + ).contiguous() + got = torch.empty_like(positives) + _exp_probe[(triton.cdiv(positives.numel(), 256),)]( + positives, got, positives.numel(), EXPF=mod._logf + ) + assert torch.equal(got, torch.log(positives)) + + +def test_op_refuses_to_run_without_a_bitwise_libm(): + """On a platform with no ported expf/logf the op must fail loudly, not silently.""" + if BITWISE_LIBM_PARITY: + op = TritonDeterministicAttentionOp() + assert op.bitwise_libm is True + else: # pragma: no cover - exercised on CUDA + with pytest.raises(RuntimeError, match="bitwise-identical"): + TritonDeterministicAttentionOp() + assert TritonDeterministicAttentionOp(require_bitwise_libm=False).bitwise_libm is False + + +@needs_bitwise_libm +@pytest.mark.parametrize( + "kwargs, message", + [ + ({"head_dim": 64}, "head dim D must be 128"), + ({"dtype": torch.float32}, "only FP16/BF16 supported"), + ({"gqa_mismatch": True}, "not divisible"), + ], +) +def test_triton_op_validation_mirrors_the_native_op(kwargs, message): + head_dim = kwargs.get("head_dim", D) + dtype = kwargs.get("dtype", torch.bfloat16) + hkv = 3 if kwargs.get("gqa_mismatch") else 2 + q = torch.randn(1, 2, 8, head_dim, device=DEVICE, dtype=dtype) + k = torch.randn(1, hkv, 8, head_dim, device=DEVICE, dtype=dtype) + v = torch.randn(1, hkv, 8, head_dim, device=DEVICE, dtype=dtype) + + with pytest.raises(ValueError, match=message): + TritonDeterministicAttentionOp().forward(q, k, v, causal=True) From cc76c48c0bee050a22a713877befe8c3fb812e3f Mon Sep 17 00:00:00 2001 From: inaniloquentee Date: Sat, 29 Aug 2026 07:48:25 +0000 Subject: [PATCH 21/35] bench(attention): add WS2 strict ROCm Attention report on MI300X 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) Claude-Session: https://claude.ai/code/session_01824WkgMDxtBKD4Ex2NkfYA --- benchmarks/benchmark_ws2_rocm_attention.py | 1536 ++++++++++++++++ .../results/ws2_rocm_mi300x/PR_DESCRIPTION.md | 292 +++ .../distributed_cp_latency.png | Bin 0 -> 46377 bytes benchmarks/results/ws2_rocm_mi300x/report.md | 244 +++ .../results/ws2_rocm_mi300x/results.json | 1567 +++++++++++++++++ .../ws2_rocm_mi300x/single_gpu_grid.png | Bin 0 -> 288104 bytes .../ws2_rocm_mi300x/single_gpu_latency.png | Bin 0 -> 132823 bytes .../ws2_rocm_mi300x/single_gpu_memory.png | Bin 0 -> 131593 bytes .../ws2_rocm_mi300x/tp_degree_invariance.png | Bin 0 -> 103099 bytes 9 files changed, 3639 insertions(+) create mode 100644 benchmarks/benchmark_ws2_rocm_attention.py create mode 100644 benchmarks/results/ws2_rocm_mi300x/PR_DESCRIPTION.md create mode 100644 benchmarks/results/ws2_rocm_mi300x/distributed_cp_latency.png create mode 100644 benchmarks/results/ws2_rocm_mi300x/report.md create mode 100644 benchmarks/results/ws2_rocm_mi300x/results.json create mode 100644 benchmarks/results/ws2_rocm_mi300x/single_gpu_grid.png create mode 100644 benchmarks/results/ws2_rocm_mi300x/single_gpu_latency.png create mode 100644 benchmarks/results/ws2_rocm_mi300x/single_gpu_memory.png create mode 100644 benchmarks/results/ws2_rocm_mi300x/tp_degree_invariance.png diff --git a/benchmarks/benchmark_ws2_rocm_attention.py b/benchmarks/benchmark_ws2_rocm_attention.py new file mode 100644 index 00000000..b0f409ed --- /dev/null +++ b/benchmarks/benchmark_ws2_rocm_attention.py @@ -0,0 +1,1536 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2026 RL-Kernel Contributors + +"""WS2 strict ROCm Attention performance and bitwise-parity benchmark. + +Operator-only. No model checkpoint or serving engine is loaded; the shapes are +Qwen3-8B's attention shapes (``Hq=32``, ``Hkv=8``, ``D=128``). + +Measurement matrix and presentation follow PR #325 (`benchmark_rocm_ffn.py`) and +PR #328 (`benchmark_rocm_logp.py`): the timing/accuracy helpers, the spawned +distributed world, and the figure style are taken from those scripts so the three +reports can be read side by side. + +Paths measured: + +- ``sdpa`` PyTorch ``scaled_dot_product_attention``. Speed baseline only, + exactly as PR #325 uses upstream ``Qwen3MLP`` at TP=1: no + accuracy claim is mixed into the speed comparison. +- ``strict-aiter`` ``StrictRocmAiterCKAttentionCore`` — the ROCm production core + (AITER CK dense MHA, non-split API). +- ``reference-hip`` ``_C.deterministic_attention_forward/backward`` — the materializing + FP32 reference core, hipified from the shared ``.cu``. +- ``triton-bitwise`` ``TritonDeterministicAttentionOp`` — the Triton port whose + contract is bit-identity with ``reference-hip``. + +The headline column is ``triton-bitwise`` versus ``reference-hip``: acceptance is +0 mismatched elements on out, lse, dQ, dK and dV. +""" + +from __future__ import annotations + +import argparse +import json +import math +import multiprocessing as mp +import os +import platform +import queue +import statistics +import tempfile +import traceback +from datetime import timedelta +from pathlib import Path +from typing import Any, Callable + +import torch +import torch.distributed as dist + +QWEN3_8B_Q_HEADS = 32 +QWEN3_8B_KV_HEADS = 8 +QWEN3_8B_HEAD_DIM = 128 + +DEFAULT_SEQ_LENS = (512, 1024, 2048, 4096) +DEFAULT_TP_DEGREES = (2, 4, 8) +# (label, tp_world_size, cp_world_size, replicas) -- world_size = tp * cp * replicas. +# Replicas run independent CP groups side by side, which is how PR #319 exercised +# 8 ranks at TP=2/CP=2. +DISTRIBUTED_TOPOLOGIES = ( + ("tp1_cp2", 1, 2, 1), + ("tp2_cp2", 2, 2, 1), + ("tp1_cp4", 1, 4, 1), + ("tp2_cp2_x2", 2, 2, 2), + ("tp2_cp4", 2, 4, 1), + ("tp1_cp8", 1, 8, 1), +) + + +# --------------------------------------------------------------------------- +# Measurement helpers (PR #328 benchmark_rocm_logp.py) +# --------------------------------------------------------------------------- + + +def _percentile(values: list[float], percentile: float) -> float: + ordered = sorted(values) + if not ordered: + return float("nan") + position = (len(ordered) - 1) * percentile + lower = math.floor(position) + upper = math.ceil(position) + if lower == upper: + return ordered[lower] + weight = position - lower + return ordered[lower] * (1.0 - weight) + ordered[upper] * weight + + +def _summary_ms(values: list[float]) -> dict[str, float]: + return { + "median_ms": statistics.median(values), + "p95_ms": _percentile(values, 0.95), + "min_ms": min(values), + "max_ms": max(values), + } + + +def _relative_l2(actual: torch.Tensor, expected: torch.Tensor) -> float: + actual_float = actual.detach().double() + expected_float = expected.detach().double() + denominator = torch.linalg.vector_norm(expected_float) + if denominator.item() == 0.0: + return float(torch.linalg.vector_norm(actual_float - expected_float).item()) + return float((torch.linalg.vector_norm(actual_float - expected_float) / denominator).item()) + + +def _accuracy(actual: torch.Tensor, expected: torch.Tensor) -> dict[str, float]: + difference = actual.detach().double() - expected.detach().double() + return { + "max_abs": float(difference.abs().max().item()) if difference.numel() else 0.0, + "relative_l2": _relative_l2(actual, expected), + } + + +def _bitwise_equal(a: torch.Tensor, b: torch.Tensor) -> bool: + return a.shape == b.shape and a.dtype == b.dtype and bool(torch.equal(a, b)) + + +def _mismatch_count(a: torch.Tensor, b: torch.Tensor) -> int: + if a.shape != b.shape: + return -1 + return int((a != b).sum().item()) + + +def _gpu_event_samples(function: Callable[[], Any], *, warmup: int, samples: int) -> list[float]: + for _ in range(warmup): + function() + torch.cuda.synchronize() + events = [] + for _ in range(samples): + start = torch.cuda.Event(enable_timing=True) + end = torch.cuda.Event(enable_timing=True) + start.record() + function() + end.record() + events.append((start, end)) + torch.cuda.synchronize() + return [float(start.elapsed_time(end)) for start, end in events] + + +def _peak_memory_mib(function: Callable[[], Any]) -> float: + """Peak memory used by one call, above what was live before it.""" + torch.cuda.synchronize() + torch.cuda.empty_cache() + torch.cuda.reset_peak_memory_stats() + baseline = torch.cuda.memory_allocated() + function() + torch.cuda.synchronize() + return float((torch.cuda.max_memory_allocated() - baseline) / (1024.0 * 1024.0)) + + +# --------------------------------------------------------------------------- +# Attention paths +# --------------------------------------------------------------------------- + + +def _seeded_qkv( + batch: int, + q_heads: int, + kv_heads: int, + seq_len: int, + head_dim: int, + dtype: torch.dtype, + device: torch.device, + *, + seed: int, +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + generator = torch.Generator(device=device).manual_seed(seed) + q = torch.randn( + batch, q_heads, seq_len, head_dim, device=device, dtype=dtype, generator=generator + ) + k = torch.randn( + batch, kv_heads, seq_len, head_dim, device=device, dtype=dtype, generator=generator + ) + v = torch.randn( + batch, kv_heads, seq_len, head_dim, device=device, dtype=dtype, generator=generator + ) + return q, k, v + + +def _positions(batch: int, seq_len: int, device: torch.device) -> torch.Tensor: + return torch.arange(seq_len, device=device, dtype=torch.int32).unsqueeze(0).expand(batch, -1) + + +def _fp64_oracle( + q: torch.Tensor, k: torch.Tensor, v: torch.Tensor, *, causal: bool, scale: float +) -> tuple[torch.Tensor, torch.Tensor]: + """Reference (out, lse) in FP64 from the BF16/FP16-rounded inputs.""" + q64 = q.double() + k64 = k.double() + v64 = v.double() + group = q.size(1) // k.size(1) + k64 = k64.repeat_interleave(group, dim=1) + v64 = v64.repeat_interleave(group, dim=1) + scores = torch.matmul(q64, k64.transpose(-1, -2)) * scale + if causal: + sq, skv = q.size(2), k.size(2) + offset = skv - sq + mask = torch.arange(skv, device=q.device)[None, :] > ( + torch.arange(sq, device=q.device)[:, None] + offset + ) + scores = scores.masked_fill(mask, float("-inf")) + lse = torch.logsumexp(scores, dim=-1) + probs = torch.softmax(scores, dim=-1) + return torch.matmul(probs, v64), lse + + +def _sdpa_forward(q, k, v, *, causal, scale): + group = q.size(1) // k.size(1) + return torch.nn.functional.scaled_dot_product_attention( + q, + k.repeat_interleave(group, dim=1), + v.repeat_interleave(group, dim=1), + is_causal=causal, + scale=scale, + ) + + +class _Paths: + """Lazily constructed attention paths, so a missing backend skips one row.""" + + def __init__(self) -> None: + self.errors: dict[str, str] = {} + self.strict = self._try("strict-aiter", self._make_strict) + self.reference = self._try("reference-hip", self._make_reference) + self.triton = self._try("triton-bitwise", self._make_triton) + + def _try(self, name: str, factory: Callable[[], Any]) -> Any: + try: + return factory() + except Exception as exc: # noqa: BLE001 - a missing backend is a reported row + self.errors[name] = f"{type(exc).__name__}: {exc}" + return None + + @staticmethod + def _make_strict(): + from rl_engine.kernels.ops.rocm.attention.flash_attn import StrictRocmAiterCKAttentionCore + + return StrictRocmAiterCKAttentionCore() + + @staticmethod + def _make_reference(): + from rl_engine.kernels.ops.cuda.attention.deterministic_attn import ( + DeterministicAttentionOp, + ) + + return DeterministicAttentionOp() + + @staticmethod + def _make_triton(): + from rl_engine.kernels.ops.triton.attention.deterministic_attn import ( + TritonDeterministicAttentionOp, + ) + + return TritonDeterministicAttentionOp() + + def runner(self, name: str, q, k, v, *, causal: bool, scale: float, positions): + """Return ``() -> (out, lse|None)`` for one path, or None when unavailable.""" + if name == "sdpa": + return lambda: (_sdpa_forward(q, k, v, causal=causal, scale=scale), None) + if name == "strict-aiter" and self.strict is not None: + + def run_strict(): + result = self.strict.forward_with_lse( + q, + k, + v, + causal=causal, + scale=scale, + query_position_ids=positions if causal else None, + key_position_ids=positions if causal else None, + ) + return result.out, result.lse + + return run_strict + if name == "reference-hip" and self.reference is not None: + return lambda: self.reference.forward_with_lse(q, k, v, causal=causal, scale=scale) + if name == "triton-bitwise" and self.triton is not None: + return lambda: self.triton.forward_with_lse(q, k, v, causal=causal, scale=scale) + return None + + +PATH_NAMES = ("sdpa", "strict-aiter", "reference-hip", "triton-bitwise") + + +def _single_gpu_benchmarks( + *, + paths: _Paths, + seq_lens: tuple[int, ...], + dtypes: tuple[torch.dtype, ...], + q_heads: int, + kv_heads: int, + head_dim: int, + batch: int, + warmup: int, + samples: int, + training_samples: int, + device: torch.device, +) -> list[dict[str, Any]]: + cases: list[dict[str, Any]] = [] + scale = 1.0 / math.sqrt(head_dim) + + for dtype in dtypes: + dtype_name = str(dtype).replace("torch.", "").replace("float", "fp").replace("bfp", "bf") + for seq_len in seq_lens: + q, k, v = _seeded_qkv( + batch, q_heads, kv_heads, seq_len, head_dim, dtype, device, seed=1234 + ) + positions = _positions(batch, seq_len, device) + oracle_out, oracle_lse = _fp64_oracle(q, k, v, causal=True, scale=scale) + + row: dict[str, Any] = { + "dtype": dtype_name, + "seq_len": seq_len, + "batch": batch, + "q_heads": q_heads, + "kv_heads": kv_heads, + "head_dim": head_dim, + "paths": {}, + } + captured: dict[str, tuple[torch.Tensor, torch.Tensor | None]] = {} + + for name in PATH_NAMES: + runner = paths.runner(name, q, k, v, causal=True, scale=scale, positions=positions) + if runner is None: + continue + + out, lse = runner() + captured[name] = ( + out.detach().clone(), + None if lse is None else lse.detach().clone(), + ) + + forward = _summary_ms(_gpu_event_samples(runner, warmup=warmup, samples=samples)) + forward_peak = _peak_memory_mib(runner) + + entry: dict[str, Any] = { + "forward": forward, + "forward_peak_mib": forward_peak, + "out_vs_fp64": _accuracy(out.double(), oracle_out), + } + if lse is not None: + entry["lse_vs_fp64"] = _accuracy(lse.double(), oracle_lse) + + # Repeat determinism: two identical calls must be bitwise equal. + repeat_out, repeat_lse = runner() + entry["repeat_bitwise"] = _bitwise_equal(out, repeat_out) and ( + lse is None or _bitwise_equal(lse, repeat_lse) + ) + + training = _training_runner( + paths, name, q, k, v, causal=True, scale=scale, positions=positions + ) + if training is not None: + entry["train_fwd_bwd"] = _summary_ms( + _gpu_event_samples( + training, warmup=max(1, warmup // 2), samples=training_samples + ) + ) + entry["train_peak_mib"] = _peak_memory_mib(training) + + row["paths"][name] = entry + del out, lse, repeat_out, repeat_lse + torch.cuda.empty_cache() + + # Headline: Triton must be bit-identical to the native reference core. + if "triton-bitwise" in captured and "reference-hip" in captured: + t_out, t_lse = captured["triton-bitwise"] + r_out, r_lse = captured["reference-hip"] + row["triton_vs_reference"] = { + "out_mismatched": _mismatch_count(t_out, r_out), + "lse_mismatched": _mismatch_count(t_lse, r_lse), + "out_relative_l2": _relative_l2(t_out, r_out), + "bitwise": _bitwise_equal(t_out, r_out) and _bitwise_equal(t_lse, r_lse), + } + # The production core is a different vendor kernel; report the gap, do + # not claim parity with it. + if "strict-aiter" in captured and "reference-hip" in captured: + s_out, s_lse = captured["strict-aiter"] + r_out, r_lse = captured["reference-hip"] + row["strict_vs_reference"] = { + "out": _accuracy(s_out, r_out), + "lse": _accuracy(s_lse, r_lse), + "out_mismatched": _mismatch_count(s_out, r_out), + } + + cases.append(row) + del q, k, v, oracle_out, oracle_lse, captured + torch.cuda.empty_cache() + return cases + + +def _training_runner(paths, name, q, k, v, *, causal, scale, positions): + """Return ``() -> None`` running one forward+backward, or None.""" + if name == "sdpa": + + def train_sdpa() -> None: + qr = q.detach().requires_grad_(True) + kr = k.detach().requires_grad_(True) + vr = v.detach().requires_grad_(True) + out = _sdpa_forward(qr, kr, vr, causal=causal, scale=scale) + out.sum().backward() + + return train_sdpa + + op = { + "strict-aiter": paths.strict, + "reference-hip": paths.reference, + "triton-bitwise": paths.triton, + }.get(name) + if op is None: + return None + + def train_op() -> None: + qr = q.detach().requires_grad_(True) + kr = k.detach().requires_grad_(True) + vr = v.detach().requires_grad_(True) + kwargs: dict[str, Any] = {"causal": causal, "scale": scale} + if name == "strict-aiter" and causal: + kwargs["query_position_ids"] = positions + kwargs["key_position_ids"] = positions + result = op.forward_with_lse(qr, kr, vr, **kwargs) + out = result.out if hasattr(result, "out") else result[0] + out.sum().backward() + + return train_op + + +def _backward_parity( + *, + paths: _Paths, + seq_lens: tuple[int, ...], + q_heads: int, + kv_heads: int, + head_dim: int, + batch: int, + device: torch.device, +) -> list[dict[str, Any]]: + """dQ/dK/dV bitwise parity, Triton port versus the native reference core.""" + if paths.reference is None or paths.triton is None: + return [] + rows = [] + scale = 1.0 / math.sqrt(head_dim) + for seq_len in seq_lens: + q, k, v = _seeded_qkv( + batch, q_heads, kv_heads, seq_len, head_dim, torch.bfloat16, device, seed=99 + ) + grad_out = torch.randn( + batch, + q_heads, + seq_len, + head_dim, + device=device, + dtype=torch.bfloat16, + generator=torch.Generator(device=device).manual_seed(100), + ) + grads = {} + for name, op in (("reference-hip", paths.reference), ("triton-bitwise", paths.triton)): + qr = q.detach().requires_grad_(True) + kr = k.detach().requires_grad_(True) + vr = v.detach().requires_grad_(True) + out, _lse = op.forward_with_lse(qr, kr, vr, causal=True, scale=scale) + out.backward(grad_out) + grads[name] = (qr.grad.clone(), kr.grad.clone(), vr.grad.clone()) + reference = grads["reference-hip"] + triton_grads = grads["triton-bitwise"] + rows.append( + { + "seq_len": seq_len, + "dq_mismatched": _mismatch_count(triton_grads[0], reference[0]), + "dk_mismatched": _mismatch_count(triton_grads[1], reference[1]), + "dv_mismatched": _mismatch_count(triton_grads[2], reference[2]), + "bitwise": all(_bitwise_equal(t, r) for t, r in zip(triton_grads, reference)), + } + ) + del q, k, v, grad_out, grads + torch.cuda.empty_cache() + return rows + + +def _batch_composition( + *, + paths: _Paths, + seq_lens: tuple[int, ...], + q_heads: int, + kv_heads: int, + head_dim: int, + device: torch.device, +) -> list[dict[str, Any]]: + """A row computed alone must be bitwise equal to the same row inside a batch. + + The strict ROCm core refuses ``B > 1`` outright (``_validate_inputs``: "executes + one logical batch row at a time"), so for that path the property is structural + rather than measured, and the row records that instead of a comparison. + """ + rows = [] + scale = 1.0 / math.sqrt(head_dim) + for seq_len in seq_lens: + q, k, v = _seeded_qkv( + 4, q_heads, kv_heads, seq_len, head_dim, torch.bfloat16, device, seed=7 + ) + positions_batch = _positions(4, seq_len, device) + positions_one = _positions(1, seq_len, device) + row: dict[str, Any] = {"seq_len": seq_len, "paths": {}} + for name in PATH_NAMES: + if name == "strict-aiter": + if paths.strict is not None: + row["paths"][name] = { + "batch_gt1_rejected": True, + "out_bitwise": True, + "out_mismatched": 0, + "note": "core executes one logical batch row per launch", + } + continue + batched = paths.runner( + name, q, k, v, causal=True, scale=scale, positions=positions_batch + ) + single = paths.runner( + name, + q[2:3].contiguous(), + k[2:3].contiguous(), + v[2:3].contiguous(), + causal=True, + scale=scale, + positions=positions_one, + ) + if batched is None or single is None: + continue + batch_out, batch_lse = batched() + single_out, single_lse = single() + row["paths"][name] = { + "batch_gt1_rejected": False, + "out_bitwise": _bitwise_equal(single_out[0], batch_out[2].contiguous()), + "out_mismatched": _mismatch_count(single_out[0], batch_out[2].contiguous()), + "out_max_abs": _accuracy(single_out[0], batch_out[2])["max_abs"], + "lse_bitwise": ( + None + if batch_lse is None + else _bitwise_equal(single_lse[0], batch_lse[2].contiguous()) + ), + } + del batch_out, batch_lse, single_out, single_lse + torch.cuda.empty_cache() + rows.append(row) + del q, k, v + torch.cuda.empty_cache() + return rows + + +def _tp_head_sensitivity( + *, + paths: _Paths, + seq_lens: tuple[int, ...], + tp_degrees: tuple[int, ...], + q_heads: int, + kv_heads: int, + head_dim: int, + device: torch.device, +) -> list[dict[str, Any]]: + """Is a head shard under TP=N bitwise equal to the same slice of an unsharded run? + + TP performs no cross-rank reduction in attention, so any nonzero value here + means the kernel's arithmetic depends on how many heads shared the launch. + Measured both on the raw production core and through the per-KV-group launch + schedule that the Vime provider uses. + """ + rows: list[dict[str, Any]] = [] + scale = 1.0 / math.sqrt(head_dim) + for seq_len in seq_lens: + q, k, v = _seeded_qkv( + 1, q_heads, kv_heads, seq_len, head_dim, torch.bfloat16, device, seed=21 + ) + positions = _positions(1, seq_len, device) + + for schedule in ("raw_launch", "one_kv_group_per_launch"): + full = _tp_schedule_forward(paths, q, k, v, scale, positions, schedule) + if full is None: + continue + full_out, full_lse = full + for tp in tp_degrees: + if q_heads % tp or kv_heads % tp: + continue + local_q = q_heads // tp + local_kv = kv_heads // tp + shard = _tp_schedule_forward( + paths, + q[:, :local_q], + k[:, :local_kv], + v[:, :local_kv], + scale, + positions, + schedule, + ) + shard_out, shard_lse = shard + rows.append( + { + "seq_len": seq_len, + "schedule": schedule, + "tp": tp, + "local_q_heads": local_q, + "local_kv_heads": local_kv, + "out_max_abs": _accuracy(shard_out, full_out[:, :local_q])["max_abs"], + "lse_max_abs": _accuracy(shard_lse, full_lse[:, :local_q])["max_abs"], + "invariant": _bitwise_equal(shard_out, full_out[:, :local_q].contiguous()) + and _bitwise_equal(shard_lse, full_lse[:, :local_q].contiguous()), + } + ) + del shard_out, shard_lse + torch.cuda.empty_cache() + del full_out, full_lse + torch.cuda.empty_cache() + del q, k, v + torch.cuda.empty_cache() + return rows + + +def _tp_schedule_forward(paths, q, k, v, scale, positions, schedule): + """Run the strict core either in one launch or one launch per KV group.""" + if paths.strict is None: + return None + if schedule == "raw_launch": + result = paths.strict.forward_with_lse( + q, + k, + v, + causal=True, + scale=scale, + query_position_ids=positions, + key_position_ids=positions, + ) + return result.out.contiguous(), result.lse.contiguous() + + group = q.size(1) // k.size(1) + outs, lses = [], [] + for kv_index in range(k.size(1)): + lo, hi = kv_index * group, (kv_index + 1) * group + result = paths.strict.forward_with_lse( + q[:, lo:hi], + k[:, kv_index : kv_index + 1], + v[:, kv_index : kv_index + 1], + causal=True, + scale=scale, + query_position_ids=positions, + key_position_ids=positions, + ) + outs.append(result.out) + lses.append(result.lse) + return torch.cat(outs, dim=1).contiguous(), torch.cat(lses, dim=1).contiguous() + + +# --------------------------------------------------------------------------- +# Distributed CP (spawned world; harness shape from PR #325) +# --------------------------------------------------------------------------- + + +def _distributed_cp_worker( + rank: int, + world_size: int, + topology: tuple[str, int, int, int], + init_method: str, + result_queue: Any, + warmup: int, + samples: int, + seq_len: int, + q_heads: int, + kv_heads: int, + head_dim: int, +) -> None: + try: + torch.cuda.set_device(rank) + dist.init_process_group( + backend="nccl", + init_method=init_method, + rank=rank, + world_size=world_size, + device_id=torch.device("cuda", rank), + timeout=timedelta(minutes=20), + ) + payload = _distributed_cp_case( + rank, + world_size, + topology, + warmup=warmup, + samples=samples, + seq_len=seq_len, + q_heads=q_heads, + kv_heads=kv_heads, + head_dim=head_dim, + ) + if rank == 0: + result_queue.put({"ok": True, "topology": topology[0], "payload": payload}) + except Exception: + result_queue.put( + { + "ok": False, + "rank": rank, + "topology": topology[0], + "traceback": traceback.format_exc(), + } + ) + raise + finally: + if dist.is_available() and dist.is_initialized(): + dist.destroy_process_group() + + +def _distributed_cp_case( + rank: int, + world_size: int, + topology: tuple[str, int, int], + *, + warmup: int, + samples: int, + seq_len: int, + q_heads: int, + kv_heads: int, + head_dim: int, +) -> dict[str, Any]: + """One CP topology running the real AG/RS schedule over the RCCL transport. + + Schedule (same one ``scripts/ws2_p2p_nccl_attention_reference_check.py`` accepts): + all-gather Q/K/V and the position ids over the CP group, run the strict core + once on the full sequence, then reduce-scatter the ``(out, lse)`` result back + to this rank's query range. Bitwise acceptance is against a CP=1 run of the + same core on the same full-sequence inputs. + """ + from rl_engine.kernels.ops.cuda.attention.cp_comm import ( + AttentionCPBlockMetadata, + AttentionCPCommunicationPlan, + AttentionParallelSpec, + RCCLAGRSAttentionCPCommunication, + ) + from rl_engine.kernels.ops.rocm.attention.flash_attn import StrictRocmAiterCKAttentionCore + + label, tp_world, cp_world, replicas = topology + device = torch.device("cuda", rank) + scale = 1.0 / math.sqrt(head_dim) + chunk_size = seq_len // (cp_world * 2) + + # Ranks that share a TP index form one CP group. Every rank must call + # new_group for every group, in the same order. + group_index = rank // cp_world + tp_index = group_index % tp_world + replica_index = group_index // tp_world + cp_rank = rank % cp_world + cp_group = None + for slice_index in range(world_size // cp_world): + ranks = list(range(slice_index * cp_world, (slice_index + 1) * cp_world)) + group = dist.new_group(ranks=ranks) + if slice_index == group_index: + cp_group = group + + local_q_heads = q_heads // tp_world + local_kv_heads = kv_heads // tp_world + + generator = torch.Generator(device="cpu").manual_seed(2357 + tp_index + 100 * replica_index) + q = torch.randn( + 1, local_q_heads, seq_len, head_dim, generator=generator, dtype=torch.bfloat16 + ).to(device) + k = torch.randn( + 1, local_kv_heads, seq_len, head_dim, generator=generator, dtype=torch.bfloat16 + ).to(device) + v = torch.randn( + 1, local_kv_heads, seq_len, head_dim, generator=generator, dtype=torch.bfloat16 + ).to(device) + positions = _positions(1, seq_len, device).to(torch.int32) + + span = seq_len // cp_world + owner_ranges = tuple((i * span, (i + 1) * span) for i in range(cp_world)) + blocks: list[AttentionCPBlockMetadata] = [] + for owner, (owner_start, owner_end) in enumerate(owner_ranges): + for start in range(owner_start, owner_end, chunk_size): + blocks.append( + AttentionCPBlockMetadata( + global_block_index=len(blocks), + kv_block_start=start, + kv_block_end=min(start + chunk_size, owner_end), + owner_cp_rank=owner, + owner_tp_rank=tp_index, + ) + ) + plan = AttentionCPCommunicationPlan( + parallel=AttentionParallelSpec( + tp_world_size=tp_world, + tp_rank=tp_index, + cp_world_size=cp_world, + cp_rank=cp_rank, + ), + backend="rccl_ag_rs", + status="implemented", + expected_blocks=tuple(blocks), + expected_kv_token_range=(0, seq_len), + query_token_ranges=owner_ranges, + ) + + core = StrictRocmAiterCKAttentionCore() + communication = RCCLAGRSAttentionCPCommunication(process_group=cp_group) + + query_start, query_end = owner_ranges[cp_rank] + q_local = q[:, :, query_start:query_end, :].contiguous() + k_local = k[:, :, query_start:query_end, :].contiguous() + v_local = v[:, :, query_start:query_end, :].contiguous() + positions_local = positions[:, query_start:query_end].contiguous() + + def cp_forward(): + q_full = communication.all_gather_query(q_local, plan) + k_full, v_full = communication.all_gather_kv(k_local, v_local, plan) + query_positions, key_positions = communication.all_gather_position_ids( + positions_local, positions_local, plan + ) + result = core.forward_with_lse( + q_full, + k_full, + v_full, + causal=True, + scale=scale, + query_position_ids=query_positions, + key_position_ids=key_positions, + ) + return communication.reduce_scatter_strict_result(result.out, result.lse, plan) + + shard = cp_forward() + forward_ms = _summary_ms( + _gpu_event_samples(lambda: cp_forward(), warmup=warmup, samples=samples) + ) + peak = _peak_memory_mib(lambda: cp_forward()) + + # CP=1 acceptance: the same core on the same full-sequence inputs, then take + # this rank's query range out of it. + single = core.forward_with_lse( + q, + k, + v, + causal=True, + scale=scale, + query_position_ids=positions, + key_position_ids=positions, + ) + expected_out = single.out[:, :, query_start:query_end, :].contiguous() + expected_lse = single.lse[:, :, query_start:query_end].contiguous() + + out_bitwise = _bitwise_equal(shard.out.contiguous(), expected_out) + lse_bitwise = _bitwise_equal(shard.lse.contiguous(), expected_lse) + repeat = cp_forward() + repeat_bitwise = _bitwise_equal(shard.out.contiguous(), repeat.out.contiguous()) + + flags = torch.tensor( + [ + 1.0 if out_bitwise else 0.0, + 1.0 if lse_bitwise else 0.0, + 1.0 if repeat_bitwise else 0.0, + ], + device=device, + ) + dist.all_reduce(flags, op=dist.ReduceOp.MIN) + mismatches = torch.tensor( + [ + float(_mismatch_count(shard.out.contiguous(), expected_out)), + float(_mismatch_count(shard.lse.contiguous(), expected_lse)), + ], + device=device, + ) + dist.all_reduce(mismatches, op=dist.ReduceOp.SUM) + + return { + "topology": label, + "world_size": world_size, + "tp_world_size": tp_world, + "cp_world_size": cp_world, + "replicas": replicas, + "seq_len": seq_len, + "local_q_heads": local_q_heads, + "local_kv_heads": local_kv_heads, + "transport": "rccl_ag_rs", + "forward": forward_ms, + "peak_mib_per_rank": peak, + "out_bitwise_vs_cp1": bool(flags[0].item() == 1.0), + "lse_bitwise_vs_cp1": bool(flags[1].item() == 1.0), + "repeat_bitwise": bool(flags[2].item() == 1.0), + "out_mismatched_all_ranks": int(mismatches[0].item()), + "lse_mismatched_all_ranks": int(mismatches[1].item()), + } + + +def _run_distributed_topology( + topology: tuple[str, int, int, int], + *, + warmup: int, + samples: int, + seq_len: int, + q_heads: int, + kv_heads: int, + head_dim: int, +) -> dict[str, Any]: + _label, tp_world, cp_world, replicas = topology + world_size = tp_world * cp_world * replicas + context = mp.get_context("spawn") + with tempfile.TemporaryDirectory() as temporary_directory: + init_method = (Path(temporary_directory) / "rccl_init").as_uri() + result_queue = context.Queue() + processes = [ + context.Process( + target=_distributed_cp_worker, + args=( + rank, + world_size, + topology, + init_method, + result_queue, + warmup, + samples, + seq_len, + q_heads, + kv_heads, + head_dim, + ), + ) + for rank in range(world_size) + ] + for process in processes: + process.start() + result = None + try: + result = result_queue.get(timeout=1800) + except queue.Empty as exc: + for process in processes: + if process.is_alive(): + process.terminate() + raise RuntimeError(f"timed out waiting for {topology[0]}") from exc + finally: + for process in processes: + process.join(timeout=90) + if process.is_alive(): + process.terminate() + process.join(timeout=30) + result_queue.close() + result_queue.join_thread() + if result is None or not result["ok"]: + raise RuntimeError((result or {}).get("traceback", f"{topology[0]} returned no result")) + return result["payload"] + + +# --------------------------------------------------------------------------- +# Environment, report and figures +# --------------------------------------------------------------------------- + + +def _environment() -> dict[str, Any]: + properties = torch.cuda.get_device_properties(0) if torch.cuda.is_available() else None + try: + from rl_engine.kernels.ops.base import _C, _EXT_AVAILABLE + + symbols = sorted(s for s in dir(_C) if "attention" in s) if _EXT_AVAILABLE else [] + except Exception: # noqa: BLE001 + symbols = [] + try: + import triton + + triton_version = triton.__version__ + except Exception: # noqa: BLE001 + triton_version = "unavailable" + return { + "gpu": properties.name if properties else "n/a", + "architecture": getattr(properties, "gcnArchName", "unknown") if properties else "n/a", + "gpu_count": torch.cuda.device_count() if torch.cuda.is_available() else 0, + "hip": torch.version.hip, + "cuda": torch.version.cuda, + "torch": torch.__version__, + "triton": triton_version, + "python": platform.python_version(), + "extension_attention_symbols": symbols, + "native_collective": "torch.distributed ProcessGroupNCCL (RCCL on ROCm)", + } + + +def _fmt(value: Any, spec: str = ".4f") -> str: + if value is None: + return "n/a" + if isinstance(value, bool): + return "yes" if value else "**no**" + if isinstance(value, (int,)) and not isinstance(value, bool): + return str(value) + try: + return format(float(value), spec) + except (TypeError, ValueError): + return str(value) + + +def _write_report(payload: dict[str, Any], output_directory: Path) -> None: + environment = payload["environment"] + configuration = payload["configuration"] + lines: list[str] = [] + add = lines.append + + add("# WS2 strict ROCm Attention — bitwise parity and performance") + add("") + add("> Operator-only benchmark. No model checkpoint or serving engine was used;") + add("> the shapes are Qwen3-8B's attention shapes.") + add("") + add("## Environment") + add("") + add("| Item | Value |") + add("|---|---|") + for key in sorted(environment): + value = environment[key] + add(f"| {key} | {value if not isinstance(value, list) else ', '.join(value) or 'none'} |") + add("") + + add("## Methodology") + add("") + add( + f"- Operator shape: `Hq={configuration['q_heads']}`, `Hkv={configuration['kv_heads']}`, " + f"`D={configuration['head_dim']}`, `B={configuration['batch']}`, causal; sequence sweep " + + ", ".join(str(s) for s in configuration["seq_lens"]) + + "." + ) + add("- Measured paths:") + add( + " - `sdpa`: `torch.nn.functional.scaled_dot_product_attention`. **Speed baseline only** — " + "as in PR #325, no accuracy comparison is mixed into the speed table." + ) + add( + " - `strict-aiter`: `StrictRocmAiterCKAttentionCore`, the ROCm production core " + "(AITER CK dense MHA, non-split API, one logical batch row per launch)." + ) + add( + " - `reference-hip`: `_C.deterministic_attention_forward/backward`, the materializing " + "FP32 reference core hipified from the shared `.cu`." + ) + add( + " - `triton-bitwise`: `TritonDeterministicAttentionOp`, whose contract is bit-identity " + "with `reference-hip`." + ) + add( + "- Timing: CUDA events, median and p95. Peak memory is the per-call increase in " + "`torch.cuda.max_memory_allocated` above what was live before the call." + ) + add( + "- Accuracy is against an FP64 oracle over the same BF16/FP16-rounded inputs. " + "Repeat = two identical calls are bitwise equal; batch-invariant = a row computed " + "alone is bitwise equal to the same row inside a batch." + ) + add( + f"- {configuration['warmup']} warmups, {configuration['samples']} measured forward " + f"samples, {configuration['training_samples']} measured forward+backward samples. " + "Raw medians, p95, min and max are in `results.json`." + ) + add("") + add("Reproduce from the repository root:") + add("") + add("```bash") + add("python benchmarks/benchmark_ws2_rocm_attention.py \\") + add(f" --seq-lens {','.join(str(s) for s in configuration['seq_lens'])} \\") + add(f" --dtypes {','.join(configuration['dtypes'])} \\") + add(f" --warmup {configuration['warmup']} --samples {configuration['samples']} \\") + add(f" --training-samples {configuration['training_samples']} \\") + add(" --output-dir benchmarks/results/ws2_rocm_mi300x") + add("```") + add("") + + if payload.get("unavailable_paths"): + add("### Unavailable paths") + add("") + for name, reason in payload["unavailable_paths"].items(): + add(f"- `{name}`: {reason}") + add("") + + # ---- headline + add("## Bitwise parity: Triton port vs the native reference core") + add("") + add("Acceptance is 0 mismatched elements. This is the contract the Triton core exists to hold.") + add("") + add("| dtype | S | out mismatched | lse mismatched | dQ | dK | dV | bitwise |") + add("|---|---:|---:|---:|---:|---:|---:|:---:|") + backward = {row["seq_len"]: row for row in payload.get("backward_parity", [])} + for case in payload["single_gpu"]["cases"]: + parity = case.get("triton_vs_reference") + if not parity: + continue + grads = backward.get(case["seq_len"], {}) if case["dtype"] == "bf16" else {} + add( + f"| {case['dtype']} | {case['seq_len']} | {parity['out_mismatched']} | " + f"{parity['lse_mismatched']} | {_fmt(grads.get('dq_mismatched'))} | " + f"{_fmt(grads.get('dk_mismatched'))} | {_fmt(grads.get('dv_mismatched'))} | " + f"{_fmt(parity['bitwise'])} |" + ) + add("") + add("`dQ/dK/dV` are measured on the BF16 sweep only; `n/a` marks the FP16 rows.") + add("") + + # ---- single GPU speed + for dtype in configuration["dtypes"]: + cases = [c for c in payload["single_gpu"]["cases"] if c["dtype"] == dtype] + if not cases: + continue + add(f"## Single-GPU Attention ({dtype})") + add("") + add("### Forward") + add("") + add( + "| S | Path | Median (ms) | p95 (ms) | vs sdpa | Peak MiB | out max-abs vs FP64 | " + "lse max-abs vs FP64 | Repeat |" + ) + add("|---:|---|---:|---:|---:|---:|---:|---:|:---:|") + for case in cases: + baseline = case["paths"].get("sdpa", {}).get("forward", {}).get("median_ms") + for name in PATH_NAMES: + entry = case["paths"].get(name) + if not entry: + continue + median = entry["forward"]["median_ms"] + ratio = f"{median / baseline:.2f}x" if baseline else "n/a" + add( + f"| {case['seq_len']} | {name} | {median:.4f} | " + f"{entry['forward']['p95_ms']:.4f} | {ratio} | " + f"{entry['forward_peak_mib']:.1f} | " + f"{entry['out_vs_fp64']['max_abs']:.3e} | " + f"{_fmt(entry.get('lse_vs_fp64', {}).get('max_abs'), '.3e')} | " + f"{_fmt(entry['repeat_bitwise'])} |" + ) + add("") + add("### Forward+backward") + add("") + add("| S | Path | Median (ms) | p95 (ms) | vs sdpa | Peak MiB |") + add("|---:|---|---:|---:|---:|---:|") + for case in cases: + baseline = case["paths"].get("sdpa", {}).get("train_fwd_bwd", {}).get("median_ms") + for name in PATH_NAMES: + entry = case["paths"].get(name) + if not entry or "train_fwd_bwd" not in entry: + continue + median = entry["train_fwd_bwd"]["median_ms"] + ratio = f"{median / baseline:.2f}x" if baseline else "n/a" + add( + f"| {case['seq_len']} | {name} | {median:.4f} | " + f"{entry['train_fwd_bwd']['p95_ms']:.4f} | {ratio} | " + f"{entry['train_peak_mib']:.1f} |" + ) + add("") + + # ---- production core vs reference + add("## Production core versus the reference core") + add("") + add( + "These are two different kernels, so this is a tolerance comparison, not a parity " + "claim. It is here to size the gap, not to assert equality." + ) + add("") + add("| dtype | S | out max-abs | out relative-L2 | lse max-abs |") + add("|---|---:|---:|---:|---:|") + for case in payload["single_gpu"]["cases"]: + gap = case.get("strict_vs_reference") + if not gap: + continue + add( + f"| {case['dtype']} | {case['seq_len']} | {gap['out']['max_abs']:.3e} | " + f"{gap['out']['relative_l2']:.3e} | {gap['lse']['max_abs']:.3e} |" + ) + add("") + + # ---- batch composition + add("## Batch-composition invariance") + add("") + add( + "A row computed alone must be bitwise equal to the same row inside a batch. " + "The strict ROCm core rejects `B > 1` outright, so for that path the property is " + "structural rather than measured." + ) + add("") + add("| S | Path | Bitwise | Mismatched | Note |") + add("|---:|---|:---:|---:|---|") + for row in payload.get("batch_composition", []): + for name, entry in row["paths"].items(): + note = entry.get("note", "measured") + add( + f"| {row['seq_len']} | {name} | {_fmt(entry['out_bitwise'])} | " + f"{entry['out_mismatched']} | {note} |" + ) + add("") + + # ---- TP degree + add("## TP-degree invariance of the strict ROCm core") + add("") + add( + "A head shard computed under TP=N versus the same slice of an unsharded run. TP performs " + "no cross-rank reduction in attention, so any nonzero value means the kernel's result " + "depends on how many heads shared the launch. `raw_launch` is one launch for all heads; " + "`one_kv_group_per_launch` is the schedule the Vime provider actually uses." + ) + add("") + add("| S | Schedule | TP | Local Hq | Local Hkv | out max-abs | lse max-abs | Invariant |") + add("|---:|---|---:|---:|---:|---:|---:|:---:|") + for row in payload.get("tp_head_sensitivity", []): + add( + f"| {row['seq_len']} | {row['schedule']} | {row['tp']} | {row['local_q_heads']} | " + f"{row['local_kv_heads']} | {row['out_max_abs']:.6e} | {row['lse_max_abs']:.6e} | " + f"{_fmt(row['invariant'])} |" + ) + add("") + + # ---- distributed + distributed = payload.get("distributed") or [] + if distributed: + add("## Distributed CP (RCCL AG/RS transport)") + add("") + add( + "Schedule: all-gather Q/K/V and the position ids over the CP group, run the strict " + "core once on the full sequence, reduce-scatter `(out, lse)` back to this rank's " + "query range. Acceptance is bitwise against a CP=1 run of the same core." + ) + add("") + add( + "| Topology | World | TP | CP | Replicas | S | Median (ms) | p95 (ms) | " + "Peak MiB/rank | out bitwise | lse bitwise | Repeat |" + ) + add("|---|---:|---:|---:|---:|---:|---:|---:|---:|:---:|:---:|:---:|") + for row in distributed: + if "error" in row: + add( + f"| {row['topology']} | — | — | — | — | — | — | — | — | " + "error | error | error |" + ) + continue + add( + f"| {row['topology']} | {row['world_size']} | {row['tp_world_size']} | " + f"{row['cp_world_size']} | {row.get('replicas', 1)} | {row['seq_len']} | " + f"{row['forward']['median_ms']:.4f} | " + f"{row['forward']['p95_ms']:.4f} | {row['peak_mib_per_rank']:.1f} | " + f"{_fmt(row['out_bitwise_vs_cp1'])} | {_fmt(row['lse_bitwise_vs_cp1'])} | " + f"{_fmt(row['repeat_bitwise'])} |" + ) + add("") + errors = [row for row in distributed if "error" in row] + for row in errors: + add(f"- `{row['topology']}` failed: {row['error']}") + if errors: + add("") + + add("## Figures") + add("") + add( + "`reference-hip` and `triton-bitwise` allocate exactly the same buffers, so their " + "memory curves coincide and the later-drawn series hides the earlier one." + ) + add("") + add("![Single-device latency and memory grid](single_gpu_grid.png)") + add("") + add("![Single-device latency](single_gpu_latency.png)") + add("") + add("![Single-device peak memory](single_gpu_memory.png)") + add("") + add("![TP-degree invariance](tp_degree_invariance.png)") + add("") + if distributed: + add("![Distributed CP latency](distributed_cp_latency.png)") + add("") + + (output_directory / "report.md").write_text("\n".join(lines) + "\n") + + +def _write_figures(payload: dict[str, Any], output_directory: Path) -> None: + import matplotlib + + matplotlib.use("Agg") + import matplotlib.pyplot as plt + + plt.style.use("seaborn-v0_8-whitegrid") + plt.rcParams.update({"font.size": 10, "axes.titlesize": 11, "legend.fontsize": 8}) + + style = { + "sdpa": {"marker": "o", "color": "#888888", "linestyle": "--"}, + "strict-aiter": {"marker": "s", "color": "#d62728"}, + "reference-hip": {"marker": "^", "color": "#1f77b4"}, + "triton-bitwise": {"marker": "D", "color": "#2ca02c"}, + } + + cases = [c for c in payload["single_gpu"]["cases"] if c["dtype"] == "bf16"] + if not cases: + return + seq_lens = sorted({c["seq_len"] for c in cases}) + present = [n for n in PATH_NAMES if any(n in c["paths"] for c in cases)] + + panels = ( + ("forward", "median_ms", "Forward latency", "median ms", True), + ("train_fwd_bwd", "median_ms", "Forward+backward latency", "median ms", True), + ("forward_peak_mib", None, "Forward peak memory", "peak MiB above live", True), + ("train_peak_mib", None, "Forward+backward peak memory", "peak MiB above live", True), + ) + + def value(case, name, key, sub): + entry = case["paths"].get(name) + if entry is None or key not in entry: + return float("nan") + return entry[key][sub] if sub else entry[key] + + def draw(axis, key, sub, title, ylabel, log_y): + for index, name in enumerate(present): + ys = [ + value(next(c for c in cases if c["seq_len"] == s), name, key, sub) for s in seq_lens + ] + axis.plot( + seq_lens, + ys, + label=name, + linewidth=3.0 - 0.35 * index, + markersize=7 - 0.5 * index, + zorder=3 + index, + **style.get(name, {}), + ) + axis.set_xscale("log", base=2) + if log_y: + axis.set_yscale("log") + axis.set_xlabel("sequence length") + axis.set_ylabel(ylabel) + axis.set_title(f"BF16: {title}") + axis.grid(True, which="both", alpha=0.3) + axis.legend() + + for filename, chosen in ( + ("single_gpu_latency.png", panels[:2]), + ("single_gpu_memory.png", panels[2:]), + ): + figure, axes = plt.subplots(1, 2, figsize=(12, 4.5)) + for axis, (key, sub, title, ylabel, log_y) in zip(axes, chosen): + draw(axis, key, sub, title, ylabel, log_y) + figure.tight_layout() + figure.savefig(output_directory / filename, dpi=180) + plt.close(figure) + + figure, axes = plt.subplots(2, 2, figsize=(13, 9)) + for axis, (key, sub, title, ylabel, log_y) in zip(axes.flat, panels): + draw(axis, key, sub, title, ylabel, log_y) + figure.suptitle( + "Single-device strict Attention, BF16, Qwen3-8B shape " + f"(Hq={QWEN3_8B_Q_HEADS}, Hkv={QWEN3_8B_KV_HEADS}, D={QWEN3_8B_HEAD_DIM})", + fontsize=12, + ) + figure.tight_layout(rect=(0, 0, 1, 0.96)) + figure.savefig(output_directory / "single_gpu_grid.png", dpi=180) + plt.close(figure) + + # TP head-count sensitivity: raw launch versus one KV group per launch. + tp_rows = payload.get("tp_head_sensitivity") or [] + if tp_rows: + figure, axis = plt.subplots(figsize=(11, 5)) + for schedule, marker in (("raw_launch", "o"), ("one_kv_group_per_launch", "s")): + rows = [r for r in tp_rows if r["schedule"] == schedule] + if not rows: + continue + labels = [f"S={r['seq_len']}\nTP={r['tp']}" for r in rows] + axis.plot( + range(len(rows)), + [max(r["out_max_abs"], 1e-12) for r in rows], + marker=marker, + label=schedule, + linewidth=2.4, + ) + axis.set_xticks(range(len([r for r in tp_rows if r["schedule"] == "raw_launch"]))) + axis.set_xticklabels( + [f"S={r['seq_len']}\nTP={r['tp']}" for r in tp_rows if r["schedule"] == "raw_launch"], + fontsize=8, + ) + axis.set_yscale("log") + axis.set_ylabel("out max-abs vs unsharded slice (1e-12 == bitwise)") + axis.set_title("TP-degree invariance of the strict ROCm core, BF16") + axis.grid(True, which="both", alpha=0.3) + axis.legend() + figure.tight_layout() + figure.savefig(output_directory / "tp_degree_invariance.png", dpi=180) + plt.close(figure) + + distributed = payload.get("distributed") or [] + if distributed: + figure, axis = plt.subplots(figsize=(max(8, 1.5 * len(distributed)), 4.8)) + labels = [f"{r['topology']}\nS={r['seq_len']}" for r in distributed] + axis.bar( + range(len(distributed)), + [r["forward"]["median_ms"] for r in distributed], + color="#2ca02c", + ) + axis.set_xticks(range(len(distributed))) + axis.set_xticklabels(labels, fontsize=9) + axis.set_ylabel("median ms") + axis.set_title("Strict ROCm Attention + RCCL AG/RS CP transport, BF16") + axis.grid(True, axis="y", alpha=0.3) + figure.tight_layout() + figure.savefig(output_directory / "distributed_cp_latency.png", dpi=180) + plt.close(figure) + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--output-dir", type=Path, default=Path("benchmarks/results/ws2_rocm_mi300x") + ) + parser.add_argument("--warmup", type=int, default=5) + parser.add_argument("--samples", type=int, default=20) + parser.add_argument("--training-samples", type=int, default=10) + parser.add_argument("--batch", type=int, default=1) + parser.add_argument("--q-heads", type=int, default=QWEN3_8B_Q_HEADS) + parser.add_argument("--kv-heads", type=int, default=QWEN3_8B_KV_HEADS) + parser.add_argument("--head-dim", type=int, default=QWEN3_8B_HEAD_DIM) + parser.add_argument( + "--seq-lens", + type=lambda s: tuple(int(x) for x in s.split(",")), + default=DEFAULT_SEQ_LENS, + ) + parser.add_argument("--dtypes", default="bf16,fp16") + parser.add_argument("--skip-distributed", action="store_true") + parser.add_argument("--skip-figures", action="store_true") + parser.add_argument( + "--distributed-only", + action="store_true", + help="Re-run only the distributed topologies and merge into an existing results.json.", + ) + parser.add_argument( + "--report-only", + action="store_true", + help="Re-render report.md and the figures from an existing results.json.", + ) + arguments = parser.parse_args() + + if arguments.report_only: + payload = json.loads((arguments.output_dir / "results.json").read_text()) + _write_report(payload, arguments.output_dir) + if not arguments.skip_figures: + _write_figures(payload, arguments.output_dir) + print(json.dumps({"output_dir": str(arguments.output_dir)}, indent=2)) + return + + if not torch.cuda.is_available(): + raise SystemExit("this benchmark requires a ROCm or CUDA GPU") + + device = torch.device("cuda", 0) + torch.cuda.set_device(device) + dtype_map = {"bf16": torch.bfloat16, "fp16": torch.float16, "fp32": torch.float32} + dtypes = tuple(dtype_map[name] for name in arguments.dtypes.split(",")) + + paths = _Paths() + payload: dict[str, Any] = { + "environment": _environment(), + "configuration": { + "batch": arguments.batch, + "q_heads": arguments.q_heads, + "kv_heads": arguments.kv_heads, + "head_dim": arguments.head_dim, + "seq_lens": list(arguments.seq_lens), + "dtypes": arguments.dtypes.split(","), + "warmup": arguments.warmup, + "samples": arguments.samples, + "training_samples": arguments.training_samples, + }, + "unavailable_paths": paths.errors, + } + + existing: dict[str, Any] = {} + if arguments.distributed_only: + existing = json.loads((arguments.output_dir / "results.json").read_text()) + payload = dict(existing) + payload["environment"] = _environment() + + if not arguments.distributed_only: + payload["single_gpu"] = { + "cases": _single_gpu_benchmarks( + paths=paths, + seq_lens=arguments.seq_lens, + dtypes=dtypes, + q_heads=arguments.q_heads, + kv_heads=arguments.kv_heads, + head_dim=arguments.head_dim, + batch=arguments.batch, + warmup=arguments.warmup, + samples=arguments.samples, + training_samples=arguments.training_samples, + device=device, + ) + } + payload["backward_parity"] = _backward_parity( + paths=paths, + seq_lens=arguments.seq_lens, + q_heads=arguments.q_heads, + kv_heads=arguments.kv_heads, + head_dim=arguments.head_dim, + batch=arguments.batch, + device=device, + ) + payload["batch_composition"] = _batch_composition( + paths=paths, + seq_lens=arguments.seq_lens, + q_heads=arguments.q_heads, + kv_heads=arguments.kv_heads, + head_dim=arguments.head_dim, + device=device, + ) + payload["tp_head_sensitivity"] = _tp_head_sensitivity( + paths=paths, + seq_lens=arguments.seq_lens, + tp_degrees=DEFAULT_TP_DEGREES, + q_heads=arguments.q_heads, + kv_heads=arguments.kv_heads, + head_dim=arguments.head_dim, + device=device, + ) + + distributed: list[dict[str, Any]] = [] + if not arguments.skip_distributed: + available = torch.cuda.device_count() + for topology in DISTRIBUTED_TOPOLOGIES: + if topology[1] * topology[2] * topology[3] > available: + continue + try: + distributed.append( + _run_distributed_topology( + topology, + warmup=arguments.warmup, + samples=arguments.samples, + seq_len=arguments.seq_lens[-1] if arguments.seq_lens else 2048, + q_heads=arguments.q_heads, + kv_heads=arguments.kv_heads, + head_dim=arguments.head_dim, + ) + ) + except Exception as exc: # noqa: BLE001 - a failed topology is a reported row + distributed.append( + {"topology": topology[0], "error": f"{type(exc).__name__}: {exc}"} + ) + payload["distributed"] = distributed + + arguments.output_dir.mkdir(parents=True, exist_ok=True) + (arguments.output_dir / "results.json").write_text(json.dumps(payload, indent=2) + "\n") + _write_report(payload, arguments.output_dir) + if not arguments.skip_figures: + _write_figures(payload, arguments.output_dir) + print(json.dumps({"output_dir": str(arguments.output_dir)}, indent=2)) + + +if __name__ == "__main__": + os.environ.setdefault("NCCL_IB_DISABLE", "1") + main() diff --git a/benchmarks/results/ws2_rocm_mi300x/PR_DESCRIPTION.md b/benchmarks/results/ws2_rocm_mi300x/PR_DESCRIPTION.md new file mode 100644 index 00000000..b187f07b --- /dev/null +++ b/benchmarks/results/ws2_rocm_mi300x/PR_DESCRIPTION.md @@ -0,0 +1,292 @@ +# WS2: bitwise-exact Attention on ROCm + +Brings the strict ROCm Attention path to a stated, measured bitwise standard, and adds a +Triton core that is bit-identical to the native reference kernel so the arithmetic +contract is testable without the vendor kernel. + +Operator-only. No model checkpoint or serving engine is loaded anywhere in this PR; the +shapes are Qwen3-8B's (`Hq=32`, `Hkv=8`, `D=128`). Measured on 8×MI300X (`gfx942`), +ROCm 7.14.60850, torch 2.12.0, Triton 3.7.0. + +--- + +## 1. What "bitwise" means here, and what it does not + +Several different guarantees get conflated in attention work, so this PR states which one +it claims at each boundary: + +| Scope | Claim | Enforced by | +| --- | --- | --- | +| Varying batch composition | **bitwise** | `B > 1` rejected at the core (§2.2) | +| Varying padding | **bitwise** | `key_padding_mask` rejected (§2.3) | +| Varying TP degree | **bitwise** | one KV group per launch (§2.4) | +| Varying CP degree | **bitwise** | RCCL-as-transport AG/RS (§2.5) | +| Triton core vs native reference core | **bitwise** | §2.8, measured in §3.1 | +| CUDA production core vs ROCm production core | **not claimed** | different vendor kernels | +| ROCm production core vs reference core | **not claimed** | different kernels; gap sized in §3.4 | + +The last two rows are deliberate. CUDA runs FlashAttention 4 CuTe and ROCm runs AITER CK +dense MHA; the tile decomposition, the online-softmax rescale order, and MFMA versus MMA +accumulation all differ. Nothing in the tree claims cross-platform bit equality and this +PR does not add such a claim. What is shared across platforms is the *contract*, not the +bits. + +## 2. The algorithmic arrangements + +The strict ROCm core does not reimplement attention. It removes every source of +run-to-run and shape-to-shape arithmetic variation from the vendor kernel and records what +was removed, so a mismatch becomes a contract violation rather than a debugging session. + +**2.1 Split-KV is structurally impossible, not merely switched off.** +Split-KV partitions the KV axis and merges partial softmax states; the partition count +depends on shape and occupancy, so the reduction order moves with it. CUDA can pass +`num_splits=1` to FA4. AITER exposes no such knob, so the ROCm core binds to the dense, +non-split API entry point instead and records `split_kv_control = "dense_non_split_api"`. +`SplitKVSpec` must be `DISABLED`; a non-disabled spec raises in `__init__` rather than +being quietly honoured. + +**2.2 Batch composition cannot change the bits, because `B > 1` is rejected.** +`StrictRocmAiterCKAttentionCore._validate_inputs` refuses any input with `q.size(0) != 1`: +*"strict AITER CK core executes one logical batch row at a time"*. This is stronger than +testing for batch invariance — there is no batched launch whose arithmetic could differ +from the single-row launch, because the batched launch does not exist. Callers materialise +each logical row separately. + +**2.3 Padding never enters a reduction.** +`key_padding_mask` is rejected outright: the core *"materializes each unpadded logical +row"*. A padded and an unpadded run of the same logical row cannot differ, because the +padded run is not expressible. + +**2.4 One KV group per launch, to make the result independent of TP degree.** +This is the ROCm-specific problem. AITER/CK's reduction order depends on how many heads +shared the launch, and TP performs no cross-rank reduction in attention — it is pure head +sharding — so a head shard computed under TP=4 was *not* bit-identical to the same shard +under TP=8 at some shapes. The provider therefore launches the core once per +`(batch row, KV group)` and concatenates, so every launch sees exactly one KV group and its +Q heads regardless of the TP degree that produced the shard. §3.2 measures both schedules +side by side; the cost is real and is reported. + +**2.5 RCCL is a transport, never a reduction.** +`_RCCLRankOrderedTransport` uses RCCL only for `all_gather` and a root-owned `scatter`. Its +`reduce_scatter` first gathers every source shard and then evaluates a fixed balanced rank +tree locally, so the floating-point combine order is ours and does not depend on RCCL's +internal algorithm selection, which varies with message size and topology. + +**2.6 The vendor kernel is fingerprinted, not version-pinned.** +AITER dispatches in Python, so a package version does not pin behaviour. +`_load_aiter_ck_ops()` takes a **sha256 of the `aiter.ops.mha` source file** and exports it +as `aiter_source_sha256` in the provenance, so a silent upstream change to the dispatch +logic invalidates the recorded arithmetic identity. + +**2.7 Fail closed, everywhere.** +A missing AITER entry point, a missing native extension, or a dispatch that resolves to a +different backend raises. No path substitutes a different kernel to keep a run alive. + +**2.8 A reference core shared with CUDA, and a Triton port that matches it bitwise.** +`csrc/cuda/attention/deterministic_attention.cu` is hipified to +`csrc/hip/attention/deterministic_attention.hip`, so the *reference* core genuinely is the +same algorithm on both platforms. This PR adds +`rl_engine/kernels/ops/triton/attention/deterministic_attn.py`, a Triton port whose +contract is bit-identity with that reference. Three things had to be reproduced rather than +re-derived: + +- **Dot products stay sequential FMA chains.** The C++ kernel accumulates one element at a + time in a single thread, so the contraction index is the *loop* and the head dim is the + *vector*. The opposite, much faster arrangement would reassociate the sum. +- **The row softmax keeps the 256-lane partial layout.** Key `k` belongs to lane `k % 256`; + each lane sums ascending, then a stride-halving fold combines the partials. + `_tree_sum_256` reproduces that fold exactly. +- **`expf`/`logf` are re-emitted instruction for instruction.** Every Triton exp/log + intrinsic — `tl.exp`, `tl.math.exp`, `libdevice.exp` — lowers to a bare `v_exp_f32`, about + 1 ULP away from the `expf` the C++ kernel calls, which alone broke parity on ~14% of + elements. The helpers reproduce hipcc's two-term argument reduction around that same + hardware instruction, with an inline-asm barrier to stop LLVM refolding the reduction into + an FMA. Verified bitwise over 4M+ inputs including subnormals, ±inf and NaN. + +The nvcc `expf`/`logf` sequences are not ported, so on CUDA the op refuses to construct +unless the caller passes `require_bitwise_libm=False`, rather than silently returning +non-bitwise results. + +--- + +## 3. Results + +Full report, `results.json` and figures: `benchmarks/results/ws2_rocm_mi300x/`. +Reproduce with `python benchmarks/benchmark_ws2_rocm_attention.py`. + +Paths: `sdpa` (`torch.nn.functional.scaled_dot_product_attention`, **speed baseline only** — +as in PR #325, no accuracy comparison is mixed into the speed table), `strict-aiter` (the +ROCm production core), `reference-hip` (`_C.deterministic_attention_*`), `triton-bitwise` +(this PR). + +### 3.1 Headline: Triton port vs the native reference core + +Acceptance is 0 mismatched elements. This is the contract the Triton core exists to hold. + +| dtype | S | out | lse | dQ | dK | dV | bitwise | +|---|---:|---:|---:|---:|---:|---:|:---:| +| bf16 | 512 | 0 | 0 | 0 | 0 | 0 | yes | +| bf16 | 1024 | 0 | 0 | 0 | 0 | 0 | yes | +| bf16 | 2048 | 0 | 0 | 0 | 0 | 0 | yes | +| bf16 | 4096 | 0 | 0 | 0 | 0 | 0 | yes | +| fp16 | 512 | 0 | 0 | — | — | — | yes | +| fp16 | 1024 | 0 | 0 | — | — | — | yes | +| fp16 | 2048 | 0 | 0 | — | — | — | yes | +| fp16 | 4096 | 0 | 0 | — | — | — | yes | + +`dQ/dK/dV` are measured on the BF16 sweep only. + +### 3.2 TP-degree invariance of the strict ROCm core + +A head shard computed under TP=N versus the same slice of an unsharded run. TP performs no +cross-rank reduction in attention, so any nonzero value means the kernel's result depends on +how many heads shared the launch. + +| S | TP | Local Hq | `raw_launch` out max-abs | `one_kv_group_per_launch` out max-abs | +|---:|---:|---:|---:|---:| +| 512 | 2 | 16 | 0.000000e+00 | 0.000000e+00 | +| 512 | 4 | 8 | 0.000000e+00 | 0.000000e+00 | +| 512 | 8 | 4 | 0.000000e+00 | 0.000000e+00 | +| 1024 | 2 | 16 | **7.812500e-03** | 0.000000e+00 | +| 1024 | 4 | 8 | **7.812500e-03** | 0.000000e+00 | +| 1024 | 8 | 4 | **7.812500e-03** | 0.000000e+00 | +| 2048 | 2 | 16 | 0.000000e+00 | 0.000000e+00 | +| 2048 | 4 | 8 | **3.906250e-03** | 0.000000e+00 | +| 2048 | 8 | 4 | **1.953125e-03** | 0.000000e+00 | +| 4096 | 2 | 16 | 0.000000e+00 | 0.000000e+00 | +| 4096 | 4 | 8 | 0.000000e+00 | 0.000000e+00 | +| 4096 | 8 | 4 | **3.906250e-03** | 0.000000e+00 | + +Raw AITER is non-invariant at 5 of 12 points, and *which* points is shape-dependent — the +failure is invisible at S=512 and at S=4096/TP=2, which is exactly what makes it dangerous: +training at TP=4 and rolling out at TP=8 would compare fine on most shapes. The per-KV-group +schedule is bitwise at **12 of 12**. This reproduces PR #319's finding on independent inputs. + +### 3.3 Single-GPU latency and memory (BF16) + +| S | Path | Fwd median (ms) | vs sdpa | Fwd+bwd (ms) | vs sdpa | Fwd peak MiB | Fwd+bwd peak MiB | out max-abs vs FP64 | +|---:|---|---:|---:|---:|---:|---:|---:|---:| +| 512 | sdpa | 0.0782 | 1.00x | 0.3228 | 1.00x | 12.1 | 32.2 | 8.195e-03 | +| 512 | strict-aiter | 0.2428 | 3.11x | 0.6039 | 1.87x | 14.1 | 288.2 | 2.468e-02 | +| 512 | reference-hip | 0.9659 | 12.36x | 2.9714 | 9.21x | 36.1 | 78.1 | 7.741e-03 | +| 512 | triton-bitwise | 1.3816 | 17.68x | 4.8578 | 15.05x | 36.1 | 78.1 | 7.741e-03 | +| 1024 | sdpa | 0.1319 | 1.00x | 0.4319 | 1.00x | 24.1 | 64.3 | 1.027e-02 | +| 1024 | strict-aiter | 0.2468 | 1.87x | 0.9293 | 2.15x | 28.1 | 1088.4 | 2.609e-02 | +| 1024 | reference-hip | 3.1389 | 23.79x | 12.2009 | 28.25x | 136.1 | 284.3 | 7.810e-03 | +| 1024 | triton-bitwise | 4.8240 | 36.56x | 20.1771 | 46.71x | 136.1 | 284.3 | 7.810e-03 | +| 2048 | sdpa | 0.2887 | 1.00x | 1.0735 | 1.00x | 48.3 | 128.8 | 7.994e-03 | +| 2048 | strict-aiter | 0.2962 | 1.03x | 1.9019 | 1.77x | 56.3 | 4224.8 | 2.027e-02 | +| 2048 | reference-hip | 12.8467 | 44.50x | 47.8741 | 44.60x | 528.2 | 1080.5 | 7.804e-03 | +| 2048 | triton-bitwise | 19.4226 | 67.28x | 76.7414 | 71.49x | 528.3 | 1080.5 | 7.804e-03 | +| 4096 | sdpa | 0.6936 | 1.00x | 3.2464 | 1.00x | 96.5 | 257.5 | 9.604e-03 | +| 4096 | strict-aiter | **0.5644** | **0.81x** | 5.7304 | 1.77x | 112.5 | 16641.5 | 2.138e-02 | +| 4096 | reference-hip | 49.3624 | 71.17x | 173.3365 | 53.39x | 2080.5 | 4209.0 | 7.808e-03 | +| 4096 | triton-bitwise | 86.0655 | 124.08x | 304.1032 | 93.67x | 2080.5 | 4209.0 | 7.808e-03 | + +Three things worth reading off this table: + +- **The strict production core is not a tax at long sequence.** At S=4096 forward it is + *faster* than SDPA (0.81x), and its worst case across the sweep is 3.11x at S=512 where + absolute cost is 0.24 ms. The bitwise arrangements in §2 cost almost nothing in the + production path. +- **AITER's backward is memory-hungry.** `strict-aiter` fwd+bwd peaks at 16.6 GiB at S=4096 + versus 4.2 GiB for the materializing reference core — the reference core materializes an + FP32 `[B, Hq, Sq, Skv]` score matrix and is *still* 4x smaller. Worth knowing before + sizing a training run. +- **The deterministic cores are the most accurate of the four.** Against an FP64 oracle they + sit at 7.8e-03 versus 9.6e-03 for SDPA and 2.1e-02 for AITER. Determinism here is not + bought with accuracy. + +FP16 is in the full report; the shape of the result is the same. + +### 3.4 Production core versus reference core + +Two different kernels, so this is a tolerance comparison, not a parity claim. It is here to +size the gap. + +| S | out max-abs | out relative-L2 | lse max-abs | +|---:|---:|---:|---:| +| 512 | 3.125e-02 | 5.420e-03 | 9.537e-07 | +| 1024 | 3.125e-02 | 5.505e-03 | 1.431e-06 | +| 2048 | 1.562e-02 | 5.595e-03 | 1.907e-06 | +| 4096 | 1.562e-02 | 5.633e-03 | 3.815e-06 | + +### 3.5 Batch-composition invariance + +Bitwise at every sequence length for every path. For `strict-aiter` the property is +structural (§2.2) rather than measured: the batched launch does not exist. + +### 3.6 Distributed CP over the RCCL AG/RS transport + +Schedule: all-gather Q/K/V and the position ids over the CP group, run the strict core once +on the full sequence, reduce-scatter `(out, lse)` back to this rank's query range. Acceptance +is bitwise against a CP=1 run of the same core on the same inputs. S=4096, BF16. + +| Topology | World | TP | CP | Replicas | Local Hq/Hkv | Median (ms) | p95 (ms) | Peak MiB/rank | out bitwise | lse bitwise | Repeat | +|---|---:|---:|---:|---:|---|---:|---:|---:|:---:|:---:|:---:| +| `tp1_cp2` | 2 | 1 | 2 | 1 | 32/8 | 1.8352 | 1.8726 | 160.5 | yes | yes | yes | +| `tp2_cp2` | 4 | 2 | 2 | 1 | 16/4 | 1.2114 | 1.3138 | 80.3 | yes | yes | yes | +| `tp1_cp4` | 4 | 1 | 4 | 1 | 32/8 | 1.3973 | 1.4363 | 160.5 | yes | yes | yes | +| `tp2_cp2_x2` | 8 | 2 | 2 | 2 | 16/4 | 1.2297 | 1.2948 | 80.3 | yes | yes | yes | +| `tp2_cp4` | 8 | 2 | 4 | 1 | 16/4 | 1.2594 | 1.3294 | 80.3 | yes | yes | yes | +| `tp1_cp8` | 8 | 1 | 8 | 1 | 32/8 | 1.4112 | 2.2029 | 160.5 | yes | yes | yes | + +All six topologies are bitwise against CP=1 on both `out` and `lse`, with 0 mismatched +elements summed across every rank, and repeat-bitwise on every rank. `tp2_cp2_x2` is the +8-rank case PR #319 used: two independent CP groups running side by side at TP=2/CP=2. + +--- + +## 4. Figures + +![Single-device latency and memory grid](benchmarks/results/ws2_rocm_mi300x/single_gpu_grid.png) + +![TP-degree invariance](benchmarks/results/ws2_rocm_mi300x/tp_degree_invariance.png) + +![Distributed CP latency](benchmarks/results/ws2_rocm_mi300x/distributed_cp_latency.png) + +`reference-hip` and `triton-bitwise` allocate exactly the same buffers, so their memory +curves coincide and the later-drawn series hides the earlier one. + +--- + +## 5. Files + +| Path | What | +| --- | --- | +| `rl_engine/kernels/ops/triton/attention/deterministic_attn.py` | New. Triton core, bit-identical to `_C.deterministic_attention_*`. | +| `rl_engine/kernels/ops/triton/attention/__init__.py` | Exports the new op and `BITWISE_LIBM_PARITY`. | +| `tests/test_triton_deterministic_attention.py` | New. 71 parity / invariance tests. | +| `benchmarks/benchmark_ws2_rocm_attention.py` | New. Measurement matrix and figures, reusing PR #325 / #328 helpers. | +| `benchmarks/results/ws2_rocm_mi300x/` | Report, `results.json`, figures. | +| `csrc/ops.cpp` | Fix: the merge from `feat/rocm-deterministic-collectives` dropped an `#if !defined(USE_ROCM)` around the `prefix_shared_attention` registration but kept its `#endif`, leaving 13 `#endif` against 12 `#if`. The ROCm build failed with `#endif without #if`. | + +## 6. Test plan + +- `pytest tests/test_triton_deterministic_attention.py` — 71 passed. 15 shape/mask/scale + configs × {bf16, fp16} × {forward, backward}, plus end-to-end autograd through both ops, + the fully-masked-row case, batch-slice invariance, and a direct pin on the `expf`/`logf` + helpers against the vendor libm. +- `pytest tests/test_deterministic_attention_cuda.py` — 614 passed (the native core is + unaffected). +- `python benchmarks/benchmark_ws2_rocm_attention.py` — the report above. + +## 7. Known limitations + +- **CUDA is not covered by the Triton core's bitwise claim.** The nvcc `expf`/`logf` argument + reductions are not ported, and no CUDA device was available to derive or verify them. + `TritonDeterministicAttentionOp` raises on CUDA unless the caller passes + `require_bitwise_libm=False`; a test pins that behaviour on both platforms. +- **The Triton core is a parity core, not a FlashAttention replacement.** Like the native + reference it materialises the full FP32 `[B, Hq, Sq, Skv]` score matrix and runs + scalar-order reductions; §3.3 shows the cost. +- **`_C` does not register `deterministic_attention_forward_fp32`.** The `.cu` defines it but + the pybind registration is missing, so the *native* `DeterministicAttentionOp.forward_fp32` + raises `AttributeError`. Pre-existing, not touched here; the Triton `forward_fp32` works and + its test validates against the op's own downcast instead of the native path. +- **A stale comment contradicts the shipped TP policy.** + `rl_engine/integrations/vime/attention.py` still carries a comment saying RL-Kernel "binds + the degree rather than paying ~3x forward time", from before the merge that introduced the + per-KV-group launch loop. The provenance dict immediately below it correctly reports + `tp_degree_invariant: True`. §3.2 shows the code is right and the comment is wrong; flagged + here rather than silently rewritten. diff --git a/benchmarks/results/ws2_rocm_mi300x/distributed_cp_latency.png b/benchmarks/results/ws2_rocm_mi300x/distributed_cp_latency.png new file mode 100644 index 0000000000000000000000000000000000000000..29a1358e83726ecaa76c5bed2cac566730ed6a52 GIT binary patch literal 46377 zcmeFaXH-?$wl#`cmJuxlBZ>(`5J5puGKvZtksL$>4CJWfAfTmU84%o(RC1Dx4p2&AF!AVfde*)ycC~O6Gc2wihjQ>CRlVx?*B(Wny?~ zr;VW2etXY5|2=hT(w06Q{AZG>HctEZ z^Pbj7tKlj2%8WZ~*-^6Yp5ujkdsZo|Q}AH#ou2*V*uSGF<5)qkoyd;0JKXb~w8x!X z%0D-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 Operator-only benchmark. No model checkpoint or serving engine was used; +> the shapes are Qwen3-8B's attention shapes. + +## Environment + +| Item | Value | +|---|---| +| architecture | gfx942:sramecc+:xnack- | +| cuda | None | +| extension_attention_symbols | deterministic_attention_backward, deterministic_attention_forward | +| gpu | AMD Instinct MI300X | +| gpu_count | 8 | +| hip | 7.14.60850 | +| native_collective | torch.distributed ProcessGroupNCCL (RCCL on ROCm) | +| python | 3.12.3 | +| torch | 2.12.0+rocm7.14.0a20260608 | +| triton | 3.7.0 | + +## Methodology + +- Operator shape: `Hq=32`, `Hkv=8`, `D=128`, `B=1`, causal; sequence sweep 512, 1024, 2048, 4096. +- Measured paths: + - `sdpa`: `torch.nn.functional.scaled_dot_product_attention`. **Speed baseline only** — as in PR #325, no accuracy comparison is mixed into the speed table. + - `strict-aiter`: `StrictRocmAiterCKAttentionCore`, the ROCm production core (AITER CK dense MHA, non-split API, one logical batch row per launch). + - `reference-hip`: `_C.deterministic_attention_forward/backward`, the materializing FP32 reference core hipified from the shared `.cu`. + - `triton-bitwise`: `TritonDeterministicAttentionOp`, whose contract is bit-identity with `reference-hip`. +- Timing: CUDA events, median and p95. Peak memory is the per-call increase in `torch.cuda.max_memory_allocated` above what was live before the call. +- Accuracy is against an FP64 oracle over the same BF16/FP16-rounded inputs. Repeat = two identical calls are bitwise equal; batch-invariant = a row computed alone is bitwise equal to the same row inside a batch. +- 5 warmups, 20 measured forward samples, 10 measured forward+backward samples. Raw medians, p95, min and max are in `results.json`. + +Reproduce from the repository root: + +```bash +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 +``` + +## Bitwise parity: Triton port vs the native reference core + +Acceptance is 0 mismatched elements. This is the contract the Triton core exists to hold. + +| dtype | S | out mismatched | lse mismatched | dQ | dK | dV | bitwise | +|---|---:|---:|---:|---:|---:|---:|:---:| +| bf16 | 512 | 0 | 0 | 0 | 0 | 0 | yes | +| bf16 | 1024 | 0 | 0 | 0 | 0 | 0 | yes | +| bf16 | 2048 | 0 | 0 | 0 | 0 | 0 | yes | +| bf16 | 4096 | 0 | 0 | 0 | 0 | 0 | yes | +| fp16 | 512 | 0 | 0 | n/a | n/a | n/a | yes | +| fp16 | 1024 | 0 | 0 | n/a | n/a | n/a | yes | +| fp16 | 2048 | 0 | 0 | n/a | n/a | n/a | yes | +| fp16 | 4096 | 0 | 0 | n/a | n/a | n/a | yes | + +`dQ/dK/dV` are measured on the BF16 sweep only; `n/a` marks the FP16 rows. + +## Single-GPU Attention (bf16) + +### Forward + +| S | Path | Median (ms) | p95 (ms) | vs sdpa | Peak MiB | out max-abs vs FP64 | lse max-abs vs FP64 | Repeat | +|---:|---|---:|---:|---:|---:|---:|---:|:---:| +| 512 | sdpa | 0.0782 | 0.0820 | 1.00x | 12.1 | 8.195e-03 | n/a | yes | +| 512 | strict-aiter | 0.2428 | 0.2560 | 3.11x | 14.1 | 2.468e-02 | 8.359e-07 | yes | +| 512 | reference-hip | 0.9659 | 1.0030 | 12.36x | 36.1 | 7.741e-03 | 8.111e-07 | yes | +| 512 | triton-bitwise | 1.3816 | 1.3923 | 17.68x | 36.1 | 7.741e-03 | 8.111e-07 | yes | +| 1024 | sdpa | 0.1319 | 0.1430 | 1.00x | 24.1 | 1.027e-02 | n/a | yes | +| 1024 | strict-aiter | 0.2468 | 0.2578 | 1.87x | 28.1 | 2.609e-02 | 1.213e-06 | yes | +| 1024 | reference-hip | 3.1389 | 3.1914 | 23.79x | 136.1 | 7.810e-03 | 8.732e-07 | yes | +| 1024 | triton-bitwise | 4.8240 | 4.8720 | 36.56x | 136.1 | 7.810e-03 | 8.732e-07 | yes | +| 2048 | sdpa | 0.2887 | 0.2975 | 1.00x | 48.3 | 7.994e-03 | n/a | yes | +| 2048 | strict-aiter | 0.2962 | 0.3394 | 1.03x | 56.3 | 2.027e-02 | 2.288e-06 | yes | +| 2048 | reference-hip | 12.8467 | 12.9922 | 44.50x | 528.2 | 7.804e-03 | 1.142e-06 | yes | +| 2048 | triton-bitwise | 19.4226 | 19.5024 | 67.28x | 528.3 | 7.804e-03 | 1.142e-06 | yes | +| 4096 | sdpa | 0.6936 | 0.7149 | 1.00x | 96.5 | 9.604e-03 | n/a | yes | +| 4096 | strict-aiter | 0.5644 | 0.6076 | 0.81x | 112.5 | 2.138e-02 | 3.967e-06 | yes | +| 4096 | reference-hip | 49.3624 | 49.5342 | 71.17x | 2080.5 | 7.808e-03 | 1.381e-06 | yes | +| 4096 | triton-bitwise | 86.0655 | 86.5760 | 124.08x | 2080.5 | 7.808e-03 | 1.381e-06 | yes | + +### Forward+backward + +| S | Path | Median (ms) | p95 (ms) | vs sdpa | Peak MiB | +|---:|---|---:|---:|---:|---:| +| 512 | sdpa | 0.3228 | 0.3510 | 1.00x | 32.2 | +| 512 | strict-aiter | 0.6039 | 0.6560 | 1.87x | 288.2 | +| 512 | reference-hip | 2.9714 | 2.9876 | 9.21x | 78.1 | +| 512 | triton-bitwise | 4.8578 | 4.9374 | 15.05x | 78.1 | +| 1024 | sdpa | 0.4319 | 0.5795 | 1.00x | 64.3 | +| 1024 | strict-aiter | 0.9293 | 0.9651 | 2.15x | 1088.4 | +| 1024 | reference-hip | 12.2009 | 12.2835 | 28.25x | 284.3 | +| 1024 | triton-bitwise | 20.1771 | 20.3071 | 46.71x | 284.3 | +| 2048 | sdpa | 1.0735 | 1.1114 | 1.00x | 128.8 | +| 2048 | strict-aiter | 1.9019 | 1.9802 | 1.77x | 4224.8 | +| 2048 | reference-hip | 47.8741 | 48.1225 | 44.60x | 1080.5 | +| 2048 | triton-bitwise | 76.7414 | 76.9707 | 71.49x | 1080.5 | +| 4096 | sdpa | 3.2464 | 3.3146 | 1.00x | 257.5 | +| 4096 | strict-aiter | 5.7304 | 5.7861 | 1.77x | 16641.5 | +| 4096 | reference-hip | 173.3365 | 173.5448 | 53.39x | 4209.0 | +| 4096 | triton-bitwise | 304.1032 | 304.4993 | 93.67x | 4209.0 | + +## Single-GPU Attention (fp16) + +### Forward + +| S | Path | Median (ms) | p95 (ms) | vs sdpa | Peak MiB | out max-abs vs FP64 | lse max-abs vs FP64 | Repeat | +|---:|---|---:|---:|---:|---:|---:|---:|:---:| +| 512 | sdpa | 0.0700 | 0.0735 | 1.00x | 12.1 | 1.042e-03 | n/a | yes | +| 512 | strict-aiter | 0.2472 | 0.3623 | 3.53x | 14.1 | 2.046e-03 | 8.756e-07 | yes | +| 512 | reference-hip | 0.9588 | 1.0180 | 13.70x | 36.1 | 9.702e-04 | 8.340e-07 | yes | +| 512 | triton-bitwise | 1.3660 | 1.3787 | 19.52x | 36.1 | 9.702e-04 | 8.340e-07 | yes | +| 1024 | sdpa | 0.1286 | 0.1435 | 1.00x | 24.1 | 1.053e-03 | n/a | yes | +| 1024 | strict-aiter | 0.2423 | 0.2602 | 1.88x | 28.1 | 2.299e-03 | 1.250e-06 | yes | +| 1024 | reference-hip | 3.0663 | 3.1218 | 23.84x | 136.1 | 9.757e-04 | 1.011e-06 | yes | +| 1024 | triton-bitwise | 4.7733 | 4.8062 | 37.11x | 136.1 | 9.757e-04 | 1.011e-06 | yes | +| 2048 | sdpa | 0.2866 | 0.2997 | 1.00x | 48.3 | 9.099e-04 | n/a | yes | +| 2048 | strict-aiter | 0.2942 | 0.3034 | 1.03x | 56.3 | 2.053e-03 | 2.540e-06 | yes | +| 2048 | reference-hip | 12.5348 | 12.7376 | 43.73x | 528.2 | 9.099e-04 | 1.103e-06 | yes | +| 2048 | triton-bitwise | 19.1882 | 19.2599 | 66.94x | 528.3 | 9.099e-04 | 1.103e-06 | yes | +| 4096 | sdpa | 1.0565 | 1.0855 | 1.00x | 96.5 | 9.905e-04 | n/a | yes | +| 4096 | strict-aiter | 0.5813 | 0.6200 | 0.55x | 112.5 | 1.953e-03 | 3.805e-06 | yes | +| 4096 | reference-hip | 48.3115 | 48.4132 | 45.73x | 2080.5 | 9.681e-04 | 1.511e-06 | yes | +| 4096 | triton-bitwise | 84.9071 | 85.0736 | 80.36x | 2080.5 | 9.681e-04 | 1.511e-06 | yes | + +### Forward+backward + +| S | Path | Median (ms) | p95 (ms) | vs sdpa | Peak MiB | +|---:|---|---:|---:|---:|---:| +| 512 | sdpa | 0.3177 | 0.3328 | 1.00x | 32.2 | +| 512 | strict-aiter | 1.0677 | 1.1007 | 3.36x | 288.2 | +| 512 | reference-hip | 3.0363 | 3.0744 | 9.56x | 78.1 | +| 512 | triton-bitwise | 4.7821 | 4.8698 | 15.05x | 78.1 | +| 1024 | sdpa | 0.4399 | 0.5001 | 1.00x | 64.4 | +| 1024 | strict-aiter | 0.8350 | 0.9122 | 1.90x | 1088.4 | +| 1024 | reference-hip | 12.0068 | 12.0713 | 27.29x | 284.3 | +| 1024 | triton-bitwise | 19.3157 | 19.4640 | 43.91x | 284.3 | +| 2048 | sdpa | 1.1766 | 1.2269 | 1.00x | 128.5 | +| 2048 | strict-aiter | 1.9060 | 2.0250 | 1.62x | 4224.8 | +| 2048 | reference-hip | 47.5117 | 47.7373 | 40.38x | 1080.5 | +| 2048 | triton-bitwise | 75.7950 | 76.0022 | 64.42x | 1080.5 | +| 4096 | sdpa | 4.0247 | 4.0579 | 1.00x | 257.0 | +| 4096 | strict-aiter | 5.8475 | 5.8916 | 1.45x | 16641.5 | +| 4096 | reference-hip | 171.5617 | 171.7337 | 42.63x | 4209.0 | +| 4096 | triton-bitwise | 300.1215 | 301.8339 | 74.57x | 4209.0 | + +## Production core versus the reference core + +These are two different kernels, so this is a tolerance comparison, not a parity claim. It is here to size the gap, not to assert equality. + +| dtype | S | out max-abs | out relative-L2 | lse max-abs | +|---|---:|---:|---:|---:| +| bf16 | 512 | 3.125e-02 | 5.420e-03 | 9.537e-07 | +| bf16 | 1024 | 3.125e-02 | 5.505e-03 | 1.431e-06 | +| bf16 | 2048 | 1.562e-02 | 5.595e-03 | 1.907e-06 | +| bf16 | 4096 | 1.562e-02 | 5.633e-03 | 3.815e-06 | +| fp16 | 512 | 1.953e-03 | 4.323e-04 | 9.537e-07 | +| fp16 | 1024 | 1.953e-03 | 4.466e-04 | 1.431e-06 | +| fp16 | 2048 | 1.953e-03 | 4.503e-04 | 2.861e-06 | +| fp16 | 4096 | 1.953e-03 | 4.580e-04 | 3.815e-06 | + +## Batch-composition invariance + +A row computed alone must be bitwise equal to the same row inside a batch. The strict ROCm core rejects `B > 1` outright, so for that path the property is structural rather than measured. + +| S | Path | Bitwise | Mismatched | Note | +|---:|---|:---:|---:|---| +| 512 | sdpa | yes | 0 | measured | +| 512 | strict-aiter | yes | 0 | core executes one logical batch row per launch | +| 512 | reference-hip | yes | 0 | measured | +| 512 | triton-bitwise | yes | 0 | measured | +| 1024 | sdpa | yes | 0 | measured | +| 1024 | strict-aiter | yes | 0 | core executes one logical batch row per launch | +| 1024 | reference-hip | yes | 0 | measured | +| 1024 | triton-bitwise | yes | 0 | measured | +| 2048 | sdpa | yes | 0 | measured | +| 2048 | strict-aiter | yes | 0 | core executes one logical batch row per launch | +| 2048 | reference-hip | yes | 0 | measured | +| 2048 | triton-bitwise | yes | 0 | measured | +| 4096 | sdpa | yes | 0 | measured | +| 4096 | strict-aiter | yes | 0 | core executes one logical batch row per launch | +| 4096 | reference-hip | yes | 0 | measured | +| 4096 | triton-bitwise | yes | 0 | measured | + +## TP-degree invariance of the strict ROCm core + +A head shard computed under TP=N versus the same slice of an unsharded run. TP performs no cross-rank reduction in attention, so any nonzero value means the kernel's result depends on how many heads shared the launch. `raw_launch` is one launch for all heads; `one_kv_group_per_launch` is the schedule the Vime provider actually uses. + +| S | Schedule | TP | Local Hq | Local Hkv | out max-abs | lse max-abs | Invariant | +|---:|---|---:|---:|---:|---:|---:|:---:| +| 512 | raw_launch | 2 | 16 | 4 | 0.000000e+00 | 0.000000e+00 | yes | +| 512 | raw_launch | 4 | 8 | 2 | 0.000000e+00 | 0.000000e+00 | yes | +| 512 | raw_launch | 8 | 4 | 1 | 0.000000e+00 | 0.000000e+00 | yes | +| 512 | one_kv_group_per_launch | 2 | 16 | 4 | 0.000000e+00 | 0.000000e+00 | yes | +| 512 | one_kv_group_per_launch | 4 | 8 | 2 | 0.000000e+00 | 0.000000e+00 | yes | +| 512 | one_kv_group_per_launch | 8 | 4 | 1 | 0.000000e+00 | 0.000000e+00 | yes | +| 1024 | raw_launch | 2 | 16 | 4 | 7.812500e-03 | 1.907349e-06 | **no** | +| 1024 | raw_launch | 4 | 8 | 2 | 7.812500e-03 | 1.907349e-06 | **no** | +| 1024 | raw_launch | 8 | 4 | 1 | 7.812500e-03 | 1.907349e-06 | **no** | +| 1024 | one_kv_group_per_launch | 2 | 16 | 4 | 0.000000e+00 | 0.000000e+00 | yes | +| 1024 | one_kv_group_per_launch | 4 | 8 | 2 | 0.000000e+00 | 0.000000e+00 | yes | +| 1024 | one_kv_group_per_launch | 8 | 4 | 1 | 0.000000e+00 | 0.000000e+00 | yes | +| 2048 | raw_launch | 2 | 16 | 4 | 0.000000e+00 | 0.000000e+00 | yes | +| 2048 | raw_launch | 4 | 8 | 2 | 3.906250e-03 | 2.861023e-06 | **no** | +| 2048 | raw_launch | 8 | 4 | 1 | 1.953125e-03 | 2.861023e-06 | **no** | +| 2048 | one_kv_group_per_launch | 2 | 16 | 4 | 0.000000e+00 | 0.000000e+00 | yes | +| 2048 | one_kv_group_per_launch | 4 | 8 | 2 | 0.000000e+00 | 0.000000e+00 | yes | +| 2048 | one_kv_group_per_launch | 8 | 4 | 1 | 0.000000e+00 | 0.000000e+00 | yes | +| 4096 | raw_launch | 2 | 16 | 4 | 0.000000e+00 | 0.000000e+00 | yes | +| 4096 | raw_launch | 4 | 8 | 2 | 0.000000e+00 | 0.000000e+00 | yes | +| 4096 | raw_launch | 8 | 4 | 1 | 3.906250e-03 | 4.768372e-06 | **no** | +| 4096 | one_kv_group_per_launch | 2 | 16 | 4 | 0.000000e+00 | 0.000000e+00 | yes | +| 4096 | one_kv_group_per_launch | 4 | 8 | 2 | 0.000000e+00 | 0.000000e+00 | yes | +| 4096 | one_kv_group_per_launch | 8 | 4 | 1 | 0.000000e+00 | 0.000000e+00 | yes | + +## Distributed CP (RCCL AG/RS transport) + +Schedule: all-gather Q/K/V and the position ids over the CP group, run the strict core once on the full sequence, reduce-scatter `(out, lse)` back to this rank's query range. Acceptance is bitwise against a CP=1 run of the same core. + +| Topology | World | TP | CP | Replicas | S | Median (ms) | p95 (ms) | Peak MiB/rank | out bitwise | lse bitwise | Repeat | +|---|---:|---:|---:|---:|---:|---:|---:|---:|:---:|:---:|:---:| +| tp1_cp2 | 2 | 1 | 2 | 1 | 4096 | 1.8352 | 1.8726 | 160.5 | yes | yes | yes | +| tp2_cp2 | 4 | 2 | 2 | 1 | 4096 | 1.2114 | 1.3138 | 80.3 | yes | yes | yes | +| tp1_cp4 | 4 | 1 | 4 | 1 | 4096 | 1.3973 | 1.4363 | 160.5 | yes | yes | yes | +| tp2_cp2_x2 | 8 | 2 | 2 | 2 | 4096 | 1.2297 | 1.2948 | 80.3 | yes | yes | yes | +| tp2_cp4 | 8 | 2 | 4 | 1 | 4096 | 1.2594 | 1.3294 | 80.3 | yes | yes | yes | +| tp1_cp8 | 8 | 1 | 8 | 1 | 4096 | 1.4112 | 2.2029 | 160.5 | yes | yes | yes | + +## Figures + +`reference-hip` and `triton-bitwise` allocate exactly the same buffers, so their memory curves coincide and the later-drawn series hides the earlier one. + +![Single-device latency and memory grid](single_gpu_grid.png) + +![Single-device latency](single_gpu_latency.png) + +![Single-device peak memory](single_gpu_memory.png) + +![TP-degree invariance](tp_degree_invariance.png) + +![Distributed CP latency](distributed_cp_latency.png) + diff --git a/benchmarks/results/ws2_rocm_mi300x/results.json b/benchmarks/results/ws2_rocm_mi300x/results.json new file mode 100644 index 00000000..86f6f55d --- /dev/null +++ b/benchmarks/results/ws2_rocm_mi300x/results.json @@ -0,0 +1,1567 @@ +{ + "environment": { + "gpu": "AMD Instinct MI300X", + "architecture": "gfx942:sramecc+:xnack-", + "gpu_count": 8, + "hip": "7.14.60850", + "cuda": null, + "torch": "2.12.0+rocm7.14.0a20260608", + "triton": "3.7.0", + "python": "3.12.3", + "extension_attention_symbols": [ + "deterministic_attention_backward", + "deterministic_attention_forward" + ], + "native_collective": "torch.distributed ProcessGroupNCCL (RCCL on ROCm)" + }, + "configuration": { + "batch": 1, + "q_heads": 32, + "kv_heads": 8, + "head_dim": 128, + "seq_lens": [ + 512, + 1024, + 2048, + 4096 + ], + "dtypes": [ + "bf16", + "fp16" + ], + "warmup": 5, + "samples": 20, + "training_samples": 10 + }, + "unavailable_paths": {}, + "single_gpu": { + "cases": [ + { + "dtype": "bf16", + "seq_len": 512, + "batch": 1, + "q_heads": 32, + "kv_heads": 8, + "head_dim": 128, + "paths": { + "sdpa": { + "forward": { + "median_ms": 0.0781565010547638, + "p95_ms": 0.08199800290167333, + "min_ms": 0.07511100172996521, + "max_ms": 0.10779999941587448 + }, + "forward_peak_mib": 12.06396484375, + "out_vs_fp64": { + "max_abs": 0.008195295214645348, + "relative_l2": 0.001967118270169404 + }, + "repeat_bitwise": true, + "train_fwd_bwd": { + "median_ms": 0.322799488902092, + "p95_ms": 0.35098745077848437, + "min_ms": 0.30789700150489807, + "max_ms": 0.3568100035190582 + }, + "train_peak_mib": 32.189453125 + }, + "strict-aiter": { + "forward": { + "median_ms": 0.2428405061364174, + "p95_ms": 0.25597020536661147, + "min_ms": 0.23843400180339813, + "max_ms": 0.2588239908218384 + }, + "forward_peak_mib": 14.06298828125, + "out_vs_fp64": { + "max_abs": 0.024676734518057408, + "relative_l2": 0.00520034717487522 + }, + "lse_vs_fp64": { + "max_abs": 8.359452712269899e-07, + "relative_l2": 3.778278560802271e-08 + }, + "repeat_bitwise": true, + "train_fwd_bwd": { + "median_ms": 0.6038974821567535, + "p95_ms": 0.656002902984619, + "min_ms": 0.5818650126457214, + "max_ms": 0.6674320101737976 + }, + "train_peak_mib": 288.18896484375 + }, + "reference-hip": { + "forward": { + "median_ms": 0.9658550024032593, + "p95_ms": 1.0030183851718903, + "min_ms": 0.9305430054664612, + "max_ms": 1.0328550338745117 + }, + "forward_peak_mib": 36.0625, + "out_vs_fp64": { + "max_abs": 0.0077411426297544494, + "relative_l2": 0.0015903199116563222 + }, + "lse_vs_fp64": { + "max_abs": 8.111189337967062e-07, + "relative_l2": 4.340800016528509e-08 + }, + "repeat_bitwise": true, + "train_fwd_bwd": { + "median_ms": 2.971434473991394, + "p95_ms": 2.9875863194465637, + "min_ms": 2.9168529510498047, + "max_ms": 2.990001916885376 + }, + "train_peak_mib": 78.1259765625 + }, + "triton-bitwise": { + "forward": { + "median_ms": 1.38155198097229, + "p95_ms": 1.3922981441020967, + "min_ms": 1.3642669916152954, + "max_ms": 1.4705040454864502 + }, + "forward_peak_mib": 36.06298828125, + "out_vs_fp64": { + "max_abs": 0.0077411426297544494, + "relative_l2": 0.0015903199116563222 + }, + "lse_vs_fp64": { + "max_abs": 8.111189337967062e-07, + "relative_l2": 4.340800016528509e-08 + }, + "repeat_bitwise": true, + "train_fwd_bwd": { + "median_ms": 4.857816934585571, + "p95_ms": 4.937367367744446, + "min_ms": 4.83418083190918, + "max_ms": 4.9755120277404785 + }, + "train_peak_mib": 78.1259765625 + } + }, + "triton_vs_reference": { + "out_mismatched": 0, + "lse_mismatched": 0, + "out_relative_l2": 0.0, + "bitwise": true + }, + "strict_vs_reference": { + "out": { + "max_abs": 0.03125, + "relative_l2": 0.0054199441038222254 + }, + "lse": { + "max_abs": 9.5367431640625e-07, + "relative_l2": 4.7804698254096993e-08 + }, + "out_mismatched": 1710619 + } + }, + { + "dtype": "bf16", + "seq_len": 1024, + "batch": 1, + "q_heads": 32, + "kv_heads": 8, + "head_dim": 128, + "paths": { + "sdpa": { + "forward": { + "median_ms": 0.13193600624799728, + "p95_ms": 0.1430383503437042, + "min_ms": 0.12734900414943695, + "max_ms": 0.16027799248695374 + }, + "forward_peak_mib": 24.12646484375, + "out_vs_fp64": { + "max_abs": 0.01027133353685894, + "relative_l2": 0.0019939102141256666 + }, + "repeat_bitwise": true, + "train_fwd_bwd": { + "median_ms": 0.43192149698734283, + "p95_ms": 0.5794868871569632, + "min_ms": 0.4262329936027527, + "max_ms": 0.6512879729270935 + }, + "train_peak_mib": 64.251953125 + }, + "strict-aiter": { + "forward": { + "median_ms": 0.24680649489164352, + "p95_ms": 0.2577746048569679, + "min_ms": 0.23923499882221222, + "max_ms": 0.26523399353027344 + }, + "forward_peak_mib": 28.12548828125, + "out_vs_fp64": { + "max_abs": 0.02608916227826974, + "relative_l2": 0.00528885768244778 + }, + "lse_vs_fp64": { + "max_abs": 1.2127858237676037e-06, + "relative_l2": 4.3688457448489715e-08 + }, + "repeat_bitwise": true, + "train_fwd_bwd": { + "median_ms": 0.929300993680954, + "p95_ms": 0.9650538951158523, + "min_ms": 0.895209014415741, + "max_ms": 0.976410984992981 + }, + "train_peak_mib": 1088.37646484375 + }, + "reference-hip": { + "forward": { + "median_ms": 3.1389429569244385, + "p95_ms": 3.1914275646209718, + "min_ms": 3.088547945022583, + "max_ms": 3.198391914367676 + }, + "forward_peak_mib": 136.125, + "out_vs_fp64": { + "max_abs": 0.007810300996808017, + "relative_l2": 0.0016039478773382583 + }, + "lse_vs_fp64": { + "max_abs": 8.732049643356277e-07, + "relative_l2": 4.1651456242074347e-08 + }, + "repeat_bitwise": true, + "train_fwd_bwd": { + "median_ms": 12.200886726379395, + "p95_ms": 12.283522844314575, + "min_ms": 12.123411178588867, + "max_ms": 12.289777755737305 + }, + "train_peak_mib": 284.2509765625 + }, + "triton-bitwise": { + "forward": { + "median_ms": 4.823986291885376, + "p95_ms": 4.871986436843872, + "min_ms": 4.812709808349609, + "max_ms": 4.9668989181518555 + }, + "forward_peak_mib": 136.12548828125, + "out_vs_fp64": { + "max_abs": 0.007810300996808017, + "relative_l2": 0.0016039478773382583 + }, + "lse_vs_fp64": { + "max_abs": 8.732049643356277e-07, + "relative_l2": 4.1651456242074347e-08 + }, + "repeat_bitwise": true, + "train_fwd_bwd": { + "median_ms": 20.177056312561035, + "p95_ms": 20.307072925567628, + "min_ms": 20.135955810546875, + "max_ms": 20.38588523864746 + }, + "train_peak_mib": 284.2509765625 + } + }, + "triton_vs_reference": { + "out_mismatched": 0, + "lse_mismatched": 0, + "out_relative_l2": 0.0, + "bitwise": true + }, + "strict_vs_reference": { + "out": { + "max_abs": 0.03125, + "relative_l2": 0.005504811866196166 + }, + "lse": { + "max_abs": 1.430511474609375e-06, + "relative_l2": 5.160771963046382e-08 + }, + "out_mismatched": 3454931 + } + }, + { + "dtype": "bf16", + "seq_len": 2048, + "batch": 1, + "q_heads": 32, + "kv_heads": 8, + "head_dim": 128, + "paths": { + "sdpa": { + "forward": { + "median_ms": 0.2886884957551956, + "p95_ms": 0.29747478961944585, + "min_ms": 0.2843419909477234, + "max_ms": 0.346794992685318 + }, + "forward_peak_mib": 48.25146484375, + "out_vs_fp64": { + "max_abs": 0.007994353511369567, + "relative_l2": 0.00202698986197318 + }, + "repeat_bitwise": true, + "train_fwd_bwd": { + "median_ms": 1.0734949707984924, + "p95_ms": 1.1113748848438263, + "min_ms": 1.0401849746704102, + "max_ms": 1.1135740280151367 + }, + "train_peak_mib": 128.751953125 + }, + "strict-aiter": { + "forward": { + "median_ms": 0.2962400019168854, + "p95_ms": 0.33936424106359486, + "min_ms": 0.29099199175834656, + "max_ms": 0.36866700649261475 + }, + "forward_peak_mib": 56.25048828125, + "out_vs_fp64": { + "max_abs": 0.020265153423261406, + "relative_l2": 0.005397474562875508 + }, + "lse_vs_fp64": { + "max_abs": 2.288034266939576e-06, + "relative_l2": 6.118480465147299e-08 + }, + "repeat_bitwise": true, + "train_fwd_bwd": { + "median_ms": 1.9018654823303223, + "p95_ms": 1.980248290300369, + "min_ms": 1.8194639682769775, + "max_ms": 1.9846290349960327 + }, + "train_peak_mib": 4224.75146484375 + }, + "reference-hip": { + "forward": { + "median_ms": 12.846725940704346, + "p95_ms": 12.992164230346681, + "min_ms": 12.708842277526855, + "max_ms": 13.059562683105469 + }, + "forward_peak_mib": 528.25, + "out_vs_fp64": { + "max_abs": 0.007803990877593758, + "relative_l2": 0.0016089924958069535 + }, + "lse_vs_fp64": { + "max_abs": 1.1420866119493667e-06, + "relative_l2": 4.112077326317473e-08 + }, + "repeat_bitwise": true, + "train_fwd_bwd": { + "median_ms": 47.87408638000488, + "p95_ms": 48.12248554229736, + "min_ms": 47.75138473510742, + "max_ms": 48.182064056396484 + }, + "train_peak_mib": 1080.5009765625 + }, + "triton-bitwise": { + "forward": { + "median_ms": 19.422576904296875, + "p95_ms": 19.50241813659668, + "min_ms": 19.359281539916992, + "max_ms": 19.68897247314453 + }, + "forward_peak_mib": 528.25048828125, + "out_vs_fp64": { + "max_abs": 0.007803990877593758, + "relative_l2": 0.0016089924958069535 + }, + "lse_vs_fp64": { + "max_abs": 1.1420866119493667e-06, + "relative_l2": 4.112077326317473e-08 + }, + "repeat_bitwise": true, + "train_fwd_bwd": { + "median_ms": 76.7414321899414, + "p95_ms": 76.97069625854492, + "min_ms": 76.64971160888672, + "max_ms": 77.09093475341797 + }, + "train_peak_mib": 1080.5009765625 + } + }, + "triton_vs_reference": { + "out_mismatched": 0, + "lse_mismatched": 0, + "out_relative_l2": 0.0, + "bitwise": true + }, + "strict_vs_reference": { + "out": { + "max_abs": 0.015625, + "relative_l2": 0.005595395404504914 + }, + "lse": { + "max_abs": 1.9073486328125e-06, + "relative_l2": 6.75179843473521e-08 + }, + "out_mismatched": 6955314 + } + }, + { + "dtype": "bf16", + "seq_len": 4096, + "batch": 1, + "q_heads": 32, + "kv_heads": 8, + "head_dim": 128, + "paths": { + "sdpa": { + "forward": { + "median_ms": 0.6936104893684387, + "p95_ms": 0.7149427145719529, + "min_ms": 0.6816930174827576, + "max_ms": 0.7499539852142334 + }, + "forward_peak_mib": 96.50146484375, + "out_vs_fp64": { + "max_abs": 0.009603632718454325, + "relative_l2": 0.002050744344877689 + }, + "repeat_bitwise": true, + "train_fwd_bwd": { + "median_ms": 3.2463834285736084, + "p95_ms": 3.3145759105682373, + "min_ms": 3.1828880310058594, + "max_ms": 3.326220989227295 + }, + "train_peak_mib": 257.501953125 + }, + "strict-aiter": { + "forward": { + "median_ms": 0.5643984973430634, + "p95_ms": 0.6076013296842576, + "min_ms": 0.5579890012741089, + "max_ms": 0.6764450073242188 + }, + "forward_peak_mib": 112.50048828125, + "out_vs_fp64": { + "max_abs": 0.021384551260978935, + "relative_l2": 0.005439155102039385 + }, + "lse_vs_fp64": { + "max_abs": 3.966678205458152e-06, + "relative_l2": 1.0105794722703805e-07 + }, + "repeat_bitwise": true, + "train_fwd_bwd": { + "median_ms": 5.730393886566162, + "p95_ms": 5.786084508895874, + "min_ms": 5.679917812347412, + "max_ms": 5.816802024841309 + }, + "train_peak_mib": 16641.50146484375 + }, + "reference-hip": { + "forward": { + "median_ms": 49.36237716674805, + "p95_ms": 49.53416557312012, + "min_ms": 49.227455139160156, + "max_ms": 49.636383056640625 + }, + "forward_peak_mib": 2080.5, + "out_vs_fp64": { + "max_abs": 0.0078084380373617535, + "relative_l2": 0.0016189978158770984 + }, + "lse_vs_fp64": { + "max_abs": 1.3814724333371942e-06, + "relative_l2": 4.407682874169915e-08 + }, + "repeat_bitwise": true, + "train_fwd_bwd": { + "median_ms": 173.3364715576172, + "p95_ms": 173.54477233886718, + "min_ms": 173.05787658691406, + "max_ms": 173.5828399658203 + }, + "train_peak_mib": 4209.0009765625 + }, + "triton-bitwise": { + "forward": { + "median_ms": 86.06550979614258, + "p95_ms": 86.5759853363037, + "min_ms": 75.3355712890625, + "max_ms": 86.57606506347656 + }, + "forward_peak_mib": 2080.50048828125, + "out_vs_fp64": { + "max_abs": 0.0078084380373617535, + "relative_l2": 0.0016189978158770984 + }, + "lse_vs_fp64": { + "max_abs": 1.3814724333371942e-06, + "relative_l2": 4.407682874169915e-08 + }, + "repeat_bitwise": true, + "train_fwd_bwd": { + "median_ms": 304.1032257080078, + "p95_ms": 304.499284362793, + "min_ms": 303.684814453125, + "max_ms": 304.59844970703125 + }, + "train_peak_mib": 4209.0009765625 + } + }, + "triton_vs_reference": { + "out_mismatched": 0, + "lse_mismatched": 0, + "out_relative_l2": 0.0, + "bitwise": true + }, + "strict_vs_reference": { + "out": { + "max_abs": 0.015625, + "relative_l2": 0.00563327785605726 + }, + "lse": { + "max_abs": 3.814697265625e-06, + "relative_l2": 1.0616847591543177e-07 + }, + "out_mismatched": 13983861 + } + }, + { + "dtype": "fp16", + "seq_len": 512, + "batch": 1, + "q_heads": 32, + "kv_heads": 8, + "head_dim": 128, + "paths": { + "sdpa": { + "forward": { + "median_ms": 0.06996399909257889, + "p95_ms": 0.07350739873945716, + "min_ms": 0.06790100038051605, + "max_ms": 0.11761400103569031 + }, + "forward_peak_mib": 12.06396484375, + "out_vs_fp64": { + "max_abs": 0.0010422287323277324, + "relative_l2": 0.00024548880248913073 + }, + "repeat_bitwise": true, + "train_fwd_bwd": { + "median_ms": 0.31765198707580566, + "p95_ms": 0.3328426972031593, + "min_ms": 0.3077769875526428, + "max_ms": 0.3374210000038147 + }, + "train_peak_mib": 32.189453125 + }, + "strict-aiter": { + "forward": { + "median_ms": 0.2472269982099533, + "p95_ms": 0.3622861087322235, + "min_ms": 0.2370319962501526, + "max_ms": 0.37651899456977844 + }, + "forward_peak_mib": 14.06298828125, + "out_vs_fp64": { + "max_abs": 0.002045633587364648, + "relative_l2": 0.0003824173102168583 + }, + "lse_vs_fp64": { + "max_abs": 8.756207581228637e-07, + "relative_l2": 3.826813085957934e-08 + }, + "repeat_bitwise": true, + "train_fwd_bwd": { + "median_ms": 1.0676659941673279, + "p95_ms": 1.1006976783275604, + "min_ms": 0.6380280256271362, + "max_ms": 1.111171007156372 + }, + "train_peak_mib": 288.18896484375 + }, + "reference-hip": { + "forward": { + "median_ms": 0.9587844908237457, + "p95_ms": 1.0179794907569886, + "min_ms": 0.9101120233535767, + "max_ms": 1.0238410234451294 + }, + "forward_peak_mib": 36.0625, + "out_vs_fp64": { + "max_abs": 0.000970187372193454, + "relative_l2": 0.00019891305228570232 + }, + "lse_vs_fp64": { + "max_abs": 8.340042594312536e-07, + "relative_l2": 4.5004346410580595e-08 + }, + "repeat_bitwise": true, + "train_fwd_bwd": { + "median_ms": 3.0363115072250366, + "p95_ms": 3.0743917584419247, + "min_ms": 2.9352810382843018, + "max_ms": 3.082179069519043 + }, + "train_peak_mib": 78.1259765625 + }, + "triton-bitwise": { + "forward": { + "median_ms": 1.3660094738006592, + "p95_ms": 1.3787032008171083, + "min_ms": 1.3476020097732544, + "max_ms": 1.4703049659729004 + }, + "forward_peak_mib": 36.06298828125, + "out_vs_fp64": { + "max_abs": 0.000970187372193454, + "relative_l2": 0.00019891305228570232 + }, + "lse_vs_fp64": { + "max_abs": 8.340042594312536e-07, + "relative_l2": 4.5004346410580595e-08 + }, + "repeat_bitwise": true, + "train_fwd_bwd": { + "median_ms": 4.7820844650268555, + "p95_ms": 4.869768595695495, + "min_ms": 4.742486000061035, + "max_ms": 4.90432596206665 + }, + "train_peak_mib": 78.1259765625 + } + }, + "triton_vs_reference": { + "out_mismatched": 0, + "lse_mismatched": 0, + "out_relative_l2": 0.0, + "bitwise": true + }, + "strict_vs_reference": { + "out": { + "max_abs": 0.001953125, + "relative_l2": 0.00043232633045456683 + }, + "lse": { + "max_abs": 9.5367431640625e-07, + "relative_l2": 4.962085268945435e-08 + }, + "out_mismatched": 1200744 + } + }, + { + "dtype": "fp16", + "seq_len": 1024, + "batch": 1, + "q_heads": 32, + "kv_heads": 8, + "head_dim": 128, + "paths": { + "sdpa": { + "forward": { + "median_ms": 0.12863049656152725, + "p95_ms": 0.14349434897303584, + "min_ms": 0.12486500293016434, + "max_ms": 0.17321699857711792 + }, + "forward_peak_mib": 24.12646484375, + "out_vs_fp64": { + "max_abs": 0.0010534945195725953, + "relative_l2": 0.000249303688450972 + }, + "repeat_bitwise": true, + "train_fwd_bwd": { + "median_ms": 0.4398934990167618, + "p95_ms": 0.5000629469752311, + "min_ms": 0.4104900062084198, + "max_ms": 0.5271030068397522 + }, + "train_peak_mib": 64.376953125 + }, + "strict-aiter": { + "forward": { + "median_ms": 0.24233999848365784, + "p95_ms": 0.26022455245256426, + "min_ms": 0.23663200438022614, + "max_ms": 0.2929149866104126 + }, + "forward_peak_mib": 28.12548828125, + "out_vs_fp64": { + "max_abs": 0.0022991352720684866, + "relative_l2": 0.0003967929347774515 + }, + "lse_vs_fp64": { + "max_abs": 1.2495849039950713e-06, + "relative_l2": 4.3279582438585845e-08 + }, + "repeat_bitwise": true, + "train_fwd_bwd": { + "median_ms": 0.8350004851818085, + "p95_ms": 0.9122098207473754, + "min_ms": 0.8214200139045715, + "max_ms": 0.9229720234870911 + }, + "train_peak_mib": 1088.37646484375 + }, + "reference-hip": { + "forward": { + "median_ms": 3.066275477409363, + "p95_ms": 3.1217862606048588, + "min_ms": 3.0435619354248047, + "max_ms": 3.1246020793914795 + }, + "forward_peak_mib": 136.125, + "out_vs_fp64": { + "max_abs": 0.0009756507335656472, + "relative_l2": 0.0002006938787806314 + }, + "lse_vs_fp64": { + "max_abs": 1.0114761641588643e-06, + "relative_l2": 4.218427372109014e-08 + }, + "repeat_bitwise": true, + "train_fwd_bwd": { + "median_ms": 12.0067777633667, + "p95_ms": 12.071291828155518, + "min_ms": 11.944625854492188, + "max_ms": 12.07181453704834 + }, + "train_peak_mib": 284.2509765625 + }, + "triton-bitwise": { + "forward": { + "median_ms": 4.7733314037323, + "p95_ms": 4.806240439414978, + "min_ms": 4.7594709396362305, + "max_ms": 4.925357818603516 + }, + "forward_peak_mib": 136.12548828125, + "out_vs_fp64": { + "max_abs": 0.0009756507335656472, + "relative_l2": 0.0002006938787806314 + }, + "lse_vs_fp64": { + "max_abs": 1.0114761641588643e-06, + "relative_l2": 4.218427372109014e-08 + }, + "repeat_bitwise": true, + "train_fwd_bwd": { + "median_ms": 19.315738677978516, + "p95_ms": 19.46395444869995, + "min_ms": 19.256607055664062, + "max_ms": 19.566869735717773 + }, + "train_peak_mib": 284.2509765625 + } + }, + "triton_vs_reference": { + "out_mismatched": 0, + "lse_mismatched": 0, + "out_relative_l2": 0.0, + "bitwise": true + }, + "strict_vs_reference": { + "out": { + "max_abs": 0.001953125, + "relative_l2": 0.00044664733527359724 + }, + "lse": { + "max_abs": 1.430511474609375e-06, + "relative_l2": 5.213412779092794e-08 + }, + "out_mismatched": 2434520 + } + }, + { + "dtype": "fp16", + "seq_len": 2048, + "batch": 1, + "q_heads": 32, + "kv_heads": 8, + "head_dim": 128, + "paths": { + "sdpa": { + "forward": { + "median_ms": 0.2866459935903549, + "p95_ms": 0.2997168079018593, + "min_ms": 0.2771719992160797, + "max_ms": 0.32239899039268494 + }, + "forward_peak_mib": 48.25146484375, + "out_vs_fp64": { + "max_abs": 0.000909865866452364, + "relative_l2": 0.0002537678065402769 + }, + "repeat_bitwise": true, + "train_fwd_bwd": { + "median_ms": 1.176647961139679, + "p95_ms": 1.226896744966507, + "min_ms": 1.119143009185791, + "max_ms": 1.2345939874649048 + }, + "train_peak_mib": 128.501953125 + }, + "strict-aiter": { + "forward": { + "median_ms": 0.2941969931125641, + "p95_ms": 0.3034307509660721, + "min_ms": 0.2913919985294342, + "max_ms": 0.33882299065589905 + }, + "forward_peak_mib": 56.25048828125, + "out_vs_fp64": { + "max_abs": 0.002052942593323337, + "relative_l2": 0.000397435773927899 + }, + "lse_vs_fp64": { + "max_abs": 2.539945519686171e-06, + "relative_l2": 6.093248379166392e-08 + }, + "repeat_bitwise": true, + "train_fwd_bwd": { + "median_ms": 1.9059720039367676, + "p95_ms": 2.0250025451183316, + "min_ms": 1.81986403465271, + "max_ms": 2.05216908454895 + }, + "train_peak_mib": 4224.75146484375 + }, + "reference-hip": { + "forward": { + "median_ms": 12.534762859344482, + "p95_ms": 12.737589597702026, + "min_ms": 12.424939155578613, + "max_ms": 12.798213958740234 + }, + "forward_peak_mib": 528.25, + "out_vs_fp64": { + "max_abs": 0.000909865866452364, + "relative_l2": 0.00020150767732632786 + }, + "lse_vs_fp64": { + "max_abs": 1.102904860772469e-06, + "relative_l2": 4.146891383633236e-08 + }, + "repeat_bitwise": true, + "train_fwd_bwd": { + "median_ms": 47.51166534423828, + "p95_ms": 47.73727970123291, + "min_ms": 47.379112243652344, + "max_ms": 47.80305862426758 + }, + "train_peak_mib": 1080.5009765625 + }, + "triton-bitwise": { + "forward": { + "median_ms": 19.188227653503418, + "p95_ms": 19.259922790527344, + "min_ms": 19.168479919433594, + "max_ms": 19.44312858581543 + }, + "forward_peak_mib": 528.25048828125, + "out_vs_fp64": { + "max_abs": 0.000909865866452364, + "relative_l2": 0.00020150767732632786 + }, + "lse_vs_fp64": { + "max_abs": 1.102904860772469e-06, + "relative_l2": 4.146891383633236e-08 + }, + "repeat_bitwise": true, + "train_fwd_bwd": { + "median_ms": 75.79496765136719, + "p95_ms": 76.00218696594239, + "min_ms": 75.73639678955078, + "max_ms": 76.14364624023438 + }, + "train_peak_mib": 1080.5009765625 + } + }, + "triton_vs_reference": { + "out_mismatched": 0, + "lse_mismatched": 0, + "out_relative_l2": 0.0, + "bitwise": true + }, + "strict_vs_reference": { + "out": { + "max_abs": 0.001953125, + "relative_l2": 0.00045030041010727414 + }, + "lse": { + "max_abs": 2.86102294921875e-06, + "relative_l2": 6.734176413189059e-08 + }, + "out_mismatched": 4922569 + } + }, + { + "dtype": "fp16", + "seq_len": 4096, + "batch": 1, + "q_heads": 32, + "kv_heads": 8, + "head_dim": 128, + "paths": { + "sdpa": { + "forward": { + "median_ms": 1.0565494894981384, + "p95_ms": 1.0855122208595276, + "min_ms": 1.034296989440918, + "max_ms": 1.088176965713501 + }, + "forward_peak_mib": 96.50146484375, + "out_vs_fp64": { + "max_abs": 0.0009904582683271101, + "relative_l2": 0.0002562693823501264 + }, + "repeat_bitwise": true, + "train_fwd_bwd": { + "median_ms": 4.02467942237854, + "p95_ms": 4.057930660247803, + "min_ms": 3.9799931049346924, + "max_ms": 4.0655198097229 + }, + "train_peak_mib": 257.001953125 + }, + "strict-aiter": { + "forward": { + "median_ms": 0.5812634825706482, + "p95_ms": 0.6200412482023239, + "min_ms": 0.5701770186424255, + "max_ms": 0.6877809762954712 + }, + "forward_peak_mib": 112.50048828125, + "out_vs_fp64": { + "max_abs": 0.001953125, + "relative_l2": 0.00040417579976339364 + }, + "lse_vs_fp64": { + "max_abs": 3.8052896318419016e-06, + "relative_l2": 1.012403669168641e-07 + }, + "repeat_bitwise": true, + "train_fwd_bwd": { + "median_ms": 5.847507476806641, + "p95_ms": 5.891637802124023, + "min_ms": 5.811403751373291, + "max_ms": 5.893435955047607 + }, + "train_peak_mib": 16641.50146484375 + }, + "reference-hip": { + "forward": { + "median_ms": 48.31145668029785, + "p95_ms": 48.41316184997559, + "min_ms": 48.2238883972168, + "max_ms": 48.416053771972656 + }, + "forward_peak_mib": 2080.5, + "out_vs_fp64": { + "max_abs": 0.0009680650127679158, + "relative_l2": 0.00020199318189363435 + }, + "lse_vs_fp64": { + "max_abs": 1.5106917023999245e-06, + "relative_l2": 4.425631521647521e-08 + }, + "repeat_bitwise": true, + "train_fwd_bwd": { + "median_ms": 171.56171417236328, + "p95_ms": 171.73365631103513, + "min_ms": 171.35488891601562, + "max_ms": 171.74888610839844 + }, + "train_peak_mib": 4209.0009765625 + }, + "triton-bitwise": { + "forward": { + "median_ms": 84.9071273803711, + "p95_ms": 85.073588180542, + "min_ms": 84.45022583007812, + "max_ms": 85.12843322753906 + }, + "forward_peak_mib": 2080.50048828125, + "out_vs_fp64": { + "max_abs": 0.0009680650127679158, + "relative_l2": 0.00020199318189363435 + }, + "lse_vs_fp64": { + "max_abs": 1.5106917023999245e-06, + "relative_l2": 4.425631521647521e-08 + }, + "repeat_bitwise": true, + "train_fwd_bwd": { + "median_ms": 300.12152099609375, + "p95_ms": 301.8339218139648, + "min_ms": 295.15631103515625, + "max_ms": 301.9183654785156 + }, + "train_peak_mib": 4209.0009765625 + } + }, + "triton_vs_reference": { + "out_mismatched": 0, + "lse_mismatched": 0, + "out_relative_l2": 0.0, + "bitwise": true + }, + "strict_vs_reference": { + "out": { + "max_abs": 0.001953125, + "relative_l2": 0.0004580324082697752 + }, + "lse": { + "max_abs": 3.814697265625e-06, + "relative_l2": 1.0641371875326284e-07 + }, + "out_mismatched": 9932656 + } + } + ] + }, + "backward_parity": [ + { + "seq_len": 512, + "dq_mismatched": 0, + "dk_mismatched": 0, + "dv_mismatched": 0, + "bitwise": true + }, + { + "seq_len": 1024, + "dq_mismatched": 0, + "dk_mismatched": 0, + "dv_mismatched": 0, + "bitwise": true + }, + { + "seq_len": 2048, + "dq_mismatched": 0, + "dk_mismatched": 0, + "dv_mismatched": 0, + "bitwise": true + }, + { + "seq_len": 4096, + "dq_mismatched": 0, + "dk_mismatched": 0, + "dv_mismatched": 0, + "bitwise": true + } + ], + "batch_composition": [ + { + "seq_len": 512, + "paths": { + "sdpa": { + "batch_gt1_rejected": false, + "out_bitwise": true, + "out_mismatched": 0, + "out_max_abs": 0.0, + "lse_bitwise": null + }, + "strict-aiter": { + "batch_gt1_rejected": true, + "out_bitwise": true, + "out_mismatched": 0, + "note": "core executes one logical batch row per launch" + }, + "reference-hip": { + "batch_gt1_rejected": false, + "out_bitwise": true, + "out_mismatched": 0, + "out_max_abs": 0.0, + "lse_bitwise": true + }, + "triton-bitwise": { + "batch_gt1_rejected": false, + "out_bitwise": true, + "out_mismatched": 0, + "out_max_abs": 0.0, + "lse_bitwise": true + } + } + }, + { + "seq_len": 1024, + "paths": { + "sdpa": { + "batch_gt1_rejected": false, + "out_bitwise": true, + "out_mismatched": 0, + "out_max_abs": 0.0, + "lse_bitwise": null + }, + "strict-aiter": { + "batch_gt1_rejected": true, + "out_bitwise": true, + "out_mismatched": 0, + "note": "core executes one logical batch row per launch" + }, + "reference-hip": { + "batch_gt1_rejected": false, + "out_bitwise": true, + "out_mismatched": 0, + "out_max_abs": 0.0, + "lse_bitwise": true + }, + "triton-bitwise": { + "batch_gt1_rejected": false, + "out_bitwise": true, + "out_mismatched": 0, + "out_max_abs": 0.0, + "lse_bitwise": true + } + } + }, + { + "seq_len": 2048, + "paths": { + "sdpa": { + "batch_gt1_rejected": false, + "out_bitwise": true, + "out_mismatched": 0, + "out_max_abs": 0.0, + "lse_bitwise": null + }, + "strict-aiter": { + "batch_gt1_rejected": true, + "out_bitwise": true, + "out_mismatched": 0, + "note": "core executes one logical batch row per launch" + }, + "reference-hip": { + "batch_gt1_rejected": false, + "out_bitwise": true, + "out_mismatched": 0, + "out_max_abs": 0.0, + "lse_bitwise": true + }, + "triton-bitwise": { + "batch_gt1_rejected": false, + "out_bitwise": true, + "out_mismatched": 0, + "out_max_abs": 0.0, + "lse_bitwise": true + } + } + }, + { + "seq_len": 4096, + "paths": { + "sdpa": { + "batch_gt1_rejected": false, + "out_bitwise": true, + "out_mismatched": 0, + "out_max_abs": 0.0, + "lse_bitwise": null + }, + "strict-aiter": { + "batch_gt1_rejected": true, + "out_bitwise": true, + "out_mismatched": 0, + "note": "core executes one logical batch row per launch" + }, + "reference-hip": { + "batch_gt1_rejected": false, + "out_bitwise": true, + "out_mismatched": 0, + "out_max_abs": 0.0, + "lse_bitwise": true + }, + "triton-bitwise": { + "batch_gt1_rejected": false, + "out_bitwise": true, + "out_mismatched": 0, + "out_max_abs": 0.0, + "lse_bitwise": true + } + } + } + ], + "tp_head_sensitivity": [ + { + "seq_len": 512, + "schedule": "raw_launch", + "tp": 2, + "local_q_heads": 16, + "local_kv_heads": 4, + "out_max_abs": 0.0, + "lse_max_abs": 0.0, + "invariant": true + }, + { + "seq_len": 512, + "schedule": "raw_launch", + "tp": 4, + "local_q_heads": 8, + "local_kv_heads": 2, + "out_max_abs": 0.0, + "lse_max_abs": 0.0, + "invariant": true + }, + { + "seq_len": 512, + "schedule": "raw_launch", + "tp": 8, + "local_q_heads": 4, + "local_kv_heads": 1, + "out_max_abs": 0.0, + "lse_max_abs": 0.0, + "invariant": true + }, + { + "seq_len": 512, + "schedule": "one_kv_group_per_launch", + "tp": 2, + "local_q_heads": 16, + "local_kv_heads": 4, + "out_max_abs": 0.0, + "lse_max_abs": 0.0, + "invariant": true + }, + { + "seq_len": 512, + "schedule": "one_kv_group_per_launch", + "tp": 4, + "local_q_heads": 8, + "local_kv_heads": 2, + "out_max_abs": 0.0, + "lse_max_abs": 0.0, + "invariant": true + }, + { + "seq_len": 512, + "schedule": "one_kv_group_per_launch", + "tp": 8, + "local_q_heads": 4, + "local_kv_heads": 1, + "out_max_abs": 0.0, + "lse_max_abs": 0.0, + "invariant": true + }, + { + "seq_len": 1024, + "schedule": "raw_launch", + "tp": 2, + "local_q_heads": 16, + "local_kv_heads": 4, + "out_max_abs": 0.0078125, + "lse_max_abs": 1.9073486328125e-06, + "invariant": false + }, + { + "seq_len": 1024, + "schedule": "raw_launch", + "tp": 4, + "local_q_heads": 8, + "local_kv_heads": 2, + "out_max_abs": 0.0078125, + "lse_max_abs": 1.9073486328125e-06, + "invariant": false + }, + { + "seq_len": 1024, + "schedule": "raw_launch", + "tp": 8, + "local_q_heads": 4, + "local_kv_heads": 1, + "out_max_abs": 0.0078125, + "lse_max_abs": 1.9073486328125e-06, + "invariant": false + }, + { + "seq_len": 1024, + "schedule": "one_kv_group_per_launch", + "tp": 2, + "local_q_heads": 16, + "local_kv_heads": 4, + "out_max_abs": 0.0, + "lse_max_abs": 0.0, + "invariant": true + }, + { + "seq_len": 1024, + "schedule": "one_kv_group_per_launch", + "tp": 4, + "local_q_heads": 8, + "local_kv_heads": 2, + "out_max_abs": 0.0, + "lse_max_abs": 0.0, + "invariant": true + }, + { + "seq_len": 1024, + "schedule": "one_kv_group_per_launch", + "tp": 8, + "local_q_heads": 4, + "local_kv_heads": 1, + "out_max_abs": 0.0, + "lse_max_abs": 0.0, + "invariant": true + }, + { + "seq_len": 2048, + "schedule": "raw_launch", + "tp": 2, + "local_q_heads": 16, + "local_kv_heads": 4, + "out_max_abs": 0.0, + "lse_max_abs": 0.0, + "invariant": true + }, + { + "seq_len": 2048, + "schedule": "raw_launch", + "tp": 4, + "local_q_heads": 8, + "local_kv_heads": 2, + "out_max_abs": 0.00390625, + "lse_max_abs": 2.86102294921875e-06, + "invariant": false + }, + { + "seq_len": 2048, + "schedule": "raw_launch", + "tp": 8, + "local_q_heads": 4, + "local_kv_heads": 1, + "out_max_abs": 0.001953125, + "lse_max_abs": 2.86102294921875e-06, + "invariant": false + }, + { + "seq_len": 2048, + "schedule": "one_kv_group_per_launch", + "tp": 2, + "local_q_heads": 16, + "local_kv_heads": 4, + "out_max_abs": 0.0, + "lse_max_abs": 0.0, + "invariant": true + }, + { + "seq_len": 2048, + "schedule": "one_kv_group_per_launch", + "tp": 4, + "local_q_heads": 8, + "local_kv_heads": 2, + "out_max_abs": 0.0, + "lse_max_abs": 0.0, + "invariant": true + }, + { + "seq_len": 2048, + "schedule": "one_kv_group_per_launch", + "tp": 8, + "local_q_heads": 4, + "local_kv_heads": 1, + "out_max_abs": 0.0, + "lse_max_abs": 0.0, + "invariant": true + }, + { + "seq_len": 4096, + "schedule": "raw_launch", + "tp": 2, + "local_q_heads": 16, + "local_kv_heads": 4, + "out_max_abs": 0.0, + "lse_max_abs": 0.0, + "invariant": true + }, + { + "seq_len": 4096, + "schedule": "raw_launch", + "tp": 4, + "local_q_heads": 8, + "local_kv_heads": 2, + "out_max_abs": 0.0, + "lse_max_abs": 0.0, + "invariant": true + }, + { + "seq_len": 4096, + "schedule": "raw_launch", + "tp": 8, + "local_q_heads": 4, + "local_kv_heads": 1, + "out_max_abs": 0.00390625, + "lse_max_abs": 4.76837158203125e-06, + "invariant": false + }, + { + "seq_len": 4096, + "schedule": "one_kv_group_per_launch", + "tp": 2, + "local_q_heads": 16, + "local_kv_heads": 4, + "out_max_abs": 0.0, + "lse_max_abs": 0.0, + "invariant": true + }, + { + "seq_len": 4096, + "schedule": "one_kv_group_per_launch", + "tp": 4, + "local_q_heads": 8, + "local_kv_heads": 2, + "out_max_abs": 0.0, + "lse_max_abs": 0.0, + "invariant": true + }, + { + "seq_len": 4096, + "schedule": "one_kv_group_per_launch", + "tp": 8, + "local_q_heads": 4, + "local_kv_heads": 1, + "out_max_abs": 0.0, + "lse_max_abs": 0.0, + "invariant": true + } + ], + "distributed": [ + { + "topology": "tp1_cp2", + "world_size": 2, + "tp_world_size": 1, + "cp_world_size": 2, + "replicas": 1, + "seq_len": 4096, + "local_q_heads": 32, + "local_kv_heads": 8, + "transport": "rccl_ag_rs", + "forward": { + "median_ms": 1.8352065086364746, + "p95_ms": 1.8726499021053316, + "min_ms": 1.8194220066070557, + "max_ms": 1.9234169721603394 + }, + "peak_mib_per_rank": 160.53173828125, + "out_bitwise_vs_cp1": true, + "lse_bitwise_vs_cp1": true, + "repeat_bitwise": true, + "out_mismatched_all_ranks": 0, + "lse_mismatched_all_ranks": 0 + }, + { + "topology": "tp2_cp2", + "world_size": 4, + "tp_world_size": 2, + "cp_world_size": 2, + "replicas": 1, + "seq_len": 4096, + "local_q_heads": 16, + "local_kv_heads": 4, + "transport": "rccl_ag_rs", + "forward": { + "median_ms": 1.2113595008850098, + "p95_ms": 1.3138377845287323, + "min_ms": 1.1833579540252686, + "max_ms": 1.3238470554351807 + }, + "peak_mib_per_rank": 80.28173828125, + "out_bitwise_vs_cp1": true, + "lse_bitwise_vs_cp1": true, + "repeat_bitwise": true, + "out_mismatched_all_ranks": 0, + "lse_mismatched_all_ranks": 0 + }, + { + "topology": "tp1_cp4", + "world_size": 4, + "tp_world_size": 1, + "cp_world_size": 4, + "replicas": 1, + "seq_len": 4096, + "local_q_heads": 32, + "local_kv_heads": 8, + "transport": "rccl_ag_rs", + "forward": { + "median_ms": 1.3972755074501038, + "p95_ms": 1.436299401521683, + "min_ms": 1.3588190078735352, + "max_ms": 1.6069079637527466 + }, + "peak_mib_per_rank": 160.53173828125, + "out_bitwise_vs_cp1": true, + "lse_bitwise_vs_cp1": true, + "repeat_bitwise": true, + "out_mismatched_all_ranks": 0, + "lse_mismatched_all_ranks": 0 + }, + { + "topology": "tp2_cp2_x2", + "world_size": 8, + "tp_world_size": 2, + "cp_world_size": 2, + "replicas": 2, + "seq_len": 4096, + "local_q_heads": 16, + "local_kv_heads": 4, + "transport": "rccl_ag_rs", + "forward": { + "median_ms": 1.2296664714813232, + "p95_ms": 1.294820535182953, + "min_ms": 1.1864819526672363, + "max_ms": 1.3393089771270752 + }, + "peak_mib_per_rank": 80.28173828125, + "out_bitwise_vs_cp1": true, + "lse_bitwise_vs_cp1": true, + "repeat_bitwise": true, + "out_mismatched_all_ranks": 0, + "lse_mismatched_all_ranks": 0 + }, + { + "topology": "tp2_cp4", + "world_size": 8, + "tp_world_size": 2, + "cp_world_size": 4, + "replicas": 1, + "seq_len": 4096, + "local_q_heads": 16, + "local_kv_heads": 4, + "transport": "rccl_ag_rs", + "forward": { + "median_ms": 1.2593510150909424, + "p95_ms": 1.3294484794139863, + "min_ms": 1.2247389554977417, + "max_ms": 1.4252369403839111 + }, + "peak_mib_per_rank": 80.28173828125, + "out_bitwise_vs_cp1": true, + "lse_bitwise_vs_cp1": true, + "repeat_bitwise": true, + "out_mismatched_all_ranks": 0, + "lse_mismatched_all_ranks": 0 + }, + { + "topology": "tp1_cp8", + "world_size": 8, + "tp_world_size": 1, + "cp_world_size": 8, + "replicas": 1, + "seq_len": 4096, + "local_q_heads": 32, + "local_kv_heads": 8, + "transport": "rccl_ag_rs", + "forward": { + "median_ms": 1.411196529865265, + "p95_ms": 2.2028517603874214, + "min_ms": 1.376884937286377, + "max_ms": 3.24902606010437 + }, + "peak_mib_per_rank": 160.53173828125, + "out_bitwise_vs_cp1": true, + "lse_bitwise_vs_cp1": true, + "repeat_bitwise": true, + "out_mismatched_all_ranks": 0, + "lse_mismatched_all_ranks": 0 + } + ] +} diff --git a/benchmarks/results/ws2_rocm_mi300x/single_gpu_grid.png b/benchmarks/results/ws2_rocm_mi300x/single_gpu_grid.png new file mode 100644 index 0000000000000000000000000000000000000000..cbefbc2cf4dc3420ad37a9d5005431f5830b8a45 GIT binary patch literal 288104 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 z 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 Ge*Ry-06Osi literal 0 HcmV?d00001 diff --git a/benchmarks/results/ws2_rocm_mi300x/single_gpu_latency.png b/benchmarks/results/ws2_rocm_mi300x/single_gpu_latency.png new file mode 100644 index 0000000000000000000000000000000000000000..748706926f3220fa19031b4c0464c678d73b453f GIT binary patch literal 132823 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
!ZG(4=#+scGzoA$=2yMvEZ7dU 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%0A%D@NB?!uyhC7~)p59x#+_nJ1 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 F{9pcuV!HqU literal 0 HcmV?d00001 diff --git a/benchmarks/results/ws2_rocm_mi300x/tp_degree_invariance.png b/benchmarks/results/ws2_rocm_mi300x/tp_degree_invariance.png new file mode 100644 index 0000000000000000000000000000000000000000..4d28bbf76361aa1be78b1e8cb35bd55850caff0d GIT binary patch literal 103099 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 Date: Sat, 29 Aug 2026 07:57:45 +0000 Subject: [PATCH 22/35] bench(attention): measure the per-KV-group schedule and correct the ~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) Claude-Session: https://claude.ai/code/session_01824WkgMDxtBKD4Ex2NkfYA --- benchmarks/benchmark_ws2_rocm_attention.py | 33 +++++- benchmarks/results/ws2_rocm_mi300x/report.md | 13 ++- .../results/ws2_rocm_mi300x/results.json | 100 +++++++++++++++++- 3 files changed, 142 insertions(+), 4 deletions(-) diff --git a/benchmarks/benchmark_ws2_rocm_attention.py b/benchmarks/benchmark_ws2_rocm_attention.py index b0f409ed..f1dca12a 100644 --- a/benchmarks/benchmark_ws2_rocm_attention.py +++ b/benchmarks/benchmark_ws2_rocm_attention.py @@ -1017,8 +1017,9 @@ def _write_report(payload: dict[str, Any], output_directory: Path) -> None: "as in PR #325, no accuracy comparison is mixed into the speed table." ) add( - " - `strict-aiter`: `StrictRocmAiterCKAttentionCore`, the ROCm production core " - "(AITER CK dense MHA, non-split API, one logical batch row per launch)." + " - `strict-aiter`: `StrictRocmAiterCKAttentionCore` called **once for all heads**. " + "This is the core, not the production schedule: the Vime provider launches it once " + "per (batch row, KV group). See the per-KV-group schedule table for that cost." ) add( " - `reference-hip`: `_C.deterministic_attention_forward/backward`, the materializing " @@ -1195,6 +1196,34 @@ def _write_report(payload: dict[str, Any], output_directory: Path) -> None: ) add("") + # ---- schedule cost + schedule = payload.get("tp_schedule_cost") or [] + if schedule: + add("## Cost of the per-KV-group launch schedule") + add("") + add( + "§ TP-degree invariance is bought by launching the core once per " + "`(batch row, KV group)` instead of once for all heads. This table is that " + "bill. `raw_launch` is one launch for all heads and is **not** the production " + "schedule; `per_kv_group` is what the Vime provider actually runs " + "(`Hkv` launches per row)." + ) + add("") + add( + "| S | Launches | sdpa (ms) | raw_launch (ms) | per_kv_group (ms) | " + "vs raw | vs sdpa |" + ) + add("|---:|---:|---:|---:|---:|---:|---:|") + for row in schedule: + raw = row["raw_launch"]["median_ms"] + group = row["one_kv_group_per_launch"]["median_ms"] + sdpa = row["sdpa"]["median_ms"] + add( + f"| {row['seq_len']} | {row['launches']} | {sdpa:.4f} | {raw:.4f} | " + f"{group:.4f} | {group / raw:.2f}x | {group / sdpa:.2f}x |" + ) + add("") + # ---- distributed distributed = payload.get("distributed") or [] if distributed: diff --git a/benchmarks/results/ws2_rocm_mi300x/report.md b/benchmarks/results/ws2_rocm_mi300x/report.md index e211e9eb..2f26369e 100644 --- a/benchmarks/results/ws2_rocm_mi300x/report.md +++ b/benchmarks/results/ws2_rocm_mi300x/report.md @@ -23,7 +23,7 @@ - Operator shape: `Hq=32`, `Hkv=8`, `D=128`, `B=1`, causal; sequence sweep 512, 1024, 2048, 4096. - Measured paths: - `sdpa`: `torch.nn.functional.scaled_dot_product_attention`. **Speed baseline only** — as in PR #325, no accuracy comparison is mixed into the speed table. - - `strict-aiter`: `StrictRocmAiterCKAttentionCore`, the ROCm production core (AITER CK dense MHA, non-split API, one logical batch row per launch). + - `strict-aiter`: `StrictRocmAiterCKAttentionCore` called **once for all heads**. This is the core, not the production schedule: the Vime provider launches it once per (batch row, KV group). See the per-KV-group schedule table for that cost. - `reference-hip`: `_C.deterministic_attention_forward/backward`, the materializing FP32 reference core hipified from the shared `.cu`. - `triton-bitwise`: `TritonDeterministicAttentionOp`, whose contract is bit-identity with `reference-hip`. - Timing: CUDA events, median and p95. Peak memory is the per-call increase in `torch.cuda.max_memory_allocated` above what was live before the call. @@ -215,6 +215,17 @@ A head shard computed under TP=N versus the same slice of an unsharded run. TP p | 4096 | one_kv_group_per_launch | 4 | 8 | 2 | 0.000000e+00 | 0.000000e+00 | yes | | 4096 | one_kv_group_per_launch | 8 | 4 | 1 | 0.000000e+00 | 0.000000e+00 | yes | +## Cost of the per-KV-group launch schedule + +§ TP-degree invariance is bought by launching the core once per `(batch row, KV group)` instead of once for all heads. This table is that bill. `raw_launch` is one launch for all heads and is **not** the production schedule; `per_kv_group` is what the Vime provider actually runs (`Hkv` launches per row). + +| S | Launches | sdpa (ms) | raw_launch (ms) | per_kv_group (ms) | vs raw | vs sdpa | +|---:|---:|---:|---:|---:|---:|---:| +| 512 | 8 | 0.0721 | 0.2488 | 1.7717 | 7.12x | 24.56x | +| 1024 | 8 | 0.1355 | 0.2467 | 1.8047 | 7.31x | 13.32x | +| 2048 | 8 | 0.2848 | 0.3067 | 1.7629 | 5.75x | 6.19x | +| 4096 | 8 | 0.7679 | 0.5965 | 2.4506 | 4.11x | 3.19x | + ## Distributed CP (RCCL AG/RS transport) Schedule: all-gather Q/K/V and the position ids over the CP group, run the strict core once on the full sequence, reduce-scatter `(out, lse)` back to this rank's query range. Acceptance is bitwise against a CP=1 run of the same core. diff --git a/benchmarks/results/ws2_rocm_mi300x/results.json b/benchmarks/results/ws2_rocm_mi300x/results.json index 86f6f55d..443477a5 100644 --- a/benchmarks/results/ws2_rocm_mi300x/results.json +++ b/benchmarks/results/ws2_rocm_mi300x/results.json @@ -1563,5 +1563,103 @@ "out_mismatched_all_ranks": 0, "lse_mismatched_all_ranks": 0 } + ], + "tp_schedule_cost": [ + { + "seq_len": 512, + "raw_launch": { + "median_ms": 0.2488495036959648, + "p95_ms": 0.2928414404392243, + "min_ms": 0.23362700641155243, + "max_ms": 0.31050100922584534 + }, + "raw_launch_peak_mib": 14.06298828125, + "one_kv_group_per_launch": { + "median_ms": 1.77167147397995, + "p95_ms": 1.8451894819736483, + "min_ms": 1.7313319444656372, + "max_ms": 1.953421950340271 + }, + "one_kv_group_per_launch_peak_mib": 8.125, + "sdpa": { + "median_ms": 0.07214749976992607, + "p95_ms": 0.10037899836897851, + "min_ms": 0.06942299753427505, + "max_ms": 0.11312799900770187 + }, + "launches": 8 + }, + { + "seq_len": 1024, + "raw_launch": { + "median_ms": 0.24672599881887436, + "p95_ms": 0.28001638650894184, + "min_ms": 0.24015699326992035, + "max_ms": 0.5281450152397156 + }, + "raw_launch_peak_mib": 28.12548828125, + "one_kv_group_per_launch": { + "median_ms": 1.8047409653663635, + "p95_ms": 2.558191692829132, + "min_ms": 1.7322529554367065, + "max_ms": 2.5664119720458984 + }, + "one_kv_group_per_launch_peak_mib": 16.25, + "sdpa": { + "median_ms": 0.1354615017771721, + "p95_ms": 0.14607899487018586, + "min_ms": 0.11909700185060501, + "max_ms": 0.1464969962835312 + }, + "launches": 8 + }, + { + "seq_len": 2048, + "raw_launch": { + "median_ms": 0.30673499405384064, + "p95_ms": 0.3156995430588722, + "min_ms": 0.2910729944705963, + "max_ms": 0.3489980101585388 + }, + "raw_launch_peak_mib": 56.25048828125, + "one_kv_group_per_launch": { + "median_ms": 1.7629389762878418, + "p95_ms": 1.8040032982826233, + "min_ms": 1.7420680522918701, + "max_ms": 1.8337639570236206 + }, + "one_kv_group_per_launch_peak_mib": 32.5, + "sdpa": { + "median_ms": 0.28476350009441376, + "p95_ms": 0.29218203872442244, + "min_ms": 0.279776006937027, + "max_ms": 0.31859299540519714 + }, + "launches": 8 + }, + { + "seq_len": 4096, + "raw_launch": { + "median_ms": 0.5964860022068024, + "p95_ms": 0.6361419618129731, + "min_ms": 0.5617139935493469, + "max_ms": 0.6870610117912292 + }, + "raw_launch_peak_mib": 112.50048828125, + "one_kv_group_per_launch": { + "median_ms": 2.450620412826538, + "p95_ms": 4.374842977523804, + "min_ms": 2.0698349475860596, + "max_ms": 4.4635910987854 + }, + "one_kv_group_per_launch_peak_mib": 65.0, + "sdpa": { + "median_ms": 0.7679409980773926, + "p95_ms": 0.8042640864849091, + "min_ms": 0.6916279792785645, + "max_ms": 0.8177750110626221 + }, + "launches": 8 + } ] -} +} \ No newline at end of file From e24b5944b7e5c65e27e63626d597ce9c23d50958 Mon Sep 17 00:00:00 2001 From: inaniloquentee Date: Sat, 29 Aug 2026 08:01:23 +0000 Subject: [PATCH 23/35] bench(attention): drop the stale pre-per-KV-group MI300X performance 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) Claude-Session: https://claude.ai/code/session_01824WkgMDxtBKD4Ex2NkfYA --- .../results/pr319_rocm_mi300x/report.md | 106 ---- .../results/pr319_rocm_mi300x/results.json | 483 ------------------ 2 files changed, 589 deletions(-) delete mode 100644 benchmarks/results/pr319_rocm_mi300x/report.md delete mode 100644 benchmarks/results/pr319_rocm_mi300x/results.json diff --git a/benchmarks/results/pr319_rocm_mi300x/report.md b/benchmarks/results/pr319_rocm_mi300x/report.md deleted file mode 100644 index e0577d94..00000000 --- a/benchmarks/results/pr319_rocm_mi300x/report.md +++ /dev/null @@ -1,106 +0,0 @@ -# PR #319 — strict ROCm attention, MI300X operator benchmark - -Operator-only: seeded Q/K/V, no checkpoint, tokenizer, or serving engine. - -| Field | Value | -|---|---| -| `gpu` | AMD Instinct MI300X | -| `arch` | gfx942:sramecc+:xnack- | -| `device_count` | 8 | -| `torch` | 2.12.0+rocm7.14.0a20260608 | -| `hip` | 7.14.60850 | -| `dtype` | bf16 | -| `q_heads` | 32 | -| `kv_heads` | 8 | -| `head_dim` | 128 | - -Backends: `native` = PyTorch SDPA with KV heads expanded; `triton` = flash-attn ROCm -Triton backend; `strict` = `aiter.rocm.ck_dense_mha` through WS2 contract dispatch. - -## Latency and peak memory (BF16, causal prefill) - -| B | S | Backend | Fwd median (ms) | Fwd p95 (ms) | Fwd peak MiB | Fwd+bwd median (ms) | Fwd+bwd peak MiB | -|---:|---:|---|---:|---:|---:|---:|---:| -| 1 | 1024 | native | 0.1449 | 0.1503 | 36.1 | 0.4758 | 84.3 | -| 1 | 1024 | triton | 0.3021 | 0.3315 | 32.1 | 1.2487 | 52.2 | -| 1 | 1024 | strict | 0.3980 | 0.4205 | 40.1 | 1.0305 | 1108.5 | -| 1 | 2048 | native | 0.2972 | 0.3010 | 72.3 | 1.0875 | 168.8 | -| 1 | 2048 | triton | 0.6379 | 0.6535 | 64.2 | 2.5059 | 104.5 | -| 1 | 2048 | strict | 0.4265 | 0.4365 | 80.3 | 2.1164 | 4265.0 | -| 1 | 4096 | native | 0.7020 | 0.7232 | 144.5 | 3.2428 | 337.5 | -| 1 | 4096 | triton | 1.4280 | 1.4877 | 128.5 | 4.6182 | 209.0 | -| 1 | 4096 | strict | 0.7343 | 0.7502 | 160.5 | 5.9652 | 16722.0 | -| 2 | 2048 | native | 0.4499 | 0.4715 | 144.5 | 1.9132 | 337.5 | -| 2 | 2048 | triton | 0.9764 | 0.9864 | 128.5 | 2.6696 | 209.0 | -| 2 | 2048 | strict | 0.7826 | 0.8113 | 120.5 | 3.9917 | 4369.3 | -| 4 | 2048 | native | 0.8225 | 0.8474 | 289.0 | 3.5825 | 675.0 | -| 4 | 2048 | triton | 1.7539 | 1.7674 | 257.0 | 5.1151 | 418.0 | -| 4 | 2048 | strict | 1.3809 | 1.4118 | 226.1 | 7.6638 | 4610.3 | - -## Cost of the deterministic backward - -Raw AITER `mha_bwd`, B=1, toggling only the `deterministic` flag. Peak values include the -already-resident forward tensors, so read the absolute det=on column for scaling. - -| S | det=on median (ms) | det=off median (ms) | Time | det=on peak MiB | det=off peak MiB | -|---:|---:|---:|---:|---:|---:| -| 1024 | 0.4490 | 0.1903 | 2.36x | 1188.3 | 180.3 | -| 2048 | 1.3386 | 0.5687 | 2.35x | 4328.5 | 264.5 | -| 4096 | 5.0450 | 2.0415 | 2.47x | 16753.0 | 433.0 | - -Deterministic backward peak memory scales as O(S^2): each doubling of S quadruples it. - -## Batch-composition invariance of raw AITER - -Batch vs the same rows submitted one at a time. Shape-dependent, which is why the strict -core materializes one logical row per call. - -| B | S | out max abs | lse max abs | Invariant | -|---:|---:|---:|---:|---| -| 2 | 128 | 0.000000e+00 | 0.000000e+00 | yes | -| 2 | 256 | 0.000000e+00 | 0.000000e+00 | yes | -| 2 | 512 | 1.562500e-02 | 9.536743e-07 | **no** | -| 2 | 1024 | 0.000000e+00 | 0.000000e+00 | yes | -| 2 | 2048 | 0.000000e+00 | 0.000000e+00 | yes | -| 2 | 4096 | 0.000000e+00 | 0.000000e+00 | yes | -| 4 | 128 | 0.000000e+00 | 0.000000e+00 | yes | -| 4 | 256 | 7.812500e-03 | 9.536743e-07 | **no** | -| 4 | 512 | 1.562500e-02 | 9.536743e-07 | **no** | -| 4 | 1024 | 0.000000e+00 | 0.000000e+00 | yes | -| 4 | 2048 | 0.000000e+00 | 0.000000e+00 | yes | -| 4 | 4096 | 0.000000e+00 | 0.000000e+00 | yes | - -Through the provider, every shape above is bitwise identical (max abs `0`). - -## TP-degree invariance of raw AITER - -A head shard computed under TP=N vs the same slice of an unsharded run. TP performs no -cross-rank reduction in attention, so any nonzero value here means the kernel's result -depends on how many heads shared the launch - i.e. changing TP degree changes the numbers. - -| S | TP | Local Hq | Local Hkv | out max abs | lse max abs | Invariant | -|---:|---:|---:|---:|---:|---:|---| -| 512 | 2 | 16 | 4 | 0.000000e+00 | 0.000000e+00 | yes | -| 512 | 4 | 8 | 2 | 0.000000e+00 | 0.000000e+00 | yes | -| 512 | 8 | 4 | 1 | 0.000000e+00 | 0.000000e+00 | yes | -| 1024 | 2 | 16 | 4 | 3.906250e-03 | 1.430511e-06 | **no** | -| 1024 | 4 | 8 | 2 | 3.906250e-03 | 1.430511e-06 | **no** | -| 1024 | 8 | 4 | 1 | 3.906250e-03 | 1.430511e-06 | **no** | -| 2048 | 2 | 16 | 4 | 0.000000e+00 | 0.000000e+00 | yes | -| 2048 | 4 | 8 | 2 | 7.812500e-03 | 2.861023e-06 | **no** | -| 2048 | 8 | 4 | 1 | 7.812500e-03 | 2.861023e-06 | **no** | -| 4096 | 2 | 16 | 4 | 0.000000e+00 | 0.000000e+00 | yes | -| 4096 | 4 | 8 | 2 | 0.000000e+00 | 0.000000e+00 | yes | -| 4096 | 8 | 4 | 1 | 7.812500e-03 | 4.768372e-06 | **no** | - -RL-Kernel does not remove this dependence (per-KV-group execution would, at 2.7-3.9x -forward time). It binds the degree instead: `AttentionContract.cross_rank_fingerprint` -includes `tp_world_size`, and `validate_cross_config_alignment` fails closed when training -and rollout disagree. - -## Reproduce - -```bash -python benchmarks/benchmark_rocm_attention.py \ - --output benchmarks/results/pr319_rocm_mi300x/results.json -``` diff --git a/benchmarks/results/pr319_rocm_mi300x/results.json b/benchmarks/results/pr319_rocm_mi300x/results.json deleted file mode 100644 index 3d9f93fe..00000000 --- a/benchmarks/results/pr319_rocm_mi300x/results.json +++ /dev/null @@ -1,483 +0,0 @@ -{ - "environment": { - "gpu": "AMD Instinct MI300X", - "arch": "gfx942:sramecc+:xnack-", - "device_count": 8, - "torch": "2.12.0+rocm7.14.0a20260608", - "hip": "7.14.60850", - "dtype": "bf16", - "q_heads": 32, - "kv_heads": 8, - "head_dim": 128 - }, - "latency": [ - { - "batch": 1, - "seq_len": 1024, - "backend": "native", - "forward": { - "median_ms": 0.1449, - "p95_ms": 0.1503, - "peak_mib": 36.1 - }, - "forward_backward": { - "median_ms": 0.4758, - "p95_ms": 0.4814, - "peak_mib": 84.3 - } - }, - { - "batch": 1, - "seq_len": 1024, - "backend": "triton", - "forward": { - "median_ms": 0.3021, - "p95_ms": 0.3315, - "peak_mib": 32.1 - }, - "forward_backward": { - "median_ms": 1.2487, - "p95_ms": 1.3242, - "peak_mib": 52.2 - } - }, - { - "batch": 1, - "seq_len": 1024, - "backend": "strict", - "forward": { - "median_ms": 0.398, - "p95_ms": 0.4205, - "peak_mib": 40.1 - }, - "forward_backward": { - "median_ms": 1.0305, - "p95_ms": 1.0536, - "peak_mib": 1108.5 - } - }, - { - "batch": 1, - "seq_len": 2048, - "backend": "native", - "forward": { - "median_ms": 0.2972, - "p95_ms": 0.301, - "peak_mib": 72.3 - }, - "forward_backward": { - "median_ms": 1.0875, - "p95_ms": 1.1026, - "peak_mib": 168.8 - } - }, - { - "batch": 1, - "seq_len": 2048, - "backend": "triton", - "forward": { - "median_ms": 0.6379, - "p95_ms": 0.6535, - "peak_mib": 64.2 - }, - "forward_backward": { - "median_ms": 2.5059, - "p95_ms": 2.5251, - "peak_mib": 104.5 - } - }, - { - "batch": 1, - "seq_len": 2048, - "backend": "strict", - "forward": { - "median_ms": 0.4265, - "p95_ms": 0.4365, - "peak_mib": 80.3 - }, - "forward_backward": { - "median_ms": 2.1164, - "p95_ms": 2.2624, - "peak_mib": 4265.0 - } - }, - { - "batch": 1, - "seq_len": 4096, - "backend": "native", - "forward": { - "median_ms": 0.702, - "p95_ms": 0.7232, - "peak_mib": 144.5 - }, - "forward_backward": { - "median_ms": 3.2428, - "p95_ms": 3.2862, - "peak_mib": 337.5 - } - }, - { - "batch": 1, - "seq_len": 4096, - "backend": "triton", - "forward": { - "median_ms": 1.428, - "p95_ms": 1.4877, - "peak_mib": 128.5 - }, - "forward_backward": { - "median_ms": 4.6182, - "p95_ms": 4.6719, - "peak_mib": 209.0 - } - }, - { - "batch": 1, - "seq_len": 4096, - "backend": "strict", - "forward": { - "median_ms": 0.7343, - "p95_ms": 0.7502, - "peak_mib": 160.5 - }, - "forward_backward": { - "median_ms": 5.9652, - "p95_ms": 6.0334, - "peak_mib": 16722.0 - } - }, - { - "batch": 2, - "seq_len": 2048, - "backend": "native", - "forward": { - "median_ms": 0.4499, - "p95_ms": 0.4715, - "peak_mib": 144.5 - }, - "forward_backward": { - "median_ms": 1.9132, - "p95_ms": 1.943, - "peak_mib": 337.5 - } - }, - { - "batch": 2, - "seq_len": 2048, - "backend": "triton", - "forward": { - "median_ms": 0.9764, - "p95_ms": 0.9864, - "peak_mib": 128.5 - }, - "forward_backward": { - "median_ms": 2.6696, - "p95_ms": 2.6816, - "peak_mib": 209.0 - } - }, - { - "batch": 2, - "seq_len": 2048, - "backend": "strict", - "forward": { - "median_ms": 0.7826, - "p95_ms": 0.8113, - "peak_mib": 120.5 - }, - "forward_backward": { - "median_ms": 3.9917, - "p95_ms": 4.0312, - "peak_mib": 4369.3 - } - }, - { - "batch": 4, - "seq_len": 2048, - "backend": "native", - "forward": { - "median_ms": 0.8225, - "p95_ms": 0.8474, - "peak_mib": 289.0 - }, - "forward_backward": { - "median_ms": 3.5825, - "p95_ms": 3.6547, - "peak_mib": 675.0 - } - }, - { - "batch": 4, - "seq_len": 2048, - "backend": "triton", - "forward": { - "median_ms": 1.7539, - "p95_ms": 1.7674, - "peak_mib": 257.0 - }, - "forward_backward": { - "median_ms": 5.1151, - "p95_ms": 5.1429, - "peak_mib": 418.0 - } - }, - { - "batch": 4, - "seq_len": 2048, - "backend": "strict", - "forward": { - "median_ms": 1.3809, - "p95_ms": 1.4118, - "peak_mib": 226.1 - }, - "forward_backward": { - "median_ms": 7.6638, - "p95_ms": 7.7376, - "peak_mib": 4610.3 - } - } - ], - "determinism_cost": [ - { - "seq_len": 1024, - "deterministic": { - "median_ms": 0.449, - "p95_ms": 0.4681, - "peak_mib": 1188.3 - }, - "non_deterministic": { - "median_ms": 0.1903, - "p95_ms": 0.197, - "peak_mib": 180.3 - }, - "time_ratio": 2.36, - "memory_ratio": 6.6 - }, - { - "seq_len": 2048, - "deterministic": { - "median_ms": 1.3386, - "p95_ms": 1.379, - "peak_mib": 4328.5 - }, - "non_deterministic": { - "median_ms": 0.5687, - "p95_ms": 0.5761, - "peak_mib": 264.5 - }, - "time_ratio": 2.35, - "memory_ratio": 16.4 - }, - { - "seq_len": 4096, - "deterministic": { - "median_ms": 5.045, - "p95_ms": 5.0906, - "peak_mib": 16753.0 - }, - "non_deterministic": { - "median_ms": 2.0415, - "p95_ms": 2.072, - "peak_mib": 433.0 - }, - "time_ratio": 2.47, - "memory_ratio": 38.7 - } - ], - "batch_composition": [ - { - "batch": 2, - "seq_len": 128, - "raw_aiter_out_max_abs": 0.0, - "raw_aiter_lse_max_abs": 0.0, - "raw_aiter_is_batch_invariant": true - }, - { - "batch": 2, - "seq_len": 256, - "raw_aiter_out_max_abs": 0.0, - "raw_aiter_lse_max_abs": 0.0, - "raw_aiter_is_batch_invariant": true - }, - { - "batch": 2, - "seq_len": 512, - "raw_aiter_out_max_abs": 0.015625, - "raw_aiter_lse_max_abs": 9.5367431640625e-07, - "raw_aiter_is_batch_invariant": false - }, - { - "batch": 2, - "seq_len": 1024, - "raw_aiter_out_max_abs": 0.0, - "raw_aiter_lse_max_abs": 0.0, - "raw_aiter_is_batch_invariant": true - }, - { - "batch": 2, - "seq_len": 2048, - "raw_aiter_out_max_abs": 0.0, - "raw_aiter_lse_max_abs": 0.0, - "raw_aiter_is_batch_invariant": true - }, - { - "batch": 2, - "seq_len": 4096, - "raw_aiter_out_max_abs": 0.0, - "raw_aiter_lse_max_abs": 0.0, - "raw_aiter_is_batch_invariant": true - }, - { - "batch": 4, - "seq_len": 128, - "raw_aiter_out_max_abs": 0.0, - "raw_aiter_lse_max_abs": 0.0, - "raw_aiter_is_batch_invariant": true - }, - { - "batch": 4, - "seq_len": 256, - "raw_aiter_out_max_abs": 0.0078125, - "raw_aiter_lse_max_abs": 9.5367431640625e-07, - "raw_aiter_is_batch_invariant": false - }, - { - "batch": 4, - "seq_len": 512, - "raw_aiter_out_max_abs": 0.015625, - "raw_aiter_lse_max_abs": 9.5367431640625e-07, - "raw_aiter_is_batch_invariant": false - }, - { - "batch": 4, - "seq_len": 1024, - "raw_aiter_out_max_abs": 0.0, - "raw_aiter_lse_max_abs": 0.0, - "raw_aiter_is_batch_invariant": true - }, - { - "batch": 4, - "seq_len": 2048, - "raw_aiter_out_max_abs": 0.0, - "raw_aiter_lse_max_abs": 0.0, - "raw_aiter_is_batch_invariant": true - }, - { - "batch": 4, - "seq_len": 4096, - "raw_aiter_out_max_abs": 0.0, - "raw_aiter_lse_max_abs": 0.0, - "raw_aiter_is_batch_invariant": true - } - ], - "tp_head_count_sensitivity": [ - { - "seq_len": 512, - "tp": 2, - "local_q_heads": 16, - "local_kv_heads": 4, - "out_max_abs": 0.0, - "lse_max_abs": 0.0, - "tp_degree_invariant": true - }, - { - "seq_len": 512, - "tp": 4, - "local_q_heads": 8, - "local_kv_heads": 2, - "out_max_abs": 0.0, - "lse_max_abs": 0.0, - "tp_degree_invariant": true - }, - { - "seq_len": 512, - "tp": 8, - "local_q_heads": 4, - "local_kv_heads": 1, - "out_max_abs": 0.0, - "lse_max_abs": 0.0, - "tp_degree_invariant": true - }, - { - "seq_len": 1024, - "tp": 2, - "local_q_heads": 16, - "local_kv_heads": 4, - "out_max_abs": 0.00390625, - "lse_max_abs": 1.430511474609375e-06, - "tp_degree_invariant": false - }, - { - "seq_len": 1024, - "tp": 4, - "local_q_heads": 8, - "local_kv_heads": 2, - "out_max_abs": 0.00390625, - "lse_max_abs": 1.430511474609375e-06, - "tp_degree_invariant": false - }, - { - "seq_len": 1024, - "tp": 8, - "local_q_heads": 4, - "local_kv_heads": 1, - "out_max_abs": 0.00390625, - "lse_max_abs": 1.430511474609375e-06, - "tp_degree_invariant": false - }, - { - "seq_len": 2048, - "tp": 2, - "local_q_heads": 16, - "local_kv_heads": 4, - "out_max_abs": 0.0, - "lse_max_abs": 0.0, - "tp_degree_invariant": true - }, - { - "seq_len": 2048, - "tp": 4, - "local_q_heads": 8, - "local_kv_heads": 2, - "out_max_abs": 0.0078125, - "lse_max_abs": 2.86102294921875e-06, - "tp_degree_invariant": false - }, - { - "seq_len": 2048, - "tp": 8, - "local_q_heads": 4, - "local_kv_heads": 1, - "out_max_abs": 0.0078125, - "lse_max_abs": 2.86102294921875e-06, - "tp_degree_invariant": false - }, - { - "seq_len": 4096, - "tp": 2, - "local_q_heads": 16, - "local_kv_heads": 4, - "out_max_abs": 0.0, - "lse_max_abs": 0.0, - "tp_degree_invariant": true - }, - { - "seq_len": 4096, - "tp": 4, - "local_q_heads": 8, - "local_kv_heads": 2, - "out_max_abs": 0.0, - "lse_max_abs": 0.0, - "tp_degree_invariant": true - }, - { - "seq_len": 4096, - "tp": 8, - "local_q_heads": 4, - "local_kv_heads": 1, - "out_max_abs": 0.0078125, - "lse_max_abs": 4.76837158203125e-06, - "tp_degree_invariant": false - } - ] -} \ No newline at end of file From f4602c069c4f88053ad2fe27d96d58e864e1ae87 Mon Sep 17 00:00:00 2001 From: inaniloquentee Date: Sat, 29 Aug 2026 08:32:59 +0000 Subject: [PATCH 24/35] bench(attention): support CPU and CUDA hosts, add the exactness heatmap 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) Claude-Session: https://claude.ai/code/session_01824WkgMDxtBKD4Ex2NkfYA --- benchmarks/benchmark_ws2_rocm_attention.py | 627 ++++++++++++++---- .../distributed_cp_latency.png | Bin 46377 -> 65704 bytes .../ws2_rocm_mi300x/exactness_matrix.png | Bin 0 -> 155510 bytes benchmarks/results/ws2_rocm_mi300x/report.md | 20 +- .../results/ws2_rocm_mi300x/results.json | 86 ++- 5 files changed, 570 insertions(+), 163 deletions(-) create mode 100644 benchmarks/results/ws2_rocm_mi300x/exactness_matrix.png diff --git a/benchmarks/benchmark_ws2_rocm_attention.py b/benchmarks/benchmark_ws2_rocm_attention.py index f1dca12a..4078b7d8 100644 --- a/benchmarks/benchmark_ws2_rocm_attention.py +++ b/benchmarks/benchmark_ws2_rocm_attention.py @@ -18,18 +18,19 @@ accuracy claim is mixed into the speed comparison. - ``strict-aiter`` ``StrictRocmAiterCKAttentionCore`` — the ROCm production core (AITER CK dense MHA, non-split API). -- ``reference-hip`` ``_C.deterministic_attention_forward/backward`` — the materializing +- ``reference-native`` ``_C.deterministic_attention_forward/backward`` — the materializing FP32 reference core, hipified from the shared ``.cu``. - ``triton-bitwise`` ``TritonDeterministicAttentionOp`` — the Triton port whose - contract is bit-identity with ``reference-hip``. + contract is bit-identity with ``reference-native``. -The headline column is ``triton-bitwise`` versus ``reference-hip``: acceptance is +The headline column is ``triton-bitwise`` versus ``reference-native``: acceptance is 0 mismatched elements on out, lse, dQ, dK and dV. """ from __future__ import annotations import argparse +import gc import json import math import multiprocessing as mp @@ -38,6 +39,8 @@ import queue import statistics import tempfile +import threading +import time import traceback from datetime import timedelta from pathlib import Path @@ -135,10 +138,67 @@ def _gpu_event_samples(function: Callable[[], Any], *, warmup: int, samples: int return [float(start.elapsed_time(end)) for start, end in events] -def _peak_memory_mib(function: Callable[[], Any]) -> float: +def _host_wall_samples(function: Callable[[], Any], *, warmup: int, samples: int) -> list[float]: + """Wall-clock timing for host execution, where CUDA events do not apply.""" + for _ in range(warmup): + function() + timings = [] + for _ in range(samples): + start = time.perf_counter() + function() + timings.append((time.perf_counter() - start) * 1000.0) + return timings + + +def _timed_samples( + function: Callable[[], Any], *, warmup: int, samples: int, device: torch.device +) -> list[float]: + if device.type == "cuda": + return _gpu_event_samples(function, warmup=warmup, samples=samples) + return _host_wall_samples(function, warmup=warmup, samples=samples) + + +def _rss_mib() -> float: + with open("/proc/self/statm", "r", encoding="ascii") as handle: + resident_pages = int(handle.read().split()[1]) + return resident_pages * os.sysconf("SC_PAGE_SIZE") / (1024.0 * 1024.0) + + +def _host_peak_rss_mib(function: Callable[[], Any]) -> float: + """Peak resident-set increase during one host call, sampled from /proc. + + The closest host analogue of ``torch.cuda.max_memory_allocated``, but an RSS + high-water delta rather than an allocator statistic: it includes caching-allocator + reuse and page granularity, so it is an approximation and not directly comparable + to the device figures. A call served from already-resident pages can report ~0. + """ + gc.collect() + baseline = _rss_mib() + peak = baseline + stop = threading.Event() + + def sampler() -> None: + nonlocal peak + while not stop.is_set(): + peak = max(peak, _rss_mib()) + stop.wait(0.001) + + thread = threading.Thread(target=sampler, daemon=True) + thread.start() + try: + function() + finally: + stop.set() + thread.join() + return float(max(peak, _rss_mib()) - baseline) + + +def _peak_memory_mib(function: Callable[[], Any], device: torch.device) -> float: """Peak memory used by one call, above what was live before it.""" + if device.type != "cuda": + return _host_peak_rss_mib(function) torch.cuda.synchronize() - torch.cuda.empty_cache() + _empty_cache(device) torch.cuda.reset_peak_memory_stats() baseline = torch.cuda.memory_allocated() function() @@ -146,6 +206,13 @@ def _peak_memory_mib(function: Callable[[], Any]) -> float: return float((torch.cuda.max_memory_allocated() - baseline) / (1024.0 * 1024.0)) +def _empty_cache(device: torch.device) -> None: + if device.type == "cuda": + torch.cuda.empty_cache() + else: + gc.collect() + + # --------------------------------------------------------------------------- # Attention paths # --------------------------------------------------------------------------- @@ -216,11 +283,27 @@ def _sdpa_forward(q, k, v, *, causal, scale): class _Paths: """Lazily constructed attention paths, so a missing backend skips one row.""" - def __init__(self) -> None: + def __init__(self, device: torch.device) -> None: + self.device = device self.errors: dict[str, str] = {} - self.strict = self._try("strict-aiter", self._make_strict) - self.reference = self._try("reference-hip", self._make_reference) - self.triton = self._try("triton-bitwise", self._make_triton) + # The PyTorch reference is the only non-SDPA path that also runs on the host. + self.native = self._try("pytorch-native", self._make_native) + self.is_rocm = torch.version.hip is not None + if device.type == "cuda": + if self.is_rocm: + self.strict = self._try("strict-aiter", self._make_strict) + self.strict_fa4 = None + self.errors["strict-fa4"] = "CUDA-only path; this run is ROCm" + else: + self.strict = None + self.errors["strict-aiter"] = "ROCm-only path; this run is CUDA" + self.strict_fa4 = self._try("strict-fa4", self._make_strict_fa4) + self.reference = self._try("reference-native", self._make_reference) + self.triton = self._try("triton-bitwise", self._make_triton) + else: + self.strict = self.strict_fa4 = self.reference = self.triton = None + for name in ("strict-aiter", "strict-fa4", "reference-native", "triton-bitwise"): + self.errors[name] = "GPU-only path; not available on the host" def _try(self, name: str, factory: Callable[[], Any]) -> Any: try: @@ -229,12 +312,24 @@ def _try(self, name: str, factory: Callable[[], Any]) -> Any: self.errors[name] = f"{type(exc).__name__}: {exc}" return None + @staticmethod + def _make_native(): + from rl_engine.kernels.ops.pytorch.attention.standard_attn import NativeAttentionOp + + return NativeAttentionOp() + @staticmethod def _make_strict(): from rl_engine.kernels.ops.rocm.attention.flash_attn import StrictRocmAiterCKAttentionCore return StrictRocmAiterCKAttentionCore() + @staticmethod + def _make_strict_fa4(): + from rl_engine.kernels.ops.cuda.attention.flash_attn import StrictFlashAttention4Core + + return StrictFlashAttention4Core() + @staticmethod def _make_reference(): from rl_engine.kernels.ops.cuda.attention.deterministic_attn import ( @@ -243,18 +338,26 @@ def _make_reference(): return DeterministicAttentionOp() - @staticmethod - def _make_triton(): + def _make_triton(self): from rl_engine.kernels.ops.triton.attention.deterministic_attn import ( + BITWISE_LIBM_PARITY, TritonDeterministicAttentionOp, ) - return TritonDeterministicAttentionOp() + # The bitwise expf/logf sequence is only ported for HIP, so on CUDA the op + # refuses by default. Measure it anyway, but the report must not call it bitwise. + return TritonDeterministicAttentionOp(require_bitwise_libm=BITWISE_LIBM_PARITY) def runner(self, name: str, q, k, v, *, causal: bool, scale: float, positions): """Return ``() -> (out, lse|None)`` for one path, or None when unavailable.""" if name == "sdpa": return lambda: (_sdpa_forward(q, k, v, causal=causal, scale=scale), None) + if name == "pytorch-native" and self.native is not None: + # NativeAttentionOp is the repo's ground-truth reference; it returns out only. + return lambda: ( + self.native.forward(q, k, v, causal=causal, scale=scale), + None, + ) if name == "strict-aiter" and self.strict is not None: def run_strict(): @@ -270,14 +373,36 @@ def run_strict(): return result.out, result.lse return run_strict - if name == "reference-hip" and self.reference is not None: + if name == "strict-fa4" and self.strict_fa4 is not None: + + def run_fa4(): + result = self.strict_fa4.forward_with_lse( + q, + k, + v, + causal=causal, + scale=scale, + query_position_ids=positions if causal else None, + key_position_ids=positions if causal else None, + ) + return result.out, result.lse + + return run_fa4 + if name == "reference-native" and self.reference is not None: return lambda: self.reference.forward_with_lse(q, k, v, causal=causal, scale=scale) if name == "triton-bitwise" and self.triton is not None: return lambda: self.triton.forward_with_lse(q, k, v, causal=causal, scale=scale) return None -PATH_NAMES = ("sdpa", "strict-aiter", "reference-hip", "triton-bitwise") +PATH_NAMES = ( + "sdpa", + "pytorch-native", + "strict-aiter", + "strict-fa4", + "reference-native", + "triton-bitwise", +) def _single_gpu_benchmarks( @@ -328,8 +453,10 @@ def _single_gpu_benchmarks( None if lse is None else lse.detach().clone(), ) - forward = _summary_ms(_gpu_event_samples(runner, warmup=warmup, samples=samples)) - forward_peak = _peak_memory_mib(runner) + forward = _summary_ms( + _timed_samples(runner, warmup=warmup, samples=samples, device=device) + ) + forward_peak = _peak_memory_mib(runner, device) entry: dict[str, Any] = { "forward": forward, @@ -350,20 +477,23 @@ def _single_gpu_benchmarks( ) if training is not None: entry["train_fwd_bwd"] = _summary_ms( - _gpu_event_samples( - training, warmup=max(1, warmup // 2), samples=training_samples + _timed_samples( + training, + warmup=max(1, warmup // 2), + samples=training_samples, + device=device, ) ) - entry["train_peak_mib"] = _peak_memory_mib(training) + entry["train_peak_mib"] = _peak_memory_mib(training, device) row["paths"][name] = entry del out, lse, repeat_out, repeat_lse - torch.cuda.empty_cache() + _empty_cache(device) # Headline: Triton must be bit-identical to the native reference core. - if "triton-bitwise" in captured and "reference-hip" in captured: + if "triton-bitwise" in captured and "reference-native" in captured: t_out, t_lse = captured["triton-bitwise"] - r_out, r_lse = captured["reference-hip"] + r_out, r_lse = captured["reference-native"] row["triton_vs_reference"] = { "out_mismatched": _mismatch_count(t_out, r_out), "lse_mismatched": _mismatch_count(t_lse, r_lse), @@ -372,10 +502,12 @@ def _single_gpu_benchmarks( } # The production core is a different vendor kernel; report the gap, do # not claim parity with it. - if "strict-aiter" in captured and "reference-hip" in captured: - s_out, s_lse = captured["strict-aiter"] - r_out, r_lse = captured["reference-hip"] + production = "strict-aiter" if "strict-aiter" in captured else "strict-fa4" + if production in captured and "reference-native" in captured: + s_out, s_lse = captured[production] + r_out, r_lse = captured["reference-native"] row["strict_vs_reference"] = { + "production_path": production, "out": _accuracy(s_out, r_out), "lse": _accuracy(s_lse, r_lse), "out_mismatched": _mismatch_count(s_out, r_out), @@ -383,7 +515,7 @@ def _single_gpu_benchmarks( cases.append(row) del q, k, v, oracle_out, oracle_lse, captured - torch.cuda.empty_cache() + _empty_cache(device) return cases @@ -400,9 +532,23 @@ def train_sdpa() -> None: return train_sdpa + if name == "pytorch-native": + if paths.native is None: + return None + + def train_native() -> None: + qr = q.detach().requires_grad_(True) + kr = k.detach().requires_grad_(True) + vr = v.detach().requires_grad_(True) + out = paths.native.forward(qr, kr, vr, causal=causal, scale=scale) + out.sum().backward() + + return train_native + op = { "strict-aiter": paths.strict, - "reference-hip": paths.reference, + "strict-fa4": paths.strict_fa4, + "reference-native": paths.reference, "triton-bitwise": paths.triton, }.get(name) if op is None: @@ -413,7 +559,7 @@ def train_op() -> None: kr = k.detach().requires_grad_(True) vr = v.detach().requires_grad_(True) kwargs: dict[str, Any] = {"causal": causal, "scale": scale} - if name == "strict-aiter" and causal: + if name in ("strict-aiter", "strict-fa4") and causal: kwargs["query_position_ids"] = positions kwargs["key_position_ids"] = positions result = op.forward_with_lse(qr, kr, vr, **kwargs) @@ -452,14 +598,14 @@ def _backward_parity( generator=torch.Generator(device=device).manual_seed(100), ) grads = {} - for name, op in (("reference-hip", paths.reference), ("triton-bitwise", paths.triton)): + for name, op in (("reference-native", paths.reference), ("triton-bitwise", paths.triton)): qr = q.detach().requires_grad_(True) kr = k.detach().requires_grad_(True) vr = v.detach().requires_grad_(True) out, _lse = op.forward_with_lse(qr, kr, vr, causal=True, scale=scale) out.backward(grad_out) grads[name] = (qr.grad.clone(), kr.grad.clone(), vr.grad.clone()) - reference = grads["reference-hip"] + reference = grads["reference-native"] triton_grads = grads["triton-bitwise"] rows.append( { @@ -471,7 +617,7 @@ def _backward_parity( } ) del q, k, v, grad_out, grads - torch.cuda.empty_cache() + _empty_cache(device) return rows @@ -537,10 +683,10 @@ def _batch_composition( ), } del batch_out, batch_lse, single_out, single_lse - torch.cuda.empty_cache() + _empty_cache(device) rows.append(row) del q, k, v - torch.cuda.empty_cache() + _empty_cache(device) return rows @@ -603,20 +749,21 @@ def _tp_head_sensitivity( } ) del shard_out, shard_lse - torch.cuda.empty_cache() + _empty_cache(device) del full_out, full_lse - torch.cuda.empty_cache() + _empty_cache(device) del q, k, v - torch.cuda.empty_cache() + _empty_cache(device) return rows def _tp_schedule_forward(paths, q, k, v, scale, positions, schedule): """Run the strict core either in one launch or one launch per KV group.""" - if paths.strict is None: + core = paths.strict if paths.strict is not None else paths.strict_fa4 + if core is None: return None if schedule == "raw_launch": - result = paths.strict.forward_with_lse( + result = core.forward_with_lse( q, k, v, @@ -631,7 +778,7 @@ def _tp_schedule_forward(paths, q, k, v, scale, positions, schedule): outs, lses = [], [] for kv_index in range(k.size(1)): lo, hi = kv_index * group, (kv_index + 1) * group - result = paths.strict.forward_with_lse( + result = core.forward_with_lse( q[:, lo:hi], k[:, kv_index : kv_index + 1], v[:, kv_index : kv_index + 1], @@ -713,7 +860,7 @@ def _distributed_cp_case( kv_heads: int, head_dim: int, ) -> dict[str, Any]: - """One CP topology running the real AG/RS schedule over the RCCL transport. + """One CP topology running the real AG/RS schedule over the platform's transport. Schedule (same one ``scripts/ws2_p2p_nccl_attention_reference_check.py`` accepts): all-gather Q/K/V and the position ids over the CP group, run the strict core @@ -725,9 +872,26 @@ def _distributed_cp_case( AttentionCPBlockMetadata, AttentionCPCommunicationPlan, AttentionParallelSpec, + CUDAAGRSAttentionCPCommunication, RCCLAGRSAttentionCPCommunication, ) - from rl_engine.kernels.ops.rocm.attention.flash_attn import StrictRocmAiterCKAttentionCore + + # ROCm runs AITER over the RCCL transport; CUDA runs FA4 over the CUDA IPC transport. + is_rocm = torch.version.hip is not None + if is_rocm: + from rl_engine.kernels.ops.rocm.attention.flash_attn import ( + StrictRocmAiterCKAttentionCore as _StrictCore, + ) + + _Transport = RCCLAGRSAttentionCPCommunication + transport_id = "rccl_ag_rs" + else: + from rl_engine.kernels.ops.cuda.attention.flash_attn import ( + StrictFlashAttention4Core as _StrictCore, + ) + + _Transport = CUDAAGRSAttentionCPCommunication + transport_id = "cuda_ag_rs" label, tp_world, cp_world, replicas = topology device = torch.device("cuda", rank) @@ -783,15 +947,15 @@ def _distributed_cp_case( cp_world_size=cp_world, cp_rank=cp_rank, ), - backend="rccl_ag_rs", + backend=transport_id, status="implemented", expected_blocks=tuple(blocks), expected_kv_token_range=(0, seq_len), query_token_ranges=owner_ranges, ) - core = StrictRocmAiterCKAttentionCore() - communication = RCCLAGRSAttentionCPCommunication(process_group=cp_group) + core = _StrictCore() + communication = _Transport(process_group=cp_group) query_start, query_end = owner_ranges[cp_rank] q_local = q[:, :, query_start:query_end, :].contiguous() @@ -820,19 +984,23 @@ def cp_forward(): forward_ms = _summary_ms( _gpu_event_samples(lambda: cp_forward(), warmup=warmup, samples=samples) ) - peak = _peak_memory_mib(lambda: cp_forward()) + peak = _peak_memory_mib(lambda: cp_forward(), device) # CP=1 acceptance: the same core on the same full-sequence inputs, then take # this rank's query range out of it. - single = core.forward_with_lse( - q, - k, - v, - causal=True, - scale=scale, - query_position_ids=positions, - key_position_ids=positions, - ) + def cp1_forward(): + return core.forward_with_lse( + q, + k, + v, + causal=True, + scale=scale, + query_position_ids=positions, + key_position_ids=positions, + ) + + single = cp1_forward() + cp1_ms = _summary_ms(_gpu_event_samples(cp1_forward, warmup=warmup, samples=samples)) expected_out = single.out[:, :, query_start:query_end, :].contiguous() expected_lse = single.lse[:, :, query_start:query_end].contiguous() @@ -868,8 +1036,9 @@ def cp_forward(): "seq_len": seq_len, "local_q_heads": local_q_heads, "local_kv_heads": local_kv_heads, - "transport": "rccl_ag_rs", + "transport": transport_id, "forward": forward_ms, + "cp1_baseline": cp1_ms, "peak_mib_per_rank": peak, "out_bitwise_vs_cp1": bool(flags[0].item() == 1.0), "lse_bitwise_vs_cp1": bool(flags[1].item() == 1.0), @@ -942,8 +1111,33 @@ def _run_distributed_topology( # --------------------------------------------------------------------------- -def _environment() -> dict[str, Any]: - properties = torch.cuda.get_device_properties(0) if torch.cuda.is_available() else None +def _default_platform_label(device: torch.device) -> str: + if device.type != "cuda": + return "cpu" + name = torch.cuda.get_device_name(0).lower() + if "mi300" in name: + return "mi300x" + if "h100" in name: + return "h100" + return name.replace(" ", "-")[:24] + + +def _load_comparisons(specs: list[str]) -> list[tuple[str, dict[str, Any]]]: + """Parse ``--compare-with LABEL=PATH`` into (label, payload) pairs.""" + loaded: list[tuple[str, dict[str, Any]]] = [] + for spec in specs: + if "=" not in spec: + raise SystemExit(f"--compare-with expects LABEL=PATH, got {spec!r}") + label, _, path = spec.partition("=") + loaded.append((label, json.loads(Path(path).read_text()))) + return loaded + + +def _environment(device: torch.device | None = None) -> dict[str, Any]: + on_gpu = device is None or device.type == "cuda" + properties = ( + torch.cuda.get_device_properties(0) if on_gpu and torch.cuda.is_available() else None + ) try: from rl_engine.kernels.ops.base import _C, _EXT_AVAILABLE @@ -957,7 +1151,9 @@ def _environment() -> dict[str, Any]: except Exception: # noqa: BLE001 triton_version = "unavailable" return { - "gpu": properties.name if properties else "n/a", + "cpu_count": os.cpu_count(), + "torch_threads": torch.get_num_threads(), + "gpu": properties.name if properties else "n/a (host execution)", "architecture": getattr(properties, "gcnArchName", "unknown") if properties else "n/a", "gpu_count": torch.cuda.device_count() if torch.cuda.is_available() else 0, "hip": torch.version.hip, @@ -970,6 +1166,13 @@ def _environment() -> dict[str, Any]: } +def _case_for(payload: dict[str, Any], dtype: str, seq_len: int) -> dict[str, Any] | None: + for case in payload.get("single_gpu", {}).get("cases", []): + if case["dtype"] == dtype and case["seq_len"] == seq_len: + return case + return None + + def _fmt(value: Any, spec: str = ".4f") -> str: if value is None: return "n/a" @@ -983,8 +1186,14 @@ def _fmt(value: Any, spec: str = ".4f") -> str: return str(value) -def _write_report(payload: dict[str, Any], output_directory: Path) -> None: - environment = payload["environment"] +def _write_report( + payload: dict[str, Any], + output_directory: Path, + comparisons: list[tuple[str, dict[str, Any]]] | None = None, +) -> None: + platforms: list[tuple[str, dict[str, Any]]] = [ + (payload.get("platform_label", "this run"), payload) + ] + list(comparisons or []) configuration = payload["configuration"] lines: list[str] = [] add = lines.append @@ -996,12 +1205,25 @@ def _write_report(payload: dict[str, Any], output_directory: Path) -> None: add("") add("## Environment") add("") - add("| Item | Value |") - add("|---|---|") - for key in sorted(environment): - value = environment[key] - add(f"| {key} | {value if not isinstance(value, list) else ', '.join(value) or 'none'} |") + keys = sorted({k for _, pl in platforms for k in pl["environment"]}) + add("| Item | " + " | ".join(label for label, _ in platforms) + " |") + add("|---|" + "---|" * len(platforms)) + for key in keys: + cells = [] + for _, pl in platforms: + value = pl["environment"].get(key, "n/a") + if isinstance(value, list): + value = ", ".join(value) or "none" + cells.append(str(value)) + add(f"| {key} | " + " | ".join(cells) + " |") add("") + if len(platforms) > 1: + add( + "A missing row below means the backend cannot exist on that platform, not that it " + "failed: `strict-aiter` is ROCm-only, `reference-native` and `triton-bitwise` need a " + "GPU, and only `sdpa` and `pytorch-native` also run on the host." + ) + add("") add("## Methodology") add("") @@ -1022,12 +1244,12 @@ def _write_report(payload: dict[str, Any], output_directory: Path) -> None: "per (batch row, KV group). See the per-KV-group schedule table for that cost." ) add( - " - `reference-hip`: `_C.deterministic_attention_forward/backward`, the materializing " + " - `reference-native`: `_C.deterministic_attention_forward/backward`, the materializing " "FP32 reference core hipified from the shared `.cu`." ) add( " - `triton-bitwise`: `TritonDeterministicAttentionOp`, whose contract is bit-identity " - "with `reference-hip`." + "with `reference-native`." ) add( "- Timing: CUDA events, median and p95. Peak memory is the per-call increase in " @@ -1087,54 +1309,81 @@ def _write_report(payload: dict[str, Any], output_directory: Path) -> None: add("") # ---- single GPU speed + multi = len(platforms) > 1 + platform_column = "Platform | " if multi else "" + platform_rule = "---|" if multi else "" for dtype in configuration["dtypes"]: - cases = [c for c in payload["single_gpu"]["cases"] if c["dtype"] == dtype] - if not cases: + seq_lens = sorted( + { + c["seq_len"] + for _, pl in platforms + for c in pl.get("single_gpu", {}).get("cases", []) + if c["dtype"] == dtype + } + ) + if not seq_lens: continue - add(f"## Single-GPU Attention ({dtype})") + add(f"## Single-device Attention ({dtype})") add("") add("### Forward") add("") add( - "| S | Path | Median (ms) | p95 (ms) | vs sdpa | Peak MiB | out max-abs vs FP64 | " - "lse max-abs vs FP64 | Repeat |" + f"| S | {platform_column}Path | Median (ms) | p95 (ms) | vs sdpa | Peak MiB | " + "out max-abs vs FP64 | lse max-abs vs FP64 | Repeat |" ) - add("|---:|---|---:|---:|---:|---:|---:|---:|:---:|") - for case in cases: - baseline = case["paths"].get("sdpa", {}).get("forward", {}).get("median_ms") - for name in PATH_NAMES: - entry = case["paths"].get(name) - if not entry: + add(f"|---:|{platform_rule}---|---:|---:|---:|---:|---:|---:|:---:|") + for seq_len in seq_lens: + for label, pl in platforms: + case = _case_for(pl, dtype, seq_len) + if case is None: continue - median = entry["forward"]["median_ms"] - ratio = f"{median / baseline:.2f}x" if baseline else "n/a" - add( - f"| {case['seq_len']} | {name} | {median:.4f} | " - f"{entry['forward']['p95_ms']:.4f} | {ratio} | " - f"{entry['forward_peak_mib']:.1f} | " - f"{entry['out_vs_fp64']['max_abs']:.3e} | " - f"{_fmt(entry.get('lse_vs_fp64', {}).get('max_abs'), '.3e')} | " - f"{_fmt(entry['repeat_bitwise'])} |" - ) + baseline = case["paths"].get("sdpa", {}).get("forward", {}).get("median_ms") + for name in PATH_NAMES: + entry = case["paths"].get(name) + if not entry: + continue + median = entry["forward"]["median_ms"] + ratio = f"{median / baseline:.2f}x" if baseline else "n/a" + prefix = f"{label} | " if multi else "" + add( + f"| {seq_len} | {prefix}{name} | {median:.4f} | " + f"{entry['forward']['p95_ms']:.4f} | {ratio} | " + f"{entry['forward_peak_mib']:.1f} | " + f"{entry['out_vs_fp64']['max_abs']:.3e} | " + f"{_fmt(entry.get('lse_vs_fp64', {}).get('max_abs'), '.3e')} | " + f"{_fmt(entry['repeat_bitwise'])} |" + ) add("") add("### Forward+backward") add("") - add("| S | Path | Median (ms) | p95 (ms) | vs sdpa | Peak MiB |") - add("|---:|---|---:|---:|---:|---:|") - for case in cases: - baseline = case["paths"].get("sdpa", {}).get("train_fwd_bwd", {}).get("median_ms") - for name in PATH_NAMES: - entry = case["paths"].get(name) - if not entry or "train_fwd_bwd" not in entry: + add(f"| S | {platform_column}Path | Median (ms) | p95 (ms) | vs sdpa | Peak MiB |") + add(f"|---:|{platform_rule}---|---:|---:|---:|---:|") + for seq_len in seq_lens: + for label, pl in platforms: + case = _case_for(pl, dtype, seq_len) + if case is None: continue - median = entry["train_fwd_bwd"]["median_ms"] - ratio = f"{median / baseline:.2f}x" if baseline else "n/a" - add( - f"| {case['seq_len']} | {name} | {median:.4f} | " - f"{entry['train_fwd_bwd']['p95_ms']:.4f} | {ratio} | " - f"{entry['train_peak_mib']:.1f} |" - ) + baseline = case["paths"].get("sdpa", {}).get("train_fwd_bwd", {}).get("median_ms") + for name in PATH_NAMES: + entry = case["paths"].get(name) + if not entry or "train_fwd_bwd" not in entry: + continue + median = entry["train_fwd_bwd"]["median_ms"] + ratio = f"{median / baseline:.2f}x" if baseline else "n/a" + prefix = f"{label} | " if multi else "" + add( + f"| {seq_len} | {prefix}{name} | {median:.4f} | " + f"{entry['train_fwd_bwd']['p95_ms']:.4f} | {ratio} | " + f"{entry['train_peak_mib']:.1f} |" + ) add("") + if multi: + add( + "Host peak memory is an RSS high-water delta sampled from `/proc`, not an " + "allocator statistic, so the `cpu` rows approximate and are not directly " + "comparable to the device figures." + ) + add("") # ---- production core vs reference add("## Production core versus the reference core") @@ -1265,7 +1514,7 @@ def _write_report(payload: dict[str, Any], output_directory: Path) -> None: add("## Figures") add("") add( - "`reference-hip` and `triton-bitwise` allocate exactly the same buffers, so their " + "`reference-native` and `triton-bitwise` allocate exactly the same buffers, so their " "memory curves coincide and the later-drawn series hides the earlier one." ) add("") @@ -1275,6 +1524,8 @@ def _write_report(payload: dict[str, Any], output_directory: Path) -> None: add("") add("![Single-device peak memory](single_gpu_memory.png)") add("") + add("![Bitwise exactness matrix](exactness_matrix.png)") + add("") add("![TP-degree invariance](tp_degree_invariance.png)") add("") if distributed: @@ -1296,7 +1547,7 @@ def _write_figures(payload: dict[str, Any], output_directory: Path) -> None: style = { "sdpa": {"marker": "o", "color": "#888888", "linestyle": "--"}, "strict-aiter": {"marker": "s", "color": "#d62728"}, - "reference-hip": {"marker": "^", "color": "#1f77b4"}, + "reference-native": {"marker": "^", "color": "#1f77b4"}, "triton-bitwise": {"marker": "D", "color": "#2ca02c"}, } @@ -1395,24 +1646,115 @@ def draw(axis, key, sub, title, ylabel, log_y): figure.savefig(output_directory / "tp_degree_invariance.png", dpi=180) plt.close(figure) - distributed = payload.get("distributed") or [] + distributed = [r for r in (payload.get("distributed") or []) if "error" not in r] if distributed: - figure, axis = plt.subplots(figsize=(max(8, 1.5 * len(distributed)), 4.8)) + figure, axis = plt.subplots(figsize=(max(9, 1.7 * len(distributed)), 5.0)) labels = [f"{r['topology']}\nS={r['seq_len']}" for r in distributed] - axis.bar( - range(len(distributed)), - [r["forward"]["median_ms"] for r in distributed], - color="#2ca02c", + xs = list(range(len(distributed))) + width = 0.38 + baseline = [r.get("cp1_baseline", {}).get("median_ms", float("nan")) for r in distributed] + measured = [r["forward"]["median_ms"] for r in distributed] + bars_a = axis.bar( + [x - width / 2 for x in xs], baseline, width, label="CP=1 baseline", color="#888888" + ) + bars_b = axis.bar( + [x + width / 2 for x in xs], measured, width, label="CP AG/RS", color="#2ca02c" ) - axis.set_xticks(range(len(distributed))) + for bars in (bars_a, bars_b): + axis.bar_label(bars, fmt="%.2f", fontsize=8, padding=2) + axis.set_xticks(xs) axis.set_xticklabels(labels, fontsize=9) axis.set_ylabel("median ms") - axis.set_title("Strict ROCm Attention + RCCL AG/RS CP transport, BF16") + axis.set_title( + "Strict ROCm Attention: CP=1 baseline vs RCCL AG/RS CP transport, BF16 S=4096" + ) axis.grid(True, axis="y", alpha=0.3) + axis.legend() figure.tight_layout() figure.savefig(output_directory / "distributed_cp_latency.png", dpi=180) plt.close(figure) + # ---- exactness matrix, in the shape PR #325 uses for its topology mismatch heatmap + single_rows, single_labels = [], [] + for case in payload["single_gpu"]["cases"]: + parity = case.get("triton_vs_reference") + if not parity: + continue + grads = next( + (r for r in payload.get("backward_parity", []) if r["seq_len"] == case["seq_len"]), + {}, + ) + use_grads = case["dtype"] == "bf16" and grads + # Gradients are only measured on the BF16 sweep; NaN renders as "not measured" + # rather than a zero that would read as "measured and equal". + missing = float("nan") + single_rows.append( + [ + parity["out_mismatched"], + parity["lse_mismatched"], + grads.get("dq_mismatched", missing) if use_grads else missing, + grads.get("dk_mismatched", missing) if use_grads else missing, + grads.get("dv_mismatched", missing) if use_grads else missing, + ] + ) + single_labels.append(f"{case['dtype']}, S={case['seq_len']}") + + dist_rows = [ + [r["out_mismatched_all_ranks"], r["lse_mismatched_all_ranks"]] for r in distributed + ] + dist_labels = [ + f"{r['topology']} (TP={r['tp_world_size']}, CP={r['cp_world_size']})" for r in distributed + ] + + if single_rows or dist_rows: + panels = [] + if single_rows: + panels.append( + ( + single_rows, + single_labels, + ["out", "lse", "dQ", "dK", "dV"], + "Triton core vs native reference core", + ) + ) + if dist_rows: + panels.append((dist_rows, dist_labels, ["out", "lse"], "CP topology vs CP=1")) + figure, axes = plt.subplots(1, len(panels), figsize=(7.5 * len(panels), 5.6), squeeze=False) + for axis, (rows, row_labels, col_labels, title) in zip(axes.flat, panels): + matrix = [[float(value) for value in row] for row in rows] + colormap = plt.get_cmap("RdYlGn_r").copy() + colormap.set_bad("#d9d9d9") + image = axis.imshow(matrix, aspect="auto", cmap=colormap, vmin=0, vmax=1.0) + axis.grid(False) + for y in range(len(matrix)): + for x in range(len(matrix[y])): + cell = matrix[y][x] + label = "n/m" if cell != cell else str(int(cell)) + axis.text( + x, + y, + label, + ha="center", + va="center", + fontsize=15, + fontweight="bold", + color="#222222", + ) + axis.set_xticks(range(len(col_labels)), col_labels) + axis.set_yticks(range(len(row_labels)), row_labels) + axis.set_xlabel("Compared tensor") + axis.set_title(title) + figure.colorbar(image, ax=axis, fraction=0.03, pad=0.03).set_label( + "Mismatched elements" + ) + figure.suptitle( + "Bitwise exactness — every measured cell must be 0 (n/m = not measured)", + fontsize=13, + ) + figure.tight_layout(rect=(0, 0, 1, 0.95)) + figure.savefig(output_directory / "exactness_matrix.png", dpi=180) + plt.close(figure) + def main() -> None: parser = argparse.ArgumentParser(description=__doc__) @@ -1432,6 +1774,23 @@ def main() -> None: default=DEFAULT_SEQ_LENS, ) parser.add_argument("--dtypes", default="bf16,fp16") + parser.add_argument( + "--device", + default="auto", + help="auto | cuda | cpu. On cpu only the sdpa and pytorch-native paths exist.", + ) + parser.add_argument( + "--platform-label", + default=None, + help="Name for this run in merged reports (default: mi300x / h100 / cpu, auto-detected).", + ) + parser.add_argument( + "--compare-with", + action="append", + default=[], + metavar="LABEL=PATH", + help="Merge another platform's results.json into the report. Repeatable.", + ) parser.add_argument("--skip-distributed", action="store_true") parser.add_argument("--skip-figures", action="store_true") parser.add_argument( @@ -1448,23 +1807,29 @@ def main() -> None: if arguments.report_only: payload = json.loads((arguments.output_dir / "results.json").read_text()) - _write_report(payload, arguments.output_dir) + _write_report(payload, arguments.output_dir, _load_comparisons(arguments.compare_with)) if not arguments.skip_figures: _write_figures(payload, arguments.output_dir) print(json.dumps({"output_dir": str(arguments.output_dir)}, indent=2)) return - if not torch.cuda.is_available(): - raise SystemExit("this benchmark requires a ROCm or CUDA GPU") + if arguments.device == "auto": + device_type = "cuda" if torch.cuda.is_available() else "cpu" + else: + device_type = arguments.device + if device_type == "cuda" and not torch.cuda.is_available(): + raise SystemExit("--device cuda requested but no CUDA/ROCm device is visible") - device = torch.device("cuda", 0) - torch.cuda.set_device(device) + device = torch.device(device_type, 0) if device_type == "cuda" else torch.device("cpu") + if device.type == "cuda": + torch.cuda.set_device(device) dtype_map = {"bf16": torch.bfloat16, "fp16": torch.float16, "fp32": torch.float32} dtypes = tuple(dtype_map[name] for name in arguments.dtypes.split(",")) - paths = _Paths() + paths = _Paths(device) payload: dict[str, Any] = { - "environment": _environment(), + "platform_label": arguments.platform_label or _default_platform_label(device), + "environment": _environment(device), "configuration": { "batch": arguments.batch, "q_heads": arguments.q_heads, @@ -1518,18 +1883,22 @@ def main() -> None: head_dim=arguments.head_dim, device=device, ) - payload["tp_head_sensitivity"] = _tp_head_sensitivity( - paths=paths, - seq_lens=arguments.seq_lens, - tp_degrees=DEFAULT_TP_DEGREES, - q_heads=arguments.q_heads, - kv_heads=arguments.kv_heads, - head_dim=arguments.head_dim, - device=device, + payload["tp_head_sensitivity"] = ( + [] + if device.type != "cuda" + else _tp_head_sensitivity( + paths=paths, + seq_lens=arguments.seq_lens, + tp_degrees=DEFAULT_TP_DEGREES, + q_heads=arguments.q_heads, + kv_heads=arguments.kv_heads, + head_dim=arguments.head_dim, + device=device, + ) ) distributed: list[dict[str, Any]] = [] - if not arguments.skip_distributed: + if not arguments.skip_distributed and device.type == "cuda": available = torch.cuda.device_count() for topology in DISTRIBUTED_TOPOLOGIES: if topology[1] * topology[2] * topology[3] > available: @@ -1554,7 +1923,7 @@ def main() -> None: arguments.output_dir.mkdir(parents=True, exist_ok=True) (arguments.output_dir / "results.json").write_text(json.dumps(payload, indent=2) + "\n") - _write_report(payload, arguments.output_dir) + _write_report(payload, arguments.output_dir, _load_comparisons(arguments.compare_with)) if not arguments.skip_figures: _write_figures(payload, arguments.output_dir) print(json.dumps({"output_dir": str(arguments.output_dir)}, indent=2)) diff --git a/benchmarks/results/ws2_rocm_mi300x/distributed_cp_latency.png b/benchmarks/results/ws2_rocm_mi300x/distributed_cp_latency.png index 29a1358e83726ecaa76c5bed2cac566730ed6a52..e9a33b45e87ddcf2a107e8e2a97cb901522d1fb0 100644 GIT binary patch literal 65704 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 QU+q?OedEnpf9yW;U&LN(l>h($ literal 46377 zcmeFaXH-?$wl#`cmJuxlBZ>(`5J5puGKvZtksL$>4CJWfAfTmU84%o(RC1Dx4p2&AF!AVfde*)ycC~O6Gc2wihjQ>CRlVx?*B(Wny?~ zr;VW2etXY5|2=hT(w06Q{AZG>HctEZ z^Pbj7tKlj2%8WZ~*-^6Yp5ujkdsZo|Q}AH#ou2*V*uSGF<5)qkoyd;0JKXb~w8x!X z%0D-DewxWm=uJ8sry7H6`0LAqb(!O)`ETgtYt}_BeB--Qe$o6l2ag3Up8xZO-^G{C z|M}G~H^_4R_`1Gg@rL<7(=q(QvGC`;%dw0F+wymwZJ8CPR zQBh%`t*e|z5AEl z@oU+?e$%Gs+RhUv@k`ZFO3xXYnYr%24OzK?Bl2RtE4eaeyHAU}_9^UEJdt0(Klk7+26bXQBdOb^~+iPFw< zeky1k?Z5x{Rjbc$mRfgLu~u7`mzNKXj401cj#|&njK{dl&05&nR#ZAOtY4pE)+9*o zuh4x*-Dq#!1}3KcwzjslkqQ#yjh60Nwgd5w137h8+U)G?4N1E9)684q^Y5(f#Wmb} z_|Q8$J3CrADx|=TDMlmRENdWlif88Q(AbzaOR7n|;;KztWePL)k32oY#`{2F;}Vn{&n!$96# zd~J+c@3+tSSr+a0%$i@aA31Ww-rhdStSQT=Ch`$3=6Uu&u8`yuFT=KNvDUpG49EMM zR<2p|0S{d#@vL%$%ock`#~9sGZ-=hnJ8kXlUKeTI)z6I!9&Fj-t--l5v}5m?r;%80 zqsowzxizcWJ~Zbz@?HD>jk7L0GcyWLKWn_%k?+DQx+cpCk@TDd4X5!YTcg@&l`LH! zF3qaSP)X~r9}jI8aZtW-;|5twmNWi>DtJbvgR`vcf`aGvtHswOub)* z7M&TXwkoaCI{4Cm?vw6wY^0SdRw%UOI*raw4Rq3+hqF7rF80#kGyWi>?KJ8oEFyA- zX4aUYFpx7Ia9lzn*;Fmt#`Mh7!|eGk1-y^_{9>Q*>Kiq`w7=)(#%DVBYRTHJS~YDI zJR%v|#tnFkJ@v`Y2a~ZaYzCSSY}({wlI}QKt2SCKCl+-s)unaLCjFbg(vK7{nd@wc z{XR8iJk(Y!=r|VJor~oRSJQS<@fWm~PJj7giI*uY&9E`!YIjdV#?{LY)8%AkKj7!# znU(9S)7_wL>LG;1rXn*6n5 zv75B)j_H5&QDsM7Sr7mbwEg#r(3*u z@yOTY3J!L5%EN~baTt9BZN5fgHHt4K?&IW?A*UHf&U*U$;PHftuOa3NBaN07;n~+6 z+DkSp{q}!2}zdoMpw(-6+YZf4WqK2j+S(1 zCg)cUrS}01#@~Jmdi6@&X{s;%i581S?zK#u->|SS z7E07bb39e$JZC3+2ZtWSGC`NwmuaT72wZB<^u+Ka?u_Rbj@x}7hNtkY;I;2Bo*p*5 zlw~R-^B}-!Or3;JMWeOrw6(RHY+Ib7wQ{ngeYoO^ilj6%t{ls@9r$3LHxu#k<5{fa z={{Pz+Hl9aif5gCr_Lxs;H0?o10E1(W+ZC zz`kP(4zz}%k?-LvpTzkL-issf-Mf3Y5_>zvyhYTsC0Fx^S>r*cGf520%w&geZJ7%{ z?W0-|AcCN?AF;2vvQj}U{(NPw(-gM-Ud@+w3Z>o$l_1Ve0OIjrWZ{#70SbaTp^0WIA)v(#`9> z54L*w`bOT{u)n8La)(N?-aC?B@it4BE;Yo(Sy+5YVxpU-eJfr=C%(uI*~`n*lZB0q zjXXyq>>*cvJZfrSU|@NGNQ}~R+4|rTmyO)q+_eeXE#Hx2CUKDc9mf0P?l3z@DJUpR z_NBL2A?~(@RK<~vHsbJYWTfW77T%g9-BK&l{+w|wq(M2RBW5+HxpTs9-TH;xb5`p^ zp`@4jPGA1Kd&<`W!I4DY0*}Ln);%?p3XyTY53%Z@Yem0*O4C~ANTEAP(&QZ zYVI77GYuGDkpv^MQnuHbaQR`?Sjg8*6vU}d)5~lH3FGI z|D*HUdmB#qDkfe!=`=ghiG@>l1rkWDE_$@by6gR}GXcT^)#ngo~Qw&UiyvSw7~!&R1g_6Lgq=xw{PEQ_B|7OIXFs|HxxeD68iT3ru~Q!F=`3r3PD4N zs+%uLW8p{ejP)nC?4!5~5B(9ZnH^z2++oDtk@Q**<T zENGl&wkq~+M5_K?0ReTgdg~&r`WjRaFXBtQ4?GJtMeO|%cI%ULpYofYOPj*-Ik?sZ zsl=%ERE6LFm40j3pMRbp`P5;0un>45lZH(hGt^$HZx<-7*!v-_Hc9ui+cp73)fg2l zGB3SyQiGZewqAH=fTOG%(-GBtSNitu?kBxk+*HPm8zT^4)e*>zXlZe|-@n{neZ%Sg z#si0Sihg5dVQDjJp}QX9PU&7mSEL^t9K3S%YEMsxPAk2O(&vC1-ldH%Nm9$S(8Xh{ z`Ci}BB35lvSy}n?;Kkp=YCbCgsd?>^p_JCu)h!oU&%iKRvOi%TD=Vu+HZEhU#QX2x zzZ)R%s%uGkDh~Yd6`|bu`{z5`)~YEneyvX~+`KzZzapUEy*A=V+SJ<%J9$~9J*Da_ z+s9j6D&BWgg`XxF2GP@V4jG-7Ua1OOGs>GwGa4IyU(--V#?e;GH#qo6cI8P&ZoKoi zZH_`bQVl)jR`NNSV>2S?d*E0^{2DyYRv|;g{D^!7@{q8Ok6929ibiT&9rDw^4HkKyYc=>?wk?oprzixB6g>QcXPAjd0XCirr_C=9j+34 z_K;fYM@~eOMT-_u!=?uEw6(7do{YG7@66rxtcR_-sP? z&`gVF+Gh}_k+UI1bFPy)r7LnAFV;Mx!+)s)6BDeZ<6kO73H0#$_} z2=)Lts_N*3X*u@sUABwzD_6S6#PhbLMH?_g5%C@XNhLu`1Rt&HG&!PxQ)d>_G3`0F z=51_;*x5{r@ObS!E_Q3*Km24knsOX%v2o9lIEUnvg1xeOlN8XLaqFNbyOK;NLt@nV zObgfhm($DNuG_jb8j&0u&~zm|y$W_d3EBiIH@)B^&>p}j^yad)95}ff_N&&o%nt9s zPqh%?%N0cQO!610#GO+i1qq_W=<8)W_5mA^`r*`r&5tfV)98G)V#SK$!zTo-dprjl zO`RvZpIeEYR zmvJRn*6(O=WaK^q(vcrOFvtl%`W)ZiC@l8O@1bU%?SK|}kjvI=lee?WIDh`U6&61d zS$r$M$yx7B=4LaTk&5B>$o|A(?rzMql%`M=aLGs$-1J*`PGENzCAqrXObMj-;npsH z;r#mdrT5(3eOO4KA;0r*toQ3zzg^3<$zp5ea8S1L=={-rP?dkpnl(L5*|rZGHZsyC zoDid1C$=7>PS;EUpyI z%v{HgKj?a>^sC5D$|i2D!^|!H<2JG^EG(OIEO0{>e9tL44x?sMKYr7>&@eFOiH2AO zPxgJWK0gM-PAdVdFd%d=!kO$dep5v$sXqWC`7AqRT4xxyY>Co#nNgXYofW+L~v-jfG)&k5>!tvy7+-cd{X?lC*h7rJ$EiR4%f`TP9 zn#Ph}R~J`t79rmh?nWk;J6Cr%jPK~lqlCGO}kMbOdB`V%gp zNZ*iKzC-8umuciLJ`vO-g7!l?0b{(zA2`nhiZ=N!d$m_s`26S3pBs|(-OGUe1Ev>$r#nRD!{AJnemnwWR7z=4yk)(d_DTPhCDoedlx9CjrBkG((Y=&T=EX= z_cPDL zd<;UqJ^Og?8J_Kh(|9yR2Y;zVF&j5A+-Z!QgZEE-`SRt^j6fKH&p1zpIMm(VY%DBY z9}_PFultgigT;;l0<7=f&&4IXNAls}e%JIBt5+*uxDZ6LJAT~mK<#m%rUVwIi^`$U zyJcf$X6AqtF>J+2%jMj7I{uD(;&YVR26MAB)G*vGMY^w69%b9iL}wsw-P-#`N%3Pg zw}Gc9sXDtuT=b|^YKn0k7e#&TxNgCv#LanAvCX~d&gPw+`u69tZTb`eL|D3SEMq=s zCbiGNP;83&`0=WC-_DA_I_$ryCWEUnfRK7wqKHK#xRIqs@k462h?dJhgbtr?Z+ft* ziJw4kP!p-vy{ZS!XUHR3mb?I3wjBL@XGg>{Z3$`mKrXG9k(Ub}8Y0@&kM-XL(f}kJ zE!^P}rjhG1=iJwlr>&7@Qppy0joLo`9ysx^VO7{_kvG^Vq?#pb>HsKsM(la4YK)dc zH|ym;%3Ze7re42&8(y^LOgeJJp05<4*G?!bNVr(L)_u2z5Zz7}!fkm}^n8emK^*Am zc=rs&X$)Z2<<;(-Mf#Y{;5TMI~3RPQkv;J!q$QONkq+1A8{NJ%WIpm zJO%4+#KgpW`SQgIINGYWO7@Zi&~0Z@Pwd;bZ;OHbi;IgL5Cm#p+FOQ)g>}_wyWo$* zjO*3~A8EN(-L({@sHWRiKIze(S|wzx+__mt)21vHoYpE*E_PD>aG4oZ+w($dN~<4H zknl*TfBXe3PuVux>u2?4+4M!@5_^&>M0p)#-#y`FtG+BeH$9B>>v_tZIS%xLj<3Oa z>UmAg>Y10Ii3ku*Iv0od5aGIdO9-mrXr!;KE1!Q?jXxg+Y-oTeLWrgJmpS6vOMQCZ z2TK5=yZyRmTO{ys4@$>#Nji6dx#>!(eGCO`fgrl&Rw(ENHwc#_{1uRYwkb2F@328`RX0%Rw5_BFTBV!o3 ziteg#{s5?m% z2nzH}{4%yp);<6<8#ZhR=hZLoLPn^2v_~>YWcJUy4CDw#qHHJJ0zi38^>ex2XXd$; z^~w6FS6fdxApjK^Kly-qh43;`R8sNcSI)2iu;uZ%okVwL8f zd9^5?H{dBDu6L7i51DEMGUAID2UAfi9@+jucr00k@9_Xi~S7cCF zvpWsUajd*NC(qSJDM=E%f&)p4q0loRpa^5xWNDP<3LRF-pUJvD9?gq5%J(pq)-&YI~}myC;@?l^siYxI-+SJ8xQ1 zvFEov2m-IO&TjDQ}VUSXHH{GrWt*Ry~EBjQ$F>TFu!5?G>@=&dxfMpdu z--n1ZO!UmcXHlA0fdmV}GZvNGL@}}a)6UWH`IP@`r`TL2%{(s-@SZ?S?%eN}vieX~ zK5$X&d3z_enL$fSYiMYQr>D#$WH5zPbLq_vr%??pS+=G|!#u~Uzq|!yR1r2!pz~xD zij;7ISMXD1tPxLB3zGO@gZDdgfSS6!(~aw75u|G$ZWkKqO~~UA7H(YrtL5;t#TRU< zr(!O-3UhI`N-ZEijH<%!66&fcWXW&H(puNAaeYu|py&!~)(rvfhaa^?Yfij>rQ(BQoO zI|ci0PyTM#oaPaV1exxnP{q&k#B)&O!tl5JwZ??oa(AzLeREkY=;J1rxf!GSq;RlE z%pi0-OK*4JWERO97(`ZTyuOqedGh{7f#x(Y1qJd?6gwDN^xIK)J>}Jx&AOOmMt?bX zsy|!i&RJvBuajeaFMy9T`7PtZCJ_7kj>U+@rVKj5p9ej4*F6_DXu4J&akW_`6k9bK%xj14m zjJn>FDzoOz?Ny^id)&i%i@iKNf>7y8OV7xvC+SeKo-q)n0s%uEF-aAL$zEB%NQ8J@ z0d7g`M68m}u6>E@+PN5E%`W{dUPow;! z$4*Vrm)_p1FPF2*#uDyf=bN4c{O9S5a)O9JxGO@6AbX$b^jb_eoqUj8gpO|MCoeTE z1w7CPfpUSOaflARQO^Su$rDaBRwHacgspJ%8d~GtbYny}vI;kE-V~i^du);)NPDvL zNZu`1S0faf4v1#fASWqNm(}%c^z9l}9Z%QdEWVo}VqB%AOqn{GH=yS{R?FTuWot<$&6mkDoKOg1Aae2}#V3K(D!RIodo$96=Pg}e>)i1qt#j!Ap zkf@?SMtZ47dU0IFrYBf7;PuWs2fQ%RoP)PM`&wM0IF4;rY#}jsfu}zkC-?6>241Q2smZ#6Jk(IW&I%% zBp|tL6}^@bAr~0`?1;HhneU->-m`}_e9U@3c zd2_Sb2tyIzo?mYc`Bot|SHoK&tYje3w}@`#G7%IU`{?51<0m_wnWxzFokNMRFaPAn zPoFLv&`6WPnX6v1RyYX440ns`FBzjD7tKHc8xY!B(NTbL%>RCUu?f}j8E6$_1IwEg~bPGZ&QJ3_3fX> zdkWf$BT1zo*O=scK(Erx3{0_Y*3P){nSVdO{;K$X#3Yk}u^k65T({|OjB0Ds!D;5M zZn^I{xY+(&y6E%YpYnzroF1u`E0|_Yx3I8KgtRq^718@X(=lop95kllYqDuuwQH#1 z=+}BBP>&xFEH`Z1=C`ZuUAO)xB=4l@Yi06l=i*kbTBT%VWtEjlLl6Q7$X9LuIcaj6Cy#S;_>5>u(_xFr5kmkc zP6_U(eyHx#QSU_bf=h@*gxkMwU(t8J^a)gXtl`beA2q!@h+O#s!G+=S<`-peEZF(3 zm2p0pW0OMO0Cms?Q5I`j{5sg6d5&q*CN_$+jEpD3QSM~Ve(04|XIoxelyHJxrG{z~_~a~CA`pzBAE9^GtPeEZHFV?Ya1#Xa065CZ(Qg~8qIG*x=DjBwS<#%JbINJZjMtG22`;5=nyYt-2(?s>K`}T>GTQ4jpgmh-`MX4&eB)_&hk*=)bO7O8?Z1}ae&+nXNT&_n zpew+}$toHJ*%`|nVNaFJN00(1ABNW&LvHH+u&G8rXk;$f(5y2jO9kx1VQ8Wbn$vxcwXJ?uPfhdi^tZaAoMQk1uy7j-uwcHVL7!j)3T5Eg zy#Bm7XA%|(Pg`l}QFcew6r1q(-+#C2ZxmoeyiY_qSsitOk8bQ2-0bH@N_cpIV4fCGxNmxe%Y|5uc zuZ9@T*aM9}>0i^~2mDlFp=o1#|9MW3Wp+aNlN8(hXY@OKYJamzFXFc~7f6lFvjWK9C(umKHg{ZAK89;`>ALMxCgE_>2{;VE$0f zsstauy?9Qz{9b*_Zm<=Esdt8su&s|fyTZJCEN?CFNQ_~1M7P_K=J&|vDY~T_P3x0R zDjE%V5#}0VG!MN@nd_D0I5}uNc*TRf(FsSiw<*ze8+prpLdQwG|4lhp5vn z%G96L%Y~}!7-5H7-NoWj%IAz!ohA}u_2^<5$JvfC%g?IK&CTgETP|}Gg>&#|a^?;* zYXBnxq)`R&d57mPkt$SR4QJU2Nl6lFsbzPjs64XqeP9P5&h~)ft_gJ)jfPf8hzY`5 znYLKHhwcLsTn?c?{=|t}v$Ip?Gx*@lr{`ypn|WOA0ISO?h1UsqJzLSb>So-sQ>u_# zmhF)l92~6tW8EGr$Wb7ur7f(j`DVShfLb2Z+P4ptDIqdG#B0V7O&EBSXr9=Eg!lmY zYV`GEqQl_ZO{-#ErzabaI5yqSTDRv^S1EU%vb41HBYNek@*M{JJ{M4?GH%`aG~q}S z9o^S~7W=nLAyoWrlYaCKSL2HZi)PeNyO@;_0^;*j-q>r6h{Om(qelR3ZG0C|kru7BQ zRibRk%xly~eK|~so->pRDlDs#E4zhqOt!2E#pY$LyQsHyGZ=EQ2Mc=BVG$ChckAr$ zH!G8{1aLQDJIB2etoFqbx?(qtih-2C8z{ibEH(MD?uwT#g`oBvIy|id`^xB>Roqgj zQ=XxO3h0TJ4?doCy^vo(%iAkHbvZo^o?e3{Ly``@3b=g4=&;N=xZA zO<2M+{wIUn=6nt(uzLlKDvCV=?`&iX3kr;o{Otw3O|?;`gNxS;7*lyqTrdd#lq#%; ziGwrEHB~*&IVaJdr3q}rsk1ry70j1K|9IJu>y!<#(Q3$Hm&K=7H%`5RCfK4YbXZ;X zH5LpYxp!I#1Y|N0CZ|O~&2t<_iGn2iSa_Q2g5d@cwMT5zg6VDRVO6W&i zf&buE;DTMHrr<&X`b147L_xf;X>zZpy`2CK7or8fM0H{IOX|reHpOEv=)2XG?}VD5 zP#*lqDV!e1ZIs`&m~P1RujQZG@h!#H>U1|-j*R1?n4JPgk46GDQCVDE=1kZ`-#rj9 z@>U!;kxnm>3_4uKF-Um&TYiCm(EUx^A5b;PjAuiQXyxh$!NyyiI2~yBWku`YtrZM1 zh4$$y=;^)k@?0J^G~Dcy9a+yR-9>6pta#GQ5r+{2O4KHCXRNi=l!c9rw`cPBtv(T* z4{1o~)G*LK*?A8$?Valr>3v2(V0D4fP)|#4s_%nor~jd?4l#leVTy`q&I)#mqiGUn zd$T3?HR$pJ{W;zq9v5V~%&Jj;5$%AQVo({fZLN|gNOrUDac?dN4a=9pz01k}E`q1f}{f}w=GV0xsS~A(FE1Mk$L|XM} zw|e=B{i);HzOv7+z#^NLE&YagusMg3$8ut*-AarWqet0##LO?O=KgvjNxx`rYHG5} zdHnb>pUu~gCN7ew1wZUhc)6Q>^!LUehK8mhgx^yG4;a{FyAlhTtD$%%H70;#MCd?> z%iAsFcM=2$G8QNM=p{MXy|Tz^*(#x@=qV*vu70$Uw1Ifl{X#k|O>ZC&yj&>?SLzSv zRdsN46;6(0+OCsYZ52D@ zzog8yXTHaA1az)j=SS3m+eXC=z(ZcmZ4&%iC$Ux}Y#FnixMGjycXyGovuTgtosQa6 znVjY82)PeM2k>g;^6$gZ<}JAenMv4ct2?_v=Y@lK(Wgm>ON-2zSOy`UvZ@U~|M&nX z5oNEZC*Bz>|1;BsC-6I=B$gr?;5UDb%~fMX#T5gi5K1XjBvtS3Ugpi4BP&FwI9ne2 zcp;JM3&nmU;?d_U6j0tkldoRMlrRBXD&-ML<9x3aY7~-M1i;>L(CSKqg9145(|orybH_}kAe8%;R;5A@fUMx_VrbjrdNVh=%xv`UkJ^1hd z_aFlu1NBFobHs&0EUxFLh{^_^U(fsCn^zAtLS)L=cP{w& zQo?#WbUe`qFo^~UN({t+%{i#YJyF$_`fxKP5CVYMSj=`0O;ewB`uJz}XPt&tSpz+a zEY9dai}vM$2MS33L`%RrG36)$5R5b!C$vvlP8@-4)sNY}Z5?s!kow?5f_4J4h{NXI z9r#O9+7O%2n>TL=<4m9C}#-7L$scAX(mxfWJ=(~ zKta?XL_diy40(I0(l{Zs6NFYt?Mwa4)C;c{=S&Zk{2kV}!rt}U*;NV*@0T7@&l4)Kf#5D@ubw>zyK zr`p$)t=4jF*lpcn*{6q#yEfQo6JC1;M*5X&*Vchr0Ik4HmRzx?N&5}V%mKmU zmJq~n@}I*Av-wCC%rvL4|JXgYB85CMGpI`01^nBj8Y#c4`hq?z4dB@PWdnqxjx^gz z5D5t=$6& zySE3bO)Z49C&56QmqF$75!W|yW#WUNtQE+Nt3fS><+|9)jvk(MLcAl-le0#=IXhgY z50%~z!t*C=2P$~qDK_=xCY#Rt`}y&V_?0}wy@6RU{+d5FW!{vPijxvT4EvCTi?8Ur zGz17jF!%uO5rSwiYFtBNWXvM6#Ix!MoU%#Sfk$y}*&$*R?1lZy8}|;;d%50_hFL)1 z6XKMEBNSWs37&wDp#kwI;v!>;*4$BKB(`8U#1v2w5KXDSDf@Z$=2;kMNo_lcl9x0j z;85<#f3IrDXIS-T*9O}kw*6CXTb~NsClMDXc8HD;n+ifFF#+mk&VuAEQ?TQAp8mdV zDMuQ!4o(uEgLK<1+$!1q#8Rl8C)%{VuD`#Z5WiJUzv|g=g|#DmV6#Rr@heQuO(^j& zT{^@~v}j01q?>~1R`7zG-4aNfpkI$p2*g%6KiFwmY%;z(wY6Jl@!@DDm_#i={0c@W zT<+bwR|RsNKs*_m4|)$!Y)2w_TGI;4?h`vQXxl0feq^tQYaFreQARA}uAAC+=+Z4> zhzHh{5$h$95lO=sc8R51Bc-Bps&S}&E^Zck2Iy)4qY1Kwg7=g!iE%Q%#Pp~uXjMlH zUo+!*bMLHZFh6$z-`C<)SwUK|YH>6)wu1@6?K9VK2r}#>;sw~;Sir}yUUenxF&G1C zzYFvojF`A8Q3k(53g{{mnsSWl8N?p@8}EdMQ2FoMxpQZ=H6&@7BY~y$4SG{&Op*ALMV9{(o;~2kZuOJ4t$8Nh1$H&k@z7J3 zkO28G5>~*I(R#30qXa3;s2(h43q0I$SSkj$OTrUj&*1%M-?vW+;NbDwOA0`ngmuAw zJ4xR8(5)w1Bqb$9hRb-j3fsLvJ+KMj>D4ZhgF0KEAjJ zLMDzlm`IQ%2-01&0heaOnZfEM?-dBos&44U23P?V~snIOr#d zdyL2*c;KXe2u15w9A~Ud1dJXX)yYMTh#VXyNBVI-_@2pUUF-wIU^BK zCx-k^!^Mo-@4^21fcR=X_~k8UXHw$3Bb{w+#Cz9KI%uvdRQQ92gF78I)kChEIZI4j z5@KQIgXf_Z#li*#hEUR=_S5ExPcR3wiG$g&nStws~NL*nA zT;5g@W4Lz|b<91B?tjj|V?5BDgYy{(Go{gh1qkl-zMjnFS1;J6ea;=qwq1F>@ig!a z3MaDB;o+)kZb$oy&cU`HZH%w_pB^!fhX*MdyLOCg>Ao>yx`2c<+TSG9;>xsy?o-S4 zzxo$1*doCv+OD9ReU79>OsqMjbaX*Q7mK*b2mbi_M;qfpQ`FBN-s2&D;GbXr=ei~R zzG~8~Y$0;SaK{3>~$#;7egd2_zqBT^=YoT_XxRFYW;vv(DMKFJNTiG3KiyU z{?w(9tCq}P*Z;ZY`k&og`g6Vh_xf+ij=Es(w()S1?TWV*R~rP581pqDx~5}O1N2sI4# ze{p_(=qvXX^RKr+dy1FfVIL8^l=A4&BWjo=v;Ci3eDgo2v;2j_hV(L16v1JV;!8+v z;e02(@zAze55PzO4;97`^M+=$JfQ9+RW8`bV5I9PRJz1!k3Xuss$E#Fm+K-w7Nh3W zO+D9+`~wB^XqykPnOEKetigN%(l}`^Ov(zWU<$A z8wi`EeUDm$Rv9Fb4nr5ze}|ZXI64ir7hZ}-c-sW44@rp%t^kf}6OwiR`VwFBTO?x= zv?ysR3X*V`c-!n<;OyXMh9g0wVrt=GZ&??~7&0?8-ixZrQ%7`54vTu+2a`I<6DJ5_ z^=46tQap*RYtOxY{;hSygNDSgr(hU?O#LuEu~ubI7!j7(M^>^oe9c5!qhi&oE^ z|3N5R<_Qmc)z1vi5XBmHZekb(dSxnYZEa1pAJ(JacJ#gPk#;!89^G7$Cp-T+D}R5S zvuYm)0kaT)SC@98<3+%-Bg}Z5^eQ{dAu$o>Jxdfo7DxsDroFfpw9~{m!{w#vGI==bY2-p2~$+NQn1E~NChxyTuvxb)^55a{@8pTOpDS#W} z_U#8q3w$gpCtx>-eZ#t38|N?5gKx3dTs?5LscC0tCmY(?t8ECSftOjP4&ZPQ*HgO6 z!kC64F`2CSPpA9OG=}+Qr27x1SXhex;UhPG-sk@wD*EpN$2Y~zOBt7a`M~Ww1 zYv_I;Tr-u}j3y=~n#OWbbH&5Z>zhUczuuqG`jE438JgqKj)`A;4{?Twe`I{aR_Hs( z!Ireqg|m!kOeJCuNQ{kLhma(9$;nl4)(Hs;>SdZC{*$g0(yL72(99y8C(!^9q%X*R z%|h@OA6u?GTUcGKgm^oIbOw3wFgPRlqT6Y+sJEj*C3QGtqvA9KD@58YQN$A;Uo@F6 zfq@yA?w~|`jl6ng@Og_mB~vIbK})tQZW5tp7iU%b*y+!qgDjuali!VWeDTdy?b42-OV^^J%1`>(GeZNM{| zwxV^dSFw22amLWD2JK*|ePZSUMe8!pWD11XriB01*u;VaTx~c8ise!A?KE(s#;F1d{0r**5RK`%tO7DL{Uw z(;Gi9{iz3mg^Ztp#K?6%1KwHS(Zd#PQoy{@glO_++q<^}&=&iP-yu>M($UbjZ$3k~YN{0PPi_11Wj6$blai85IcKo358He_i|&-Y)lQ8N>4-#$&NisijcXuc5LZ_**ZcuL==mH=z0JtH&%^a)e@1~9W-;hk6Or6&OjUSES zr`2829YW@yOe8H3EFMB-!SlHPKTzgh*rY2-w)2n6qh;#>1D%2O_X1n7RlXnP|Kgi- zdY^aKe|O#ufO*1fAHh{Ii~vuemq*AeLZ9+ zdWq4aO{fr1lZ>!*K+2P*7P#-=78A>(HQT~rN=($i3q45o^7xh|3eHv1y+w3-Vlyc1 zn@2~?`XoSO0*YDoLal}}Xaj{bxk92IcFH5u5zy;|-aQn=(B1m|46n@Y^s=+9S z0RMEsCq$Zy$&4f2%!G(D&x}65xosc4kjWaE{|yP14m*p8h>U{LCT5*(Z=s_{yUK+I zNJAmf#K}Y-B2Yb+6OAHM0wyx%7kj76A@-b=W*M=B{8_G4m4sdhv;?!T^Yg2sEIr^M zIR8rV=JyhB%d5tuD6+C?m;Yk#c5?rW4nF@99q6v_z9Ig$+(g2SOfv)MEKh2JJAd@O z#9H>FN6$hcAoD4RHU;XWw6O5`i<0>u5OnrZ5w{Q-T87JEr9dq5$hs>9f+mQM=KJTF0sMQ;DFAm4WQ8o5PZ8yyDX*Yzk`Z0qjzE0 z1V)rid^4eARQZb^p}WT`x`Yrrbc|}(W)}g2=tiU0=C}y6i1OE+cs*&D1G*x3%L(pl z7@td0OcS(nMj-yj0ETiXgxq2&{s%zn`5p_rsHUu}94xZ%aPKhy0w$s6CoW?$0L{5WAkhHKf2vzetxnNA1>2`>Kk93drnA1Fl8rBV*f8|aQ z*J+LmX#wm2xJNo1e^UpSDb``QPwZhof}j)G1PZa8ScB5~iX5j8_-<;gm1+*~MHm@jr)9L+HIM@OO#A zhYl%VOac*0D<#@ts02zW>=RumUOedGbq)l+;=pK({2>rE2Iv~>ZpgtL8q7+=UQI_Zi1xia5#pahx>LlnbY>Aw> zKFowcSGG@g7HLq1`6OLa2^|eUrjfMfze|p_9sgYdesB0PI!1y2iqpSN6JPuP%VC^9 zpTK{~4BTp3;)V7r0~C;?hr!jF*ksV{7YZRX3f6Zr9szzJvpXE~0Zsg>-nh6feAJ|~ zkjzoCDt%0)9)<~+c&IQfjm+>Q+CP~Mh0tgdy^w?$C2iI!6O#s|2d>tvc8J8)2*X6V zBit<+Q$pm>0wdx>Kk>Ljv_1kGGJJP-%X?1QGBw_s(zceFa~cBRw$|24NoUcD&v= zdZ(!vtMxQ^2JeK~rQOG47E0n*{Wqk+s{g3M9<#tCXo%JWB)K0oS{iI)=nPT=yRj-~ z0Sd@pt9MeIys>1N5h^zbx&g!v0b?Gf7oH@u3s9(5r8dF77WRPQ<2*k=$Hs5B|KgQoVQT~Z#W%THECp2DwXKrj-Kd(`RnT*JAUyI=~9b9fiGy$_6Phl zXy|WUJsgHe6bVH;3q!t$E-=OT_iu&oUKc5)h~S1cv|7+KLPEW;8F2l1Z~lGh6ziS- z7L%`skiM;-U$0I)+B}3A5k0^e??L@q_0+Iq!X^jm?b0zgvDjq&_Wil?AA1l^uT!LB z9$^s52&C8f_Y(gV+f1*_Hy9*J5yYjnA|oD%)z9Gq!MByo==u5iCB?D$MhV1m3?m)E z<~4?!?r&w8Zs(G0Iq4mkA4^OvLq&TapU?l|P9=NUPRsy zZet82vL5@Y{&V0h`LPFk_A(AR;lW!N2r&vXH~XPOl|-!n`K5DVoYD<{M>Og1hRabY z&v}M43X?$%Kwdme3pW^a@S_Nx4pd7+M%|7EcksCdKg z-#!;?J8t7m>G3%6gyO%|%$FAC&fg2EvlAwCRKg!rHdtF=5#TKJ79lXPSFNp`6$pTB~8LRfS(aKo>3$axkc zOk~p0TuYYenD@ftyzs&?Xg5VQCRLoaaC+KqBQ!o{lWWNSW&XXco8TBaEPVHm{rmsz z;Ac!vzQC}3dmI?SUL;ZYo0Y-mc$<>3TQX0m=7kUM-&cY&iujf>wu$Jra2Zm6HlA`< z6lOS#MiUDu3|(hXSy1oazu*6ISa!6JrbZfk(F2@DgAUj-iHTmo%UmE#qre;zwG|@< ziFHr7ZUG)9sj@8gf@|`}NLx%PbkhX^B_VYG?e3V`rvo=i5bR0k!8KY-9C4rE+x=iy zen2Y(G2;Iv6Si;;N1z!4jtW%wxlHvPA>+tEJ3VH}%+AJ`7&7so3a+;r^jw{J!t?g9 z=fXx#FGS1+X~uW)%`b4g5`!(&Ju*73txY*29L(bXLMXJ@ z%i`ojCr%K%7CPvZu@Y7^7j(m6UX}_{#VArENlR4HBv1TzS$Mp}a0(x$;j8P5Fqyi{ zXF(%-HJ3aUHiBLiBposksc(IR6g4#T@{K-6@Qc(iIG&g=cM+Vp7X&|G@>GBUt6n@< zQnXU4&=P0_TDsnqyn8&!5h%frGZQ0*0Bw|N0`0Ph~Nu|mF3b!4E z5!@1TC!+ylSc=iL0qX+BrkqKmo#pg)IT1wVX9Soa1#3hf#$fR@6^wo4KjfrpLVI>w ze*Tet8bdaTI+q#^7VRJbIm~xlUG=s^epEbcYddZg{DE`-Pl za_nHN?bAGd1EbOd29Lso01v~v6jEKYQpgajG#V`WM`?Z4X$#>3zxKFl?ir!yyKW>S zgUQfft1^P^kQzpSjYeIpI&Y|&v?}e+ty^6X9iEu|{fL*{kX}R>0Sqc&A#*e_8|%0$ zKTHLRCML0PL=e}ZRc~WsBQX|G$gE%BH?NCRqzzoLXPTWb+5^w$@15>M(4{}n9D;-V zzy)S8v`t=<%NE|dcW;vo%3zq9a)!Wb8YwEQEgj>2esno0(* zB6sS4oPDAnK*rFJUJKkg`cbsdn&3@_+5qQK!$7Dg!VEhx_IDzg(;pX5t|HwtYqlK? zdjDSjZ9xH|g6nOnoDLFle_U0R+eE$54AR=+$y~L68#XV!SKf4ksmPyG0hcYgLMW zDS=FfC>;`#XO}|zOAYS$hldjr1^{x&9J5+7lLc7>Fx%&*#0US|#JoBK`Txhb5VU(B zlfYOb0X>ckza)0t!TR~fAwP|+NT$}4!-aJq-G1n$A`^+)zkCUSC#M_3vIrnWwo``X z1LNHj#d|*f(<=Zo(R*}xMAT_H_N6^k!g;_b--)!eg>-L!85Il$Ghzy8E!?@G7$;Z? z_yYz2M0a6}UlM({Ok4c<7jl9QB5C-TdnrEeRk+7bIrgT9uaga3_HL zSh{T4bBzF84`SZQlP5Rm-JO4VkZ-bM5Sma(U!>9x)dm%~6qSr)v@&&>>}Dgz$kNgn zdE*7}^Y&R2TevwoPl*p5Ge}_|)u57zV5DygsuG!BiK!`OMBqW5@_|1b>9gDI#&RL$k;?Oj$BCj?}B7MqN6&{-d0R;!TG!}u<$6v59tGJFW`rJ zg8%wYb=($gtHXI?p^(lcXoeULQ;U{_YcUHceW%dvt&yuS-VK!r9z-RMLG^)Bdem4z ztc)iXmYHq~<7C(fxK)IfE#B=mJLYeR?smBR4|LgKux0-5#1-Czjm&@ZPsaIQ|B;Y= zH3G`uPQ1*gFi1@|_Tw!5098M;3^fex zUuadOki(1lVX$$mnp`|Di zM!*PD6jK;RxfVzu_BW1&tRl6p!DD$IU!TYk^34#F%;#mFL_d!K4OuRlS5Y&Y0)B` z3#7FWwz1Px5;2HN9EF`xV@9I?{)J%qR^icdWfOF6$r#LYBy{(-p)y{`$?`QDqL}gT zB(MuQPiK%}qr_g0CYEN_g(YJ27uSVe@MD{RjJd4dAkGRzFX6gc0qdEI8Gi`%uh*!| zf5P=Iky5KC&_TgMU^e225|AUJEl&zMNyBBybiethwdUqPu^LdKow6t4%{Lz+#= z&=(jJd<@V~Sc!5DBq!+3SJ^bqy)e)p?J?PCo`uP`% zE3R9v3@Zv27gB^F4O&2HQ^ zg@1-b6|Gx_G@LsG~Hh0WwZqbzJMZt3I-c&QfKP_PdeZAtO}+ zb`j?)KpvS&P4d>i2X0RYCm7k2N{khx3j^9$K~~D^yR}B--&5gpnzRBT_fHOK?F1_Y zY)~}<_CA>Si0ct`VN)K?rXj`{kug7@Pz(kv=GRF4(B*Mv4t}UxWGdJw%nSSBASIL) z=HGBAM+Sj8ug?~qzccv<8=i=>9)JfFdR2Y)Le!w6oYeVOnkiOlR2~Y^2b>n`)&hD2 zJ6&2D0@!Wk1)6YQ|Gg7I7MSVjG*rwsiE3)Yjva95UP|nO-T7Z154Hi?$}wkVCyovH zst$u^erXG#rD!d702z*@iGNuL6lwK+?p!i5kO+SeG*WC0=dYcPdJmRQAc0bR1~f1Q zaH0R8u-1%m?L@y1QqV2-`Cs}$n`yD^rf3jL5#7}4hFvv9EOAWh9uCUgVe0jDN>1 z5p2yL{#t}V0)&b66mTrr)I9O*-{vv#;6#Msp*9M(tPI>?)G)9@q%DXnfTiyIz0gGg z4ak6ch1VupA=H%^wo$Q`ynfxUl)dnx&3Mr^bVDJ}k{OzqN`~#S@Kc@_j%prgl+Vn{ zT90Aae|=rwU5YqBv^5x@F@Eabxj9rO?xYpbx>xnzxjE28N_Y;KsA%4e!MguoxDfE> zz)p~E9*{@3eqGBPODJ+IFHo0q8V!Tub3HizMoa!!dtm;eONmlcj0hYXXh{Ga>1!s5 zv8g}5R^G>JFiq^^KNuzvd3B6{&pAE+D!L+XJg0@?XUzC5hyGy8sO zt5&Hx?Nq2jWyS`zfKZV|1i__R!=?fv$mRlqETV~^K_F_!I;~p`0@Meb4l?6XA+7lvH;NYKQmG2H)o_1F5Pd8Y)Qze!nckkK7M!8RTS=Q{sBQk96-2 zql!KWxVZa~=s5Z&QGGS<4LXK)w1oU}(pHYE4<0cCFNA9KMNCxxuSxVpMve}p+RVf? z2RrZM4pL)gqN)(pe-huqU*7HS+}R_Bwp5QRR}sebKkMnr;bfSMHovRSsc76dt<>KG+~o2mKn(WBJFz8g-#8f3K+r(f^)2U~CSUBXz6 zoyGzF{uO-ZNB+CBay|!mBBaA$;^7n`j-PjTgg!`V1n`zE-U=g`l5jXl=vNJXbC&7@ zLCP^F8npSPi`P%s`dnhCAD(}jtuj%U@ZSXrHi*`<#jk$|o;CYW6J|D{)Ke{T!#dNT zw|nVp<9Z3ba^*^V-QKI9kG2*BZt-2)_7CEKZU4qY$?)h(xPLq#+TEWKg=C z8Nv`!zdy;EK0B8*dUkyq-s}0Fc1V9+E@g&Si@oTUu^_ zBcSf~yz+%tBHWgj1c4}g63LA)d~sH9V_Z~216`%N)J@qZLEIMa>t2gM8@2D)%MXYYKj^X`e>k!2p}+P5{# zUv|gz?1l^PHd)4 zUGcpSmFm8^^{JlQXR|kbFh?+F*Gr+QvBh7@B9DAL34%( zvS;dE3)cvoRI!SVZ?tf90S0b3scUGkL6}^{awn&zmVu@UWuR+V`Q|OonRO#cDJiAk zNuwBs=t5Epr2X>}e}8`tenA57#X=@MDllgru>U1ryJpFw8G)vwPiNJ_SCSB`EdP?% z-QXUqMS%n*&FGW19@y)(#rd!PL4IViFjlXcn!uHf&zQbSu?yw%m9W-?>b?kYEotpa zi{XeOIiMpX2zwsjkdjkU%)rvz>`sgpN#bNdr!iY?0Y`Bnd%S*dQkaw`TzSOoTUWWP z{p6H0bC&JhhDv_>ram+@uSC7i<0L6{54{@0GP`nr;vra8tudJRY5Fhs|DQi#LH=BX z|NSD|a(ehTPpg!a6df1>j0c-`Mt-$@b!llSUn+|3!cavEFr{=Z*ZHy0k}3!Jq{7Gw z{Ih1Cn-z*J<7m&}jt=1*aPvCou$zWcyXgR zkegaRV3H8H11=G~(y}sYsXutDRk>D|!u1ZTbgz%vH zFqjRXDuYlqFvle5AR1=G!b7$KaM3A+GZAEqKn9Wx+Q!OIEi5dIhWeyuKl=giI*)=6 zMH)0vF+~Hvz)HienAU^sr_3|YTnKpdWmYT8{7NzTwsO!V%*N3%T07{eLe7(;3dmt2 zf{^ayh@!k%Xm2fohFlfyxF-@Ow#D_W@`HU?P4ZC~0}O;KhC);@*nST)kv=k*SjZYg zDyN{=MNHn+xRB+zJ)F|nx6vh%-U3E5r$fD&p!Cdn@zCgV;KHiS@)I&j5FNuFJ$lr+ zd?IkzJAY`)YeoBbCJ)$Sg;DRV+SR?Vd~L&%5v&w;Ub=jFK9rIjypDhV&1yaP97VCD z*N9 zW^hJi8_*Sq-LTdB+OgpY{AdY2{JT>0jJc4To69J%bM(z@3XwdbF3RYR<%S34TNi>( zsL|ifT)qgE17B)rluc3g`4OF`^@>&47VwhfG%aEBv4uQnPKH~YUW5G?aU-o={b0o! zh9;nURD&1ySSj2M_OigcGYp)33Z*R~)!L0HfMAloNJzfEzJ3lm5IIqEve75AaJ4(gqJa#MXt(h}vnoszAMA?u4QE#xE(9*-V(G#OH{|9n zhoi^|GTlwxiTx8#yu`zOJ<&fLU_r*#c;{s{1<_WzObw*&vtpiCF!OJHo6_} z%X#TXi}LLZ{UmOF{4B2g^;3}n-Wc~^yS8Y4m))C)9WIA#DkmXDo_|B*Xe&C);k_aR_iB9Xo)*B-tCNY4Rh3X1CB6 zd6G&dk6P{|ChofAO9!3FBiafrC4FDLJG9^ZEs0?WylCU%5^J4&4!)sX=(Nt{VSgLL z$3lmn#{h5~T79Yb;;L`KU7jI>B%Fix7kh#Es^AWa!&Wj?4SKa|Y(!5_Z;m0$?X;oj z{P178Zq-_qE$>OYS>0M`GV&tr#{IccOv;G7S-CuQoPz^nfuPnOI;^opG%#_#qFl;m zuQ(#Qh?YXq3crw0xSX{R!wk{Tsut0*uD;$KJ=2&x^d1wXq^5h-1(+vTuRE>cpju6b z0{F^u^XAQC^$XnH-3`$jie1B8xR|93@w9TSlJmK^(T;^21j7ENooC!H_&1c`m&6%| z4KW2v4yIO2Xww%Duk4iV?`c>(%lOqw%+}%I85@&V5}2+QucflTdR4cukc&Q6kCS}E zBO4&=_>I`hva|UbhCqvQJKDc(Z$iGplaXHwmCo&08xQ3kEaaAU6e_S%NSe zhpj`Apz5^I%kyw%Ywxe0;HIi?eV{IZAuE_5R?NQpb7st2y7$SQk5rcJJ?o3CsT@s8 zqTKc%4=e@M@R-r^?vg29`i{l@6IlH3fyJLl_dfyd{|C&1f=uuRo{2E-XuE3UvVA*v z@o;D~?<@XXl9>^_7;SVMUc5!IKqruhc54;v1C=0|T1d$b&|NKrCZ~NxvzzgWtha25fG=Ga z-F!nZg=ZiJ*KW@1`^w6BkAZ8>nI!-oE04;Us2izbk%>tb#y+g178DC z#`;|UX^OOMs35D!7l_9S5ydg;iRZ*+dB6X+i0j55BQBp;0Y>|u|ARqkzmzq=1o{ps=o%Bge*VKC;YRaoH5p6>R^5Iz@W{D)1AOut!aZtO0h&sK{+x?UWedK}8WFCY(X4!&obdlxao;uNMz-<}b z8IDeAH~Sy=g&4qBX@NRT$ygtkdv;8}#pfn%muBEyO6zYyA_U~497Dc4GV%wvhK#fb z6S0Bke6{V4PSpfpv2XHHO-lO^1M^DIoNkN;tnLQ!m7Xh3DRTLo{oo(RHQ%_r17RJg9*T4R!VK z-H!4dbLa9kY%qADpu%u3;GNu>VO1KZ8;Y{R`OCw`?r6K_h8 z0e!PV8CAxT^7$h;lxWjCpUH=3SwB!UGc!|)(YbM^GHKPo_EOAX%zhIZTZNBeEz1Rq zXvw#fzt*-@slgX{3_8m^H`o9aKKN8Z@M$2cHmWh}lE^s{e>hWTx-{X^cC+7wZ0jl} z;GXAaN6dKGp2nS>orD%at&H7$+#?p#3v?V+&|1%Xfs{qzm?EB-8JcPnA_LL0{fK~5 zWg!J!{3=d0xw&f{Wo6`7~wqAvNCB}Z^u>B zvE3oVwQ*Vk8CN#M)&6TXt)+?~-{c8IQcD8D$gu0`>f{+A3&~_xe5jRI-M?|@XdE@t z#Tvs}y`~@3mk0D}+Cenbo)RkX+8eNUl`|`VPKO(THKib&!kf4_L<*xhd3nZoYI|Hz zP>_cxOIZpqSU4(j+{zYbm22I8!Mk#LxjOpndDi90zw_byA19SxMP068`^lxO`*S>W z50;~I2m!6Nf?o8k=0I!+4>Wm)x55LB<-b;~Yy7V86G(AFy7==4tjwQ_@IPCGmGZnP zJ|#sR7eWQ0Axub(PfOE;mdlw5QWyi%OmUC6h<&-l9T^|oi;)(|d8iJf(@7_cf{yE$ zcuOt(OKLDJh6F*lj6tJAH`rDkysu@}fO1#yuiIq!_ajlu-jrD!lc z3?`@s8)xSWOLpFOqvq2umh_i74Ib@8^ z;*|8=Y`PXwL0v@ghIvp3IcRIds?ck6yO!BdiiS$DxKge@Y>CAfdn?1vbM!%oePMyN z>O!yZQgj|Trz(3}OqV8&>W&+G;c-I66_}zgxbIM;A$u1Lxfcl3$v^hNr^kZ2jD$=c zwGqIgzS0SvsHeE|Y#PWaRM~MP6$YVlmIG5$&oV#=nd1&(!6vyd@=yX18OoWxcu0om zRcnUk11vbycV5{dSP)!5~!+5x1bDn;u%r8=O1S<%Z ziqb?wtYRpmaIDvFDUeITvI*zt#ec0-D}hBnj?oP8a`VL9?Pzq3&;*2X;f~!NTS|6B>h*2glgFDuPTf<**E*BwuVdThY#M8US z4ZJ3}wUu{&AL~5;XBo!e02L_b^}_1A5QIY8Ct3OU)JTs596AITKVKy0a@s+MgfGr& zlWas(B^;o_PcjNHyV#d-&==m3+ zkna8#^XXrYvs-Wk8YZpZ_x6&#FF5W$}S4@6eo|dMUuH_JxJOQ>$ z#h*ke=}oJsD%~0p7se@+jHeOG$xB@Y*qh`UBWDGxeXFAMG$0)M)d{-twbV~@+Lf1Q z5Mzexx{`nse!jVCr+iT^8WOn2V~uF(%h@ura)Jn3vG3VEpZtN#p*<*1*J4@h6y#^G zbZ-;jH49fR((xOGAprBi(~8=Lz1y%@sld}S#1mX)dFCV2b0N52EgYI#&uj+>2eQq$ zhP_EBUHKNK13gL^AHWWG$nJ%}u3h_9`mXyK$khLvAbTaqYT9wa2bZ~d>mV|>evGcr zWVH|}JGS}D6Uwd*4v}bxdiys!<=<49dPZJ<>FVAy#{KKn)zuZBxBL}Z#{+2AQ4GRK zF^G_wbzLxURFE&~{#@@4>~)umXi^&>t%)c&$H+kzvlON7$Lqv8oi=Xr{h4!h7Xf>$2WC^8 zb3SJ8&(nySr)j{%wvqzCiw6&z^*F#WL|W#$fO&ekILYTmk)pyWQ@|5>xQx>MlJZQr zBO=^hhAqYr>R@@!Qi6iuDD-94t(ZE&18H7dn{2Co?Kx9-Du}BGNSh-lIX%($N|VA4Sm6ZD9qei45wNk8+l?$ZZ)VAAh4F51zXpUM|uJnNX7NT>j8jWP{`P zk2T+Q09RIFU<@_z_9>8xD0L&nWbKrBGY~pEAXAiD8azHHlg7zPC_kqi4Z3~vOw-X< zDx8k9Uurn^7p+EzmMZ#Z9&y3?h@hp2f?-4sk`VdcRF&&5rcPKBG{b)ZYIIY+cX2l< zEjasSSp@^vD8I85JVrZl;8h*jNTDoRHYSu+&|qGCnHYRa!r`I49vtG_5y)?ViB*U8 zzm6Kv(_jP2qJdn`<6{Tj$P#{ge^^t@w^ersSIR-0lsr43ErPYA1Q~xV7+D`YWkYe& z6+Z4?gOY;q2x2*i@xbhioRgm$>EolBlLU*9)`bp;RoJy+LAdUmp3a&>J=d~WP1}zr zBqlDP3I#aAaxP!JH+@I2_jyrn!L=Xa^eoL!Y3I|DfWJq+AZXb`w1M5LllGgJ9rw+~ zlWYagzHJ-?85I?y6braYc#|(W^L}hnb@8DI)P#qaka@wN$clt zytLc!xbJz=ePD1L=t{%jS&VWp>T7O5UQva^dVP{dNonP5^*f)SRA32QL4H`c+w`b% z@CQjm;i&uC4c^H(>Xal)?;8}H{o?ep`Hns})&{o_Fv0cKMd}I4i_cT0sKyfUrZnnSJZ4R8#FQOc!pP*7x+U`_3miTH*O)e{ye zYvCYzPjr4`I=pFoxUoFSD`dZ{D5nbPg5AUmy#t#FJXM{^`=~!HxxN z$1oFbyP2tXg|~rvOdv%ynAxePJ~DiPHrg>*ud9&Sqyrh;3vzV=%$Qu(yL>{cRCWB- zamaqI>(SuD?v;;&!!+B&YRkbrDZWM29POH|8|t2gXz7Oh{zTqzJZ9gJO29%g*$EU_ zhN2Z7thHJ)=ISG;doZE+iyoMncSb;;t)q2-E@6~||0KnxdY#* z<`t)>aeVVw-(#98ObwPWHmgFZ@al4vJ;X9~P z!?gr_RUB$WmV&}_!8q3Jqh}bAy1T8@aSf?J+-=@{^#UKzD*=VlB(O?|x~x9X_F9dt zG>X8n9FnF@0Q7>GgU`Mv;Hvm21jS0>2Q@N)fEPrLR2)N@wZY>fHy%kfd-zCO6~@-= z4}o~Ph1cOK8{QJCQU*s1`{7!p&cpA|d@$4K#gEPb&1!Ib?$s(B?LVn(kX|69n2XKw z0v9KfhyA^nwpjq;$qVGWlm`>-rhRJ+@s@qI&?m~n#Tjkh^kA*q!`r!Iz%AeBZJ;zx z3o5bg=FOc8O{L#&WaHp@yY#dGCftOYdKF4X4g!~wR|fQEN(^9&>%c=#<#IX5)s5>8 z-t`U&0of^ieTEwYC#ML7F@8MhUE|8_$_Hi|VO%YvBr$qV?t?)*VokEB+^Ol;o^M}r z4gE#C5o^ctBXBHR-T+XH7vjj>F7QJrpr)UM^{Md%wl!M)MiJi$wal#z8)>%xXB<=l zvw?eey?XK!2OR;4TJu3DB7&JN(zR{1OPQqjfZAJ%LnCeNGKnB|1BF^86lvsGccw(K zje)3Dq4W8y)U4SK5t8wF+T9V$X3%u-j*H*UEcMNL98Ob>DK?O=5?2d_Lrgw+GQZfF zRic*j{PGEOusuf#QF3~EC54NifmBygky?`slgy)j7+`fblwFMtRL4{cPr?F3dc{55 zbc27rKrdt98qjOb3w{|zQzD!d#%d1Gw*o`RFIf<+D~`yslsepGZE82)(oUd)c##JO>}AQ)Nk@}T!CWQGwGO{HLGd2oMPfY}}O?KnG&!!XE+z?0X=h*0LD z4*xXDDO15i8->N9L~NKS*)UT3tl_KfPP%ehy|oIYwIlsYh~EB|`a|UTyM>D&ycC0S zgd=T3X0Z4@oYYJ7Z8uuUx<1JVfEwP3)+T$C1psIfOLz74S|C_~Po-Kug@~wy;A!LIy|UUVL5|6SpOQ z@8c@02YB%1JLx1CLWrn=wrMkr=})dSf6y$UrqJ{Xwp&)o=@VgR27J>+`~qj(Z1V2{ zxag7sDhzJ6kXQzA4OL)?g;hU8SX4#65jL24B+C zKtJe0i{eQV_RN;+Z19}L;Q_7tic#{EZwB>oXE(x@21+|q(c-SD2L28kdKAI!%EDJh zs^Q;j*lyFawy_CaTDj~Cz?g5w4xyT+R2#xSG0fV9qbz7HnXO$k6`6AG+b{U$6o}Js z@Dy(KfiX6GQ@uvXFaQK}>9mD08@UQRc+&^>f z4B7p-s#Z`S8#{zlr)u^Vr?a?YV58yxG}JjpqB2YwE0`?Oduv~>+-a}fm5ba=0=1!Y zs_EOGKJkm3c49}cV_u?*khJJ?5N+wI*5*pfkalVSECqOZc@Zt?0cOULC$LtL*EG zlTy#ZW6_xka%FX&U-r}Qq|cNJ&of{IxYr+RM@Bscdqb@9A-}=3E3RKhu>p?LPM+?I zgS)C?FOVRQ@;zuQTTqr(on&~2@0pUT0(J%vK$XEHfot--JB{|Jwn`CpVGc{{^VcU* z%D{Tosd}tA-38oK0y-rYD@-w;aQ564v+TDvlo3B8y{kGdcc(xEd23yHzCLom_o2y& zfot$P=fIsnwx?Fuc?{8;5jS?98%|ysUasv)uWqMDMaqU#3#nr_+BLI*CU=qh=Z86K zYFRUlUY(kch_Oh@FamdyoVHOhyx-QFY0EWxp6As|p+Z44Iml= zE`)E|$Zph*~&g*F*e#kExnM*h*)DGlt>UjxG z9*FN^$H(45ZCedJt1mi16jpc>r{`_Q>j3+h;;M0chO)?fdXUI&h~0#{Cu@9J7x#4e z=pYOxt%#Gv5?m3zCt)9(p0=(H%1%*;@>ArMOdjRXW&744ndow!-H*Nz27#jpiTU@2 zlnxl9pDqV2J1Wzl)#bp|%Lh6$2ozR-8ZOWRZz?AsI4G1PbIV8bjI&D$x^v-uyo$Qy zNR!||^7E}bbq(?n;H6TtAU>B)2=10pf+slKmDji5RpdPTsO;MTRyuq=l)Z0#Qix5t zfQf|_3sck6_q++RA8-(gwgM>9k-^PHJZD*Fgew*na7gTTlfNi#BB)eR(;@9!c9jbI zu`|R%8eZH!b4vKn;*gmwKc@O2OX4l#x<`I!yKVMETFWtS5P z&S(Y@Fo~X~^;6#QD<}ep583CP1kmeywoe$?bB(DOY)sn~dK1}oT9Lw%b91iYPS5jI zDOxCNVdJvuLOv&V%~3*;KhWUK=g1Xnp?)D2NqI%cCVxXf*5UTis&+_rbcaa*1j)>Z z9b~WnczgfdE8BQ1VS=ktr#SJ_dr8CF#$!KZLUY2$*%}>^#(vU^T zJNNBfRdHN23!*JtbTdofos6QucGgteq^Bw>EPAditEm1haivX}{kQ`;JU8ICRIXpN zufQQ{&C$Th?Mk77FJP@yBcp|>H6q=E0IV2qn5WmzR5yP!A2CiJ?rk913d-MGlI$MZq(%1v1)b*x3j zI5Ka)BTGz2nyXkyEq9Ui3zauLs$YE=H@V0CZiBh~Cp|4r72AWO5eh=E?r?x~9ycA` zKDO8g9f*G>H#=80A|zR1@_o$=tbWAYxYVTk+c~V6jzKeTw*~gpA5S5fSm~__c04SD zSshqYCQl{s`6bK1lW8tn&&^eyF{z@4Xm~tE1Ew|b63(;%v;T_f*do-d?FMqnCRLu8 z5t3(oa5KqS5;#inq6EU^iB;g~2dEZ2kFb4hyb6BE^~j}Q(zKDy_?{t8>Jbt+$z3u; zjZPz>)C!HsYj629>yXr8jfh0r5Tqh3)j_DD{NZupC3!K}0y)r3YH@CTZarK#2OxuT zD6*&`8MK_a?8)KbC8%02myK{`+PK%Di#k19YNM6?+l+52|JOx2lC=kpW6(Zqmoe^_LLSPlS|^xNj>hb z19KQCRV*bM6snU5D9K6OzO8dtu(wH1%N&oGvxTFb+AyO8v*5SYtRBRz-vN)HC2SCE z?D|HTv3)G4g@t-9PVMHjg+z#^WiYV9aggI4d3v&FpB#=O*D)o{#sj0(KIbEs$L3i^p(3g*OQI$%T1u2tI<* z;WtUk9X+=@1S$F(%JN-`&!`qQ9a>><7hHV$}UR(C>#R$T#Dvko{1N?~965xCok7&JIB^PNn+?X0}=vT-ISa-&oFj}~!} zwe=&0nNY?@v^fflm(enkm!MeSAG|kIDa@p|&j^v|>0^!4x-nEA~ zJSq0lB;906v_MRIZN&F9>cZI`IqC615YjQvy+R_Y;plk#k;nUt26NBpsgH(5U6V9D zH>d8ipKYJI^5P`j8_-eXk>#ukhP8KiPSuq4JjxWa{mR0vW^ChJclnv`Sl?h^sssnh z)>C&=?rfbr?_IV1#s~k074)E&Qrdh<(a7*cmSiOPlowJ`MPx3LnVYyLJK;pzLFAp2 zNCKv~lU&Gva&$1UDOfjxaYa&0G{*BImK6dfvB<=*HY3I<8NDPPAM^Gk$j>{GnwpbC z8QHxGlFw$p!m;UHZ-KPGfK{n)Df(v+c}!>>yzfoi_Y@ws_+@6q`N!4?ja32?hR&anlsvkvlejDt0yFvBxO7{oCs z^Dv2n)p_S>!v!G1taxf#X-M(QNeKW`r$LSrQW+^vB{dlbu^(B|S1(HK?OSBI$v>M7jX417EUYs9-3+sB#~u7-^Aq2#F!opOAn#nNcXOL8ewp{zFg? z?qiGWPbHiCGvS}FSA${QHbU1_Z0%eFzm-38sr4i~s)W-B9+$*vQ}T}#aAP0>l9FfK zlAY_i!L4ar*k8j;z{_sSDeneftwH~cq-`Q(cH_J|`v^~Hph=WP~vsNLWU7rkQ`T%jVL5x2NN->yNFvm*{+tV%XoHGhbM)Sf3JAn!9^?5I&chl3}Wq2beDD1Ba|cw zvi^}z{an~$={MyFh>^&pNth#Rx1yIRbQ~T7vbgI~Y_{PcQnDE(0WH3C)oAIyEOQ-a z@hI0Gh^sONrayok+7Az_Z;sQHGBNYlZT#PRTWMCp@h~TdK%oqDo-0Fv37@JxuIJF4 z1P)9bZ2aee^1^bovk7mK0Z3c%$n$@~M~G)eJ#SE5)CJ?fH)GF6aK|iQ>I31fJ&ngc zKwB4HEXq1c7eJr5qY@gwA_BY_-9SWiPN&>!6qWjQ_R{f=FPTr*0ONs&!1+yO|20X9yo&ufEaiPK~@mS$muFa3)#2; zS423mm{*1x$tnwuP$AzG$vD*q$)h}U1U*kj$vlPtnTi%aAGH8&tF9<#_WeL0iDg^3(G@o4U-3=ysLa^l?Bq>;1QysNbe(LN%oT|pTDp9 z!ykTy%gJTOlFjWHN2aHB=wlc*RY#KCAfZ4-$v@8xCJ8y&kAm0^V%n+3;Z8P^Q_}lR zwFxh4Wm@oX%jlDiK?rLK*KtRi!OU^?9to@q8GbrL${&@qni~m`!V%O`I0?mfoL_Ci z%PBU7YQ-p_G=ZQB6%ClfuRMQG`aT(@kzc_9lbc-c8@Z=@TL41Y%FI&mJQVJ0<4BuKXu2W40lpN4KJtLK-d!R24WWd5m z;K~iD0FoX+4{}ArFf9RRx(j7>igw}O|2F*n#|c#hQM49hw-y~a5~Fb~b|7+9fTs^d zva$>KN?xaiB_qYpGFOPeJNmZR)XKrZfvACXr<|b;gvO8SRICNzZpA~1#;zm#2-nj7 zJUMU0a_9lB^uS5iKzydj#d7_%k1IPw_L|{+62)!?p+vD#y%Zxp{*FG2Zg93_TErR~ zlCXE>^fb}c-wS^7=)R`;huFVV-=gW~P$#+^g~N8_up)_q2r?dY_@t7d1OLIf2%jGh zTrB2S%5G(st-(oYBuA0qvwwX-urd$NiVFHjmH=|UI^Hr1TyBagv20hER@}%GD<*y>HK1}5p7}eC}fZ4gA*mU zQ3+7*?WQuzE}8xsXXnTBNAB>ge|x|GJ1};dgG@bwbT)o`Ag?GrS=CA(8cNY=S_XKs z&UFYQSPM!>_o79GL+jc6VjA-T=QQ>IBc5+c)oZXd9^^u&6c*WpWRfolZ^V-UsUv|M z?K=RJ@YBdY?^{R6618?~)pZson@1`5OZ|X5Ry&VJ=f>x3r2i#Nqi|zxJ&GP z;v!rEzbt7jTiEqjnV&q*iuAlrY+OgT7KCXQsQLsjOXLYq@)807LF9<&An}gwTH4_X zw`5SoEO_L{S7`kkn+dLC>~0CH*%|f140db=q{bZFW^`^ExZXQM2>=7!s~-EPT^DwP7D9;Oe0D+NfwpMFoPtMA=gF9iK0?V*2%QkKV0HpR{#Ai zNq5ZBk);rqzy{kOLXKuZM--_y-82|68G8_O46KM&v8J8x=W;6x&(9^C=$9T0RfdRkyg?Kk&RIGj~?P_i+0aMrgoS|zLRaK+Nb!P4yVK_??SdovqrUQX`g zoWkq}O&uJr*o$y+S^dvXaN5|Ja2=7V*TAc6ymC&%e$^`4bn@RnboW1?TlL4PRg^QQ zlwBT;beg#AGWt0?!Mkzep_^>^IyO2SMAul_P_p)KYr-j;QRl2 z14H{irSReY`&Xv$>X`rjJO12CcU|Fsf9aZ~|Er__`%5PUr2oJFSeY47V(uT?+qJs7 zy7Jv-hGMKQhRa5YwwnH|i#vb75y#@y-?3-!?Jga^Gs4DSGPANr+b93|j*zGO* zCKDq9twgWR%U6Z-_t>w!<{&dTCw=bR9rK37LpC-xvf+G#JF@@6vYoWE6CEN4ch$KU zEyZ3xJqzAOMn(poaJHu5&{S4QQB{c$uxf~<{4IQ9YRg{y*fo|4KU%?Tmx;?(o%t%K zPMz}c^;NEYdR8SyYOCW&i^A6Zd>^unor=+B;+p_n*Sp~(tZgoy?VgaYeuAM_)v^xJSNm^`oTf$P+1N?(Pi8Cx?I4io`Q<^1Ix%`FWSl z5-+{}>d5-Z-ipRN8@-uv57Z|F*!5K= z;U#6mczP8C*W+2Qt!4Xlumi&SquehxCaYw*aR~^hK6&y)#O~Xj)2B}}QM|VwP_2m) z%W)V~?5zkLSKy-|A5zik?d`4mMO~g>I1qN1V>U*Bz9`{$qMWHx*i zclmzuZ(?dvoKc^u!CRoiSo;3`AU=F;ONN0FPng5Zkcw=i zh>E25wjAM=k=88UtfVmOSJ(asD2zBq9(CWIojadmG4|@$s-|h0jkIKZj69LYU5%~b zzx-~)`*-h5@mazJZKAqddbWDA|6EyK)O>N}2~(18ksF)ZvqzGB5xVVpHla+tA09~J zFnPvW{c)}GQ<|0_hx+rA85tQ1!|5e_cHcfocQTPTe0tsu|NHsw*RNln;w6%#0~ku( zEt*nYZ*q_zaxF*}r)|fM9aJVRE-u-J$6vFtvr*EEX2ToT7?do}M!e0B*UL1#o3V5; zQbgD=YLJGOHbtkvi9$7ROpYsbof5B`U3JnpQTt^kn@TbhMKjkj?9JP^2W;<=H$V3) zEL~%%tKcfJEZnwnW9{hhBO|sQqOltVk&QJRN>IfzVbn|e)#*BxB1TFQB3(U>+cukShgxsr$4`5W7U>D zxFe5T#cO9mr^&1Toh1L{wCYd)J9p%xL|v)A%~>XLqunKX)NG@Q-D1nr^`*ycY{r>- z+TCjG+G@lXFQllYhcHz=$e@1_W!xaYc%<-JeTkmjX^Z={{6r3Z<4WC$(lL%k9nNaXj}RF_sr!ExG(0-&myh6 zi#aI!MH~{lJQuABD(>FBs}Rbm<1o~~N>MEHr{{MXX<_QQ&O?vg} zB))QtJf?Eueewt*{|q&!rwdrM z4TPCQamSn0KE3tNKmVlm_4M=@*spl~T+YMyZI;>#bwb%S>Ne2Pi8!2_>Z=Njj*kB4 z#tkgxA)LLU$&bf|8j=F5YFc*(mdA2v<(l@C`jMND;1~Deg^<`xgHp0W%;%%;ND z+S@A97cJqXdv7;K-7}p6hp9f<&H`sSdJ%_DILM7JYz?rBWUEg&jmRV1C}}4cl=&yw z_g7P6cQVVER)lcaIbB?w`hf6Rn`u7z#~_NM7Eh**dcMoV(_EYG zXE)YwA3~tov~8Q}g$urueN`!lbxEggtQ*2@^yxp8h2voQ<;|_O60Z`hP3rgWSA<2 zZQi4b%S7&hiQ7DzSU%>YeP7+NQUSv_RzX`@gke{i|4pEY1`;U5)ml3 zna$zz`wd0mG0!_mINIAQA(``;RMSy5_Gja)8=C$=PUZENB`crtG$E}-5`ixVxjjuI zOA1%0_O17Jm26W5v7bv*L-h%A;niY4R~FMJ+L}!Cf3!Duj4?|4n~rt76uCP8oLqyk zv1f>>gQI!f7wh6Y3=LyLxb&XU98(=YxXAbVSu7KD^hv0$8)dxYr)P4tgfn?JW^_1geXz+C9L)R;Vv`aj7Ht*qV08F640a~0JKjyWv zR62ee$FgntDu-7}SXhA4UFNwt8#_y@0t69~gz!0IEt;PVetGN7rWEh1>)K0$0C-;5 zzArnx?DdWHGc^)F2k?gZwmr8I1Y_EAEQULsyH#?`8x=c4u#M&uU*F@4p{v)dp#g;G zcxj)aoTO-87kd{`{MnmZn-hehX$7rK+jFf%u6({${YW?le?7{>qv$&MP2uiNX61BU zF~`w172H+J?6B^3TMoX9)=jWMO=0jwR#FngdKx$LqRgjpWQr z^BjwP?%u5(YD~#-n=y~XH3U*oyl~+Hby$Sj6VMAV5-#CYyzPK^Dxz7>oUHdY`i6Mf z2)>`Qa}yJ_@9zH1Ou-tf;>&e>VkBpk{l@2_*DGJ2q% z#+Molc?LhfzEN6YaQU*sw+}n`O+Othf01x8lE=EbAz7t@jXhk`q$a9y{ei2GU%h(8 z^meSfBprJ(jD=}?u73ktZmqmK(+JD4pWi&lx~tG^##3222q(*NWyzJ?X)(9yPtWz= zD$=uEC{NSMo9+$O+uy0EneRFkj~`DWakJESU$}^)Idzo`LJGfe5^KrMD!imLzm^=kx-rm@a~8KiboU^O5E0SD zhie9g!GT~~URfNcc&7b~6h@rZ?{Cy;gY~!l^SV%cwv~1ea&D*y(zq-CA z$9-;$=`AAgt{pp`AUBP+<(wyY6sIvTyK2YOhw94)1~YHBOOR{i8)7r;&&Ftnql!q5f#EkbC#;F^IZ6zP@Jt5n4@%5yotkb z6UUFmMGL&peqp=VuB%g3hW(-Y$W6jK3}F{p{f9qHS>P0jFOFm}(Ii)vb>fr^b5Y{JO}qLX zaj>`U<2i#va)D z26D2o$sou!AULFUZQr_;U%Uip*KBEig4H_IYvo6R=gNPs_#DD zX#_B_Nw!>LX@9y}=>s0Iz*;GDYgVu3(|_|PsoS!Qs}!GUzuW~Ryn|6{@Ah>M3ED&2 zCupzu=ZbsLY?}our*m7j*{^F@!^X~jL0?~A#BDker!VdSqx2D4-$)V11V zoiFXBoyR(AQD;p90S+(Eb(7=Y`|X>dvnTmLcXluW<=!yt&oeCFIWt%^i}$Y*%S z6lGjJe$r|sGVCE^K<~uF#C~35U)vFWUcJB;gk7Y#cvL6Cvco;Sy>YA8ZW_SaP0q}y zoH=t71yUW~b`sY7e=`v-tF2m|zjj&fu&-fGVh`DmNOPYFd9>q&#$`_~Zu`HUBaN22 z-#r%d5LMMtcJ|7lB4FT_Ex&@FicHi_zj~N(mRS$)uoCP*;C=%$4w7S0`NolCFAP@14k@X4H8evp9S(%m8exY1?App>qFJI0E^FV5P zfXvJv8eR*7396@p?Rq~ZU4HNLF_0zT>C>mXkT(%I_g7@TN7#IZe>DAi{Q*$JWsJw{ccJ z(<|X*{UWOTOj|gZRY}TqsxLSzD+}j4E=D?F2!YH8Szw?xhP+w9mphKbO&auG2z3eJ z{1*GK_dI`oJW|YE6$Mr1zNinI$mMk(FG@hr{P^~*Bz{`u!QrzG<6T8euQ{co^kk0& zuG_jl7AbV!I1t?hfU*Y>5fP*^1jHp*%(!>P_u~iGN=4(*IgL?;kplma2U8~4?uHoY z?K&^6oI)xL4*Fiup*mBPt-PGga^#2<%FyPCp0eJdp=2EVWW#dmrp=q{i#--eeS`C9 zfpcf)z%4n?i$Qd33nvCw3-^k*nAK9rYJ_Hb(7FDWTO9g*X?>_)OD zLZeAO!x+yAua&}ye4HXyf4!G>Mm>%o!I1JpFROg|hLWMoMF*jfPu3k(4axZgU9Bwr zP$_V-BDESoXe}OQ`ui72at^X;#Osq)QWR79_V3+mdX*t5>CO_+qv^*WRy#X8MRKxH z#-K_z2j?N>;(Rz2VtCA>B_Mj06+jq`#d6^o1&{hW6 zIsSm>{3+aVHiQq{l^Eon;iVr_1k^JT%1+N?CYU5^052Kj`9LIpQdc7^4gkiqWtjw! zn}+h%Z!h<0;3y@~0Hd7F^WVQ{7P@3ijAQkaz+irimM8%T?CtMQz!hRWeE2l#D-{%M zC;`pwbqQTqZb${Yh2@Mvh-JfK5rF#U%^PyW)~;R4iUJGal^k#5NXEj%(#vlLy{LCv<=0hC0bRJO_Sr zX&ZMQIFP<}^KSLd4&0)-I{x(Z;sqJS(~h+Eml~F$#EdFJRGyr^MUW1?gr_zF%mu|K zZ!+^u5p`(x?>86JHvi#io;<^xi}c9IKGfT5l(i5abfw-1`BBv>%K<^zk4{(-#|0mu z5#eH{Nz0}jY0110%V-9ewrSI*Iur_xv6Mh%6}1c&l!$=KNwzVWwmqddN6OSO6G=%) z0;W;kl_p9p0FW?;b)%~79Ue|02nL}#oX?CZ{&UIRU>@1#7cxH;>fGg8HrB8E{L-nd zdyk()2*_#A=|=rdk|4dX-5FFN1o+gdI9|qrF7!p%_!3kFiwpF1M~zBO zx2j;~x@~kvDN=$pRK82Et}%5^FtZ2=sn^HLDuwYFP1nmwBq1}ZzU{9Ji?J#GA*Fbl z<~_hs>&T`SZNEx723OFtGaz$Vk{Dc<7aR$M1BXzL6Fm*Y!LsoQO2)cGm)^m_B$U!E zsj0>wG^Ym?s-h(%C`X$K-UeVYVcZ2$&dg(RG%s1Nq_|NC(OT+#VoQrUdBNePG$q6U zKLP?1@L$?ax$)L4X~3?Wf(}H@7@sHvKEm4=J_0eMhVUK2)LR{?MUFEPGqvMgXQt7; z+goYl@zaExBo`+`q>Za&=`hj-d0TVvVAPr5oouN{TQaD$xlg;7J8mp?oidKuf4MW? zQE9~bL=HicNFip3Bb3vLpquIu&Y78&FI3@LIE{Cu;XTapP1Qf{0pJl(1-2yLeeT)H z!Z26CbyjJsJ;wYNP0FMw%r;YsxE_zvCQY|UlWYv&6N;=Dl;h>bi4#E4yt;FEh3Ko3 zcdOP*k~$2B|2 zr+l&}9EX(8N=v8cm3R(ymz1c?bqd;aDPw;ef%fnX0w4BEcwWt%${O((=YNuf+mr;A zB3#J!X`{(s^z>-}!{(q^Aes>dHOg&>6`_6@BuEoW^wrCbg)>uA^(V#)W3V;@xVUWv z&NjD?Ei5c9vLtncDX=cfhVMooQ*dw~fZ_>)@oTn9+C6&`fEyq^m{DVa;84Wt3LN+j z+}j9>%zUW;{Mvr;t1nc7-;pv0cZ-y$gxN+&PQl@1Q+Rrl`yxaRCJNClR>y$SQl)ku z^q)kDM(85|)Rzm>g9a+E-Erdg3tf3!JL3u*OXwe!WMvB329<++0Fij2Cm>N1us_SQ z?KTS&We2P{?NOacsb_2-6B! z%4=&s0vVT@VNj;IT#XdO+xBh1;0MxI>YJx1OPdm+Jzl{H;g7RKoA3%i|q~h-wh^7Z|%{!S1UDGu?>_F??4(pTt$U0O%y}&mIt%!2tns zA-Oel$N8N3qaODn((*Fb0t4> z_Q|uWKYzS375y_2hm0DFf446>G7cx(0tH{G->WcQ6U|$OC5vMxfH`HTkSh#t9=CEu z2{?l?uXXD^8ATjKew5k-#Zvj4$FvK2G(AiE9CK(gAH?G4O+oJyw!8C3oHQ~S0V04( zEEIKRnZ)Pq@v^j(jqUw_VwTk=#ElQE^`J%^dx%U55Pr zy#gH&B?<&bBT+^axjFjLpAev&YbOAmkfMaQpb{VpaN1gUXuPj49&l>^p=gj*AcDBc zT|!h&bEJ5;Aw@|UMC8#N_Puwn9-G1l`kYSKZlL7naystpW!_6*;$!r@mQ_%3;G(Hs zKkW`VXc{PpY2&@{W;L0XBakBu3rheY4g1Yn>5Fgt9ISZPwtwGqMuPIpsB#@_mOaWW0Y1c)mFWR9qo*gH9?;)p_GP&d%ATx&_# z4qlV$y;h4~?{ST&?%;yx(dF77sqE}Eat=w^ysPk~=lmCMreVugUBG1n0|URuaY5h1 z5K~zwtBN5#Eg(bne*Jnm?~REY;VN3eRa6XlG}lIDPnlpZ5c87u@fxe&IvaPB&9&R> zXAwdkC+i_!qx6c;$mHb%TR$T~HiqhXnoYG3!h9K-uyni-Rb9RDp6Vnc&(9}iHav)X z2UNiJA^64m8`&>Fsd0N0U;Ux{{L*Va*#n4H2$v^GkXON$3+;}0TTji#uI>sD>wA^x z?Jb@B&p*O;-##7iT6*wpoLNv%(2qf^E%Adpj=NRI3k7l<02l)4Y(PIJq0;Z3&tsg; znd+~xKtR~GT=U>*Ow7zkRt*rM28FF8A7Bg*t%4NDL;>o_-ZZ6&>Mq)MN)b_K20R7x zpkje(?Ne+>_{UH#F4jYaKM=@c!z0^b!d+C2E+Gy|1z(YqlOu?ONK#bJuh6s`U)r1Wv>c$JiNh^w)Aq(G zQzB|Z=kvs%zX}Dsny`yRrmUK%EL(mgY(La)Qi>RS|y29|Gk7_Ln($^xbuJ za~pcUM=vNyK4}=(no=t4FmQiD7%amuf_#O1X#BnjRQt4o6_y!Widq(|V`5_bUa8}w zDUJBoKvg<$_51bMn-N}jZ7Q{_-^Os!pI zL?OFbFJRM^nwWUBU@G%bm#Xu$R<0%Y>UV(eG8rH3@Eh?ZP&RfBbrpM@Xq~`;42z{z z))H|TxJ2i!nyza#*EfzR<2QB{$OAA6%g@f4mS-0InCh3?`P}8v)gQf~<+(Mr{mqdlocF7|-%P_6 zi&)kcsfLOturt2yqe6d1W!HF?rs{Zs|149K#QJS?rdd20L4R4&Bhh8(jkV|1Bwn_0 zLJ5$jQ=qDNx_896+e5Immxf*aQ{o+$@h-8^mJG&DtAIOqj_W4O3MLu})!Qv(XI{Nj zrrO-i>WakKP~zoP@D|mt$xUI#!-svx`mFBar&n&fAqObUXrR8R8k5w1Y1f-M)YMf~ z#nHN{sLwlwX*fBhwbdlrBZR5LtUU8+PFnX*9LUleRC3oz=Blnh(XV%P;?~4CBwZqw znAv3C*u|+W)XG5Fc_3kNX4qtIdpTRRVABkn}eDMUwSN!aDm` zxGOpPhSsgT?2l5yV=%R@_%WB8Mj%mUw!e$Pxr}CZx4bf)A$suOtCm?E9UXp`@0SXu z2wVXg2~3`OZs&3ZSv&fE?)lF7|=%S%gjD)cir z=~701M0`%Rs3x8b1_lNntSDPx+ayvdVRb}ozupABPs}1IGBM<^h9^7UhG7b&F@z8m zuzcY6eV+$UdkIE>CP5rD$niVJliwQ>3v|t@q7uh*UgnQ{Qc9$!>lB0-&f`-0J{!<; z`EpCS(= z<%Wv#^77PY-t5h9^L`2TGt#gP4DE-iDp{0Nn>K79tV|VW z+<_eOuzOeAPXB0}JQPeQ-MyKpfr?aAI()A*xUy%>EvAMvEqqNc4z=9)Hv$@abIV+{2SQWgF;hIl1qEo)$ zR^p(l97(zw%|L&`=_=E9?s!#fY1-G?#ZQkcs+JWmVq=>fTm0XGt8Yk8&K4X zmUl~Q^xoQ|d(O%0{F0!)OuHszD&OuG<;$g1MX#t798nMw38V}_;B%UzXZ>O`0CIl- zw~81-KvSf88W5)w)nRr-1N0Y*a4E520sA*XH_6d|yUtsDRqs*6q&%&Ow#wQvy9%p9 zWDQpF3o}D0>6$7Ya$747PC`fg4)q5ic(<=mPr$n>i#OLwZ+(3a|X+^jc8o>T7rZ9T1^- zi^ClPxYf;52tNlRMO_uKWXgd@WK&PCD(CKQ3Er9SEefjGryf=6+rHuK<7!C{cIFG` zj`;BNgTJ(Peh7ycl8iL5jy-trpzVci40kG2P$|a;K{s#T9s;VbBb1v*AQHfnkVd0= z+HFoZMO`OjU+6qc*&vgt(OGHd{t`7ya2SkM(`ffG;hrbKzIOj!o8U!jW0$(>XrS68k#!K8I}_)< z;E_r3u3*nQB_UCwyzI}t{&}pg)eVT=d~6)?xG46|6&0ytjrDIb-x#=^ex2m{=_Ajb z@Q&F6k42{*M-@l;@N3@X&Pi4 z%Id<|k=DTEDL05G(2;FsI%O17RMUu9NU1~h{1gteBoI#EdUZ3muQ6gq$-ZmNV}J1w zE$f~n*S)jEt@CVSTu3jsDB~|7Xw{W1zg(lQ{(glwB@!p01(D;4G)r(V)1bu99{~h0 zeyutvof`*aPJkm|D-i`_G;az2xzL~_w@>gGM8BA8*H-=b@q<-BplQSAhihV=J$nZK zj#PeI9T*Iv>2Kc6exHe6{Hi))MhH_dyM`iR&LO1r_Vh3UgGNkk*tShTH{r;O2M^OY zTa?~v-&LU^GDTZ92j{AHeb0zPvR+u4sv)#oPZ8W`gwRRabn?%2>ylw~gC=eq-exq` zSEFcZnmEyx`B6nRtNB(6a|P{3IOQm}wLEQBR#sF6hZPwo=2St^0pn@n{gqHJ!?7>} z@C~@_`HR&~QRRS!a57CZCzIC6@tvq*1wta?z=uzwbv-HR#WGWTA>Ar2E-z5}1@^hN zw3TS#Adq}j!=h+ZWSCJg+KO`<9cF0cJ8Yy)#J@)2j51B884V)51i<+tNxtL<9T0P( z7<|nD=zXc?`#`Q0^6wuLqLnNvZe&{i+}2H-kmm2h#=*IL`*vcLfk-1)7ilPUZ1`b; z>BW?w)zSXy)?c4}Lmd<}M0=7DNjr9X5Oj(vn;bBraskH&L3mx8x@CsCur1#)1=LRh;GPCyRgq_T zjTn+_k(L}fm#Pw{|CHig(4pj(q~n96fGZ(*2I&*f=7{6Kr`LO}$NaTrTAaZ!nc8=S zd5lobugxKRLZg~qBTN;9CxgPD>^$C8dZpr$7Thg$T`jv}jk+rbJRj|AKMNtY_(vaY zkCY28OGAc%wAfsSJ)3IkLnzLv)nf29014gir0s_?+X#Qc3`$lDP~D|f^I*vs6UAZA z#T#72!^>twj914Jt-|a{FoZ2_f2+sM+SH05@DY6s$&IV%qi8;{`cFu zD?-{6g$s>BXi6kT%OeESCl{@xwO1+U9liN+y(AN_|`4qYF z$pg(EqYAd~`t@HPi0fT4uvkb932ygiWNSJiUfKaY46rq9$L^WQNkybIB69{GRXB~x zS+yj?ZFa6`2MEaZjzeB&nF5rN=2QKmQ$H|@u9z=n>plJ z33p^J7U5W>z;{>5ch@^X4XjJUGo2GU`3@AQ2}F8=$nyB{7ccCjU5hxQBkvp&1ZgIo<+LfUnZ{e7)Z(LVtvBDH4>3~lZvUrp6L@K@w?4xK^q!(%;* zD7gGUEuA2Q+Oz#Q>%Ol{;furNKgRmxpH>9jg%5258^SVlmS&puO8w?Ib@B(mJje<- zJgDPVk{Mu_1GUhARCpL*j1b$z%7yaGuV(;3bEeCEoX7*JIu{eX(_m&7URW|0mZ-QS zX{UHXSB4HptYda@gfpjoCeUu>g4Lelkap9r*Rpy&`8=2A*~Q{Rp&Y{m6U2lM!SQ~g z%1cfCVzh3^b_KDI<^g@(+Z5YY#{^_mCW=Q#t-r++*y|Y-HB}RH^%CbX4F)|}w4cE3 zWatuwG&^`|9(!-D$=W%c zr>$mxXO0rw+I}ADT_Z@-9J6{zRder&Nfa@Nc)egWuz-+B#4sL5hFJ&TTXKZEKN@jY zjM3S^*&JB@?Rl~m81YGHmr!Fsv70pBayQS93Ei2?Y=4EFiRs&e^<^JJI8qQChrzm$ z&v5aP&=bPr0-{Nsz^XhN8nZv3hV%0_&Sjp5dQ&K% z_Q`7(44v>OxF@QnwJ}bpBU1ufn;sF<_IL3WboPXIrR_~3`%{uSI1JPVPw_xn}XYD zUpfc{o-~fT8eL4{Fnmj<6jX)rN+YaMgRbzJ5wT?c5F~FZ7v2M6je%dxwEB$T@i17l|~I zV_JJ!@r~bC6ldpUHY7#E(4lx*C=RIEsedBTU#xr(foH-EnMw6$==8grw*mwAwcq;j z@COiXoI3X)4%NP7zFzS1d-Sd<5S#lZ8a5^h(GV4A+A8bo>s=zlubH1bc{0DML>~lcUeyKG!R};w|?mt5ujAQam$tqQzjm8()Z-#RZSZ*R}5TlVfeV%K>)N38NK!?fJ+HYz*}*m&b>Y72!N~jZY}wy7lv};k z5DsAR>&+GPTDj_7y%?;f%dHE^Pg8Ly<(^DaSrp=;u}tV^v&MW!i=KytsA);zi-a5) z^!@lXVW!1c`)MbAapU;-0#j)#Z&bHgo0wR{pM@M%cq6nYA)Xo?Q++Cg&cicji!WjS zLF$zImw+PK!cjwwsWu4c@%+i;s@(1<(_fRiV6_wEI{JBzoVa-D(&LGP0oW?S$?+GW zWGdwzhNMwdr@JZ2YsEFjV(j_z=SEI7aGI?c29++0K}SG_eKCZK{}h#OP%}VRgPP z4%TN2mz<1HJZuObaCmx(VX}tziG`96kCsn~4Ns3?NK8O)Nc%kUkn`o_tD+s+xoy%H~dulri!D<614v42@sy3%lVRSB zT`V$|(#EVSHQU0f28EvY--i2lO+ z)~>z5YN@9c=+Z zYWTr>%q!wTI3MN9moEa|VHb{&WfHO$gd>iUSAv$6Pe4iRtY7B7A&xy5GPtO&t{%?7 zdA_o$sw!D=29}tSF==9#-BCy0PI7e&!Udmhf-=1X@542jNI#_P4&gd5*bnE;qZ(q$ zhFsT9KZjK$G{wX?(sM;;rx+y5&ISpe5;LCjAPs#l&BGR*4KYetE~3MRok9wcPO|_y zVd~{ed?F$u<=vNOndWHEFOqFA9ce3d}wtaoVAkKBU*CZN#fjx{t%q`a4GeoB#E^gs0 z=wrW#Q+=EdncH=wP%z_=1k3PPORb4P@mCK56=rnn0>MXc6sEwBO#3r8b*HfaqH=F# zn3Ce@9A5&|P*4f<6Oj!&YSHpsU+Otq5vZtGKwue}4J8hPb!P!>^0aK(j~+b-K&}Qy zm|wD22=P(U!`YU94R&N6z@=%_obXNypH=fi|mWh)1`1LK>f)iLA2PF?yWL|Q4w7U(&o z!KnPUDsfN4NqiPMPu`G^9JMsyKsu^=bF78anu9QyHr+oY>FdSPb(@|$U?dr23t>5# zDG>Vz_Fq}-{@IrHCvh7h>|P*xVEo0%@R33Y6C`L-e(tGz3431b%~8HMYhEH(V7(aM zk3$vpsh%70atoQZanQH0D@0Os@ zz0^4|w4?#ixMv2Q3B~1u6wR0U`9lyx++b7ymBpW;Pi(Ir9x&#hy^yl8@hlOr?8Sdo zj&?IJo!eRx|xx>t4MiUP*ucYdgj;4m{ zOb2+6+x?-t35QhVp2sw7KI39{68PC4p6&^d*sp)?JSw>+ zaQt)&TJNaH5jWOt<FtYT>LFkxp_@u>Mr zcalG>Y6AF;iv9Wk+m2$#VXQ+K1bN-i?K+&4-qF#>vECo^^Thv0js3aQFG2DheAJg5 z1B-zCr1C6`q0Px7)@NHZbN*rm+pNyxB10vj>FNFrTAlLSIhZ>>X}q+HQ+=k*3ciLo zcMuNmdwY8meFyXz}4xA|yIwYAw=Zb|jEOj1ELSU}U-qV1_o3 z3P3)1OkkXAL@NVTrx~cn3}#YNN?|eqS&kg*PECAL1&I5^-HcvSQeaBOH6olGE5C|g z#7an^_Ekrwpg>IoVgVEl<-Ul}=XcG{_ya8EBw)>qb!r_tbO;U;D%nz^ZsN^|QI*ia zXv85}Gk(CSAC1w(qfQ*VWLxkNh{=HsID=?!;FcZXGBk*;!9RsH_49XELlcrG!NB4Q zA0hM{thfvCgb|kv4VSJq5niF`IAT#F(bk1{L`^MtmeWr#Tz-28?Dma|Nzg7g6HT6E zAT)-PZaM&@BzznZ+mAhc-1TX)-XYxVj7X ze;QOC3S?F2m3XX1#3^EFB|sH@HL$xH{<(TR9Ny?cVR0ATj@Bq-O+K?aCgS!6)>Xtw z#VYLDx$`ifLdunP$jQczTe2;ikuu0y0X+=j=@oE|Ne4Y?^a1^%48t|i)Zmv_?)I3| zk&u1L zj=p=)o)c3YP1=fKwte7}WM|QQ)Br`Fcza22 zD?UCkv!U?V*MmQ(;bk&~qp8g2$vI#6d}BLl#>O#BK`~F*1FT>wI0w?thm(L*sGNK( zBVG#Tcs}b6A&fgXheHg`vL31|F=7KL4Ztv^+8<4{3%t!X4JU9x${Cl>Mv8lA68Z)h zEJ)Bs*SqorGycl6S`*o;hHCUCgY+P8mspZlioft6NdC!HTz4`DfVi}fbSp63VM7bC zH^Xzmn=HeE9s2v_%a^QZEFerZxT|!eX*M9mQX!DTu7?Z8#%g1{(HfRW{FqQt10+^H zL3>oeArzac&KDF=+%%Z*L)%O>0%_LcHn>fkk0kzpB71;r1yY3>oK?zL3l<>_3w$K3=2&{+Q{sz( zfL4zKLYj;lla!><(yIUl#Gg+1DS|Jd#8DkJsp)wHXKZg=Cw-s-eXGfMy?tKom5+o; zvzZc38FBf--Z>0pgECqTGTJT%hNz+MMMk%leZ?I%BL=_X{*B}V!bsKvd}j6UN83AU zXf4NpNCx0|@?tq~p8->XQ6K{}%pU<&F>Umf*?364;*}}<41U(0f8)8LLfADfFBL%Z zLraPr+QVEWIS?<&jsl`s`Y*oO%ryY>YY;ws)fb>;m5j^p`OpSOnjdgT;J9!q;B~aK z%XDWH=RglO>FXg4iBc!0a1zU%w6tMgPl*V?EQ1Bu%zRHOMT>ikI8F7Tua=?{v zSv=|_{KA47Lo15i!9myZo5F(|nqN|u*)YbAJO}lNclJ6C9POb)T2TBnQpQ-AhpmKQ z8vPF&Y;{pD*HU8{g#zKk-}XO<#Hlm|i%%jPAGq(cXU-hO(qQ9rPRJq=Dbc)x^C1^j zKz)_Ls`7GHS;5nb(}OWsOS8&{-1(l%`LOd*#kRkA@gfnu0!dL`Zs_x;VAuFpA|sRU zg$vsi97TUZ{DrWmsvyi$ryiil1PLleYBWT*+=4BzSQC6zHU%`@k|sWg#Y5+9 zrFHPc)gP0C@Vu)Kb1&RBh|21bS#dXIF2O;U)oGu{C^We=^z9qs-jPA%4}LaQw488I za^9=yx}N>T-1OH?UwP+x!A*2-k>wU+o`rOjgg1QNp!> zLM)58Va+gTQB(7Ro(B|8Ri7XvdSIj)jxhL}!|WQ{Btw=(A#y+0$Wf&6ojxZkil zKEA#Uc=t4hy*$U@EX6%#B<9p9T;}L?+Zilxwpm5)Ii{TW{meS4xSN|kL5M<*{-<{B z0^ZXRGb18;P=mByJz53}7u2wVE=2I2@l+romZZKe=pr64JZVb z7^G3N8(@PC=G2EkE|FD$5x5TIfG^u%$R8hp!^ysrSqPm1MnOPX2;WpDM!x;9BY&`plob@P9}2<4H9f+$nYPL7mB3hfA~7WD-e%5<&DW8KW+dD z$}o7LpE_hvLksHQC*Qtz5%EP%c_RqEB{2f_JwUpzY!##VMp>k zvPm|=B%vw+5eciNt*sq|>X$TZfy-2aRYE!8B7MqV?{KIub?lm)oy{G(SRF!~NaWby zC<5{j>rtBa%ev%`RL~ft{}L7hGMhsxI13;-39CuQWe^Fik8>8Qlmzk20^o>fwy3fU zlx1sCBAFE1&=NXsMnK(OS909>QrXBS+S$hnEiNs7tp8Kem+mdJt0 z|2)TShb7)XML!SAAaHC4b3bf}%6N(#r%_FUXTfsU;|S8w&@f{g33kDuCP|dItl^3l zs)|0PIX^$Y_qu|bTC=j+1&=x*y8>b#L%NOw!D|M33?C%?QDh_;eL!{=5IVzk%ehnp zCerOhIxonIpymM!)POw3%>3sC!qY>&O-9JVL=@A^DYOp9lf?rx`!(nSF%G-0{PfI5 z;Qj$W!7h1V2&(AOucoNw@(T(`qXB8Ngi=K=*f5AG)C2cH_Q%4bFo@4tneAvxqR-Oo zBFo*FToQeV3m4W!3!rRbe1lJoh!05A-p|}g_Oa;_#ekYKWL5-_-tRFILuhSGeoMiA z)FT6BgA@Q*pdgJYUb*s|bn`=9O9rLErj?sYPX8WVm)B2Jto1NCfvg5m*Fb_gqW=aa z|KxKI4wGw4I07^X3R$`u12oqGj?>^l@Seu5m$Jbc;KeLM*#8cTLF6VaX}j6gA49wQ zRe*w}B7KPfl&a(sk=0LL&)DrY%;Degj@mBEqEAo|rXS0Lx|`%Os8=Seog zYmh68fJe+1U0q#7bp){Gjo6+jvvK>DXNUrX*CyjGMBk~y#ZPnuJYr_6)z!;Hz6I-% z?L4kav^7ZcpRk(5`V?5sL|zm36cV@+7H1lG|Hz3F5Lu>x4Zv9;aHm>kr=U@n%z|*6 z9(V#Fco_HGZ0TEF>4P#m6B82;XcKO5j`Cw4h)V>}OaUS`TEW(mP~`>^ZAAR0MFnCM zC_q-q#wQ+HMUP2@``~u*NJTOG?~!QVC9H^z5zRNTH)XCMXt=$@=%ZKMxD+PJ$$DAF zek7tNRTS9u`jA!(%EmulgSavonq2kSmItlA=zk)0%O`{g6=3533|6A@q?fRgH(Rwz z)Ruso!m);=dPd8Q=%=0;D;Nj#)uzVc{2DJz0<0SsqASQXo@@ImhCPr{@Z5ITXG%n^4tF@ z0phcl50X-0Q4#oK4n4hYB*=s9um#-0m-r1o+?VbQVjP0hPxbCh&528O;lXQN<(9fYca* z1rILHmptx{*QCerKAJ^tc(JxP!VyE<^0{|UiKIy&q{e4DT7dFu@L5qW6rgi=E4@fV z-&HYDQ8M9#aM|R$M22uzq27U!0&+KD1j!f@QU(CH02DA%+R%+1gIt+Mh!NgXA%w8Eh!9l^M1b2Q(FP!vYO2XmT zf5IuX%2r!PM@jo4*a-AahfVYEyr%PKYP|_2e^8mg78EUx6$HHC|Myc1f)OTyRcQ&~ z