Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9c693f1
Merge branch 'pr236' into jian/cross-config-attention-pr-4
zhangj1an Aug 4, 2026
4a5f068
Merge branch 'pr238' into jian/cross-config-attention-pr-4
zhangj1an Aug 4, 2026
f2c6acb
feat(alignment): bind rollout and training attention contracts (#235 …
zhangj1an Aug 4, 2026
4ad305b
fix(alignment): bind dtype, batch size and split-KV policy (#235 PR4)
zhangj1an Aug 4, 2026
301f676
Merge remote-tracking branch 'pr236/feat/issue-235-attention-cp-contr…
inaniloquentee Aug 13, 2026
9832e3d
Merge remote-tracking branch 'upstream/feat/ws2-cp-attention-referenc…
inaniloquentee Aug 13, 2026
c30be4b
fix(attention): require runtime-verified cross-config binding
inaniloquentee Aug 13, 2026
59d68eb
fix(attention): type split-k runtime boundaries
inaniloquentee Aug 13, 2026
23f2803
Merge branch 'codex/update-pr238' into codex/update-pr263
inaniloquentee Aug 13, 2026
1bcb885
fix(attention): fail closed without runtime contract
inaniloquentee Aug 13, 2026
48a4130
fix(attention): export only available CUDA operators
inaniloquentee Aug 13, 2026
4de96a4
feat(attention): bind strict H100 QK norm and RoPE path
inaniloquentee Aug 15, 2026
d9e04b9
fix(attention): align projection collectives and runtime evidence
inaniloquentee Aug 16, 2026
46c5692
fix(attention): use portable projection plan default
inaniloquentee Aug 16, 2026
f55a681
fix(attention): satisfy PR4 lint checks
Aug 16, 2026
553d799
fix(types): narrow optional attention ops
Aug 16, 2026
62d73f8
feat(attention): bind shared strict core runtime evidence
inaniloquentee Aug 17, 2026
683dd8b
feat(attention): reuse WS1 PR315 deterministic operators
Aug 18, 2026
2e5cc49
feat(attention): add unified ablation matrix wrapper
Aug 18, 2026
a3c3d12
fix(attention): report selected core provenance
Aug 18, 2026
43e67c5
feat(attention): enforce canonical bitwise ablation core
inaniloquentee Aug 18, 2026
4177445
Merge remote-tracking branch 'upstream/test' into codex/review-pr263-…
inaniloquentee Aug 18, 2026
fb58a1c
fix(attention): fail closed without production CP backend
inaniloquentee Aug 18, 2026
c65dc30
Merge origin/test into PR #263
inaniloquentee Aug 24, 2026
9f22eb9
Merge branch 'test' into jian/cross-config-attention-pr-4
inaniloquentee Aug 24, 2026
cb142d1
Merge branch 'test' into jian/cross-config-attention-pr-4
inaniloquentee Aug 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions docs/operators/attention.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ friends) are validated against.
This op covers **only** the softmax attention. Qwen3's QK-Norm and RoPE are applied *before*
the call (see the chain), so the `q`, `k` passed in are already normalized and rotated.

For the WS2 Attention experiment, the measured boundary also includes QKV and `o_proj`
projections plus their TP/SP communication contracts. Those projections use native TE or
vLLM callables only after an H100 bitwise probe; otherwise both sides use the deterministic
`DetGemmOp` path with BF16 I/O, FP32 accumulation, ascending-K reduction, and Split-K disabled.
The model input RMSNorm and residual add remain outside this boundary.

```text
q --\
k ----softmax(QKᵀ/√d + mask)·V--> out
Expand Down Expand Up @@ -95,6 +101,34 @@ Existing WS1 implementations do not yet export attention-domain LSE or implement
CP merge, so they are declared incompatible with strict WS2 requests instead of being selected as
a silent fallback. See [WS2 CP-aware Attention contract](../design/ws2-cp-attention-contract.md).

### WS2 deterministic CP reference

### WS2 CP-aware dispatch

WS2 distributed callers use a separate contract-aware entry point,
`kernel_registry.get_attention_op(contract)`. It validates explicit TP/CP ownership, fixed
`(out, lse)` merge semantics, causal or packed-sequence offsets, and decode KV-cache identity
before selecting a backend. Legacy `get_op("attention")` behavior remains unchanged.

Existing WS1 implementations do not yet export attention-domain LSE or implement deterministic
CP merge, so they are declared incompatible with strict WS2 requests instead of being selected as
a silent fallback. See [WS2 CP-aware Attention contract](../design/ws2-cp-attention-contract.md).

Split-KV is part of that contract rather than a recorded backend extra. Strict runs allow
`disabled` or a fixed logical KV chunk size, and must export the actual per-CP-owner block
boundaries, FP32 `(out, lse)` merge order, final downcast point, backend, and fallback reason.
Runtime-selected `auto` plans are diagnostic only unless both training and rollout export and
validate the same actual plan.

The rank-aware drift benchmark can emit a CPU smoke artifact or a torchrun-friendly GPU report:

```bash
python benchmarks/benchmark_ws2_cp_attention_drift.py --smoke --json
python benchmarks/benchmark_ws2_cp_attention_drift.py --smoke --tp-world-sizes 2 \
--cp-world-sizes 2 --kv-chunk-sizes none,1 --include-backward \
--output artifacts/ws2-cp-attention-drift.json
```

Split-KV is part of that contract rather than a recorded backend extra. Strict runs allow
`disabled` or a fixed logical KV chunk size, and must export the actual per-CP-owner block
boundaries, FP32 `(out, lse)` merge order, final downcast point, backend, and fallback reason.
Expand Down
58 changes: 58 additions & 0 deletions rl_engine/kernels/gtest/tolerance.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,62 @@ def load_contract(
return contract


def resolve_logprob_threshold(dtype: Any) -> float:
"""Return the fixed WS1 selected-logprob absolute-difference threshold.

The contract path is intentionally not configurable through this accessor.
Cross-configuration experiment definitions may select a dtype, but they cannot
inject or override a numerical threshold.
"""

dtype_name = _normalize_dtype_name(dtype)
contract = load_contract()
try:
values = contract["accuracy"]["default"]["logprob"][dtype_name]
raw_threshold = values["atol"]
except (KeyError, TypeError) as exc:
raise ValueError(f"WS1 has no logprob threshold for dtype {dtype_name!r}") from exc
if isinstance(raw_threshold, bool) or not isinstance(raw_threshold, (int, float)):
raise ValueError(f"invalid WS1 logprob threshold for dtype {dtype_name!r}")
threshold = float(raw_threshold)
if not math.isfinite(threshold) or threshold < 0.0:
raise ValueError(f"invalid WS1 logprob threshold for dtype {dtype_name!r}")
return threshold


def tolerance_contract_fingerprint() -> str:
"""Return a deterministic fingerprint of the current WS1 contract contents."""

canonical = json.dumps(
load_contract(),
ensure_ascii=True,
separators=(",", ":"),
sort_keys=True,
).encode("utf-8")
return hashlib.sha256(canonical).hexdigest()


def _normalize_dtype_name(dtype: Any) -> str:
normalized = str(dtype).strip().lower().replace("torch.", "").replace("-", "")
aliases = {
"bf16": "bfloat16",
"bfloat16": "bfloat16",
"fp16": "float16",
"float16": "float16",
"half": "float16",
"fp32": "float32",
"float32": "float32",
"float": "float32",
}
try:
return aliases[normalized]
except KeyError as exc:
valid = ", ".join(sorted(set(aliases.values())))
raise ValueError(
f"unsupported WS1 logprob dtype {dtype!r}; expected one of: {valid}"
) from exc


def validate_contract_schema(contract: Mapping[str, Any]) -> None:
"""Validate four-judgment schema, dtype policy, roles, and aggregates."""

Expand Down Expand Up @@ -1056,6 +1112,8 @@ def tolerance_contract_fingerprint() -> str:
"tolerance_contract_fingerprint",
"resolve_tolerance",
"resolve_tolerance_support",
"resolve_logprob_threshold",
"tolerance_contract_fingerprint",
"validate_backend_provenance",
"validate_contract_schema",
]
45 changes: 45 additions & 0 deletions tests/test_attention_ablation.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,44 @@ def forward_with_lse(self, q, k, v, *, causal, scale):
assert torch.equal(result.out, q)


def test_cp_production_configuration_fails_closed_without_ag_rs_backend():
q, k, v = _qkv()
cp_sharding = ShardingSpec(
tp_rank=0,
tp_world_size=1,
cp_rank=0,
cp_world_size=2,
global_q_heads=2,
global_kv_heads=1,
local_q_head_start=0,
local_q_heads=2,
local_kv_head_start=0,
local_kv_heads=1,
global_sequence_length=4,
local_sequence_length=2,
global_block_indices=(0,),
global_block_token_starts=(0,),
local_block_offsets=(0, 2),
)
contract = AttentionContract(
role=AttentionRole.TRAIN,
mode=AttentionMode.PREFILL,
dtype=AttentionDType.BF16,
batch_size=1,
query_sequence_length=2,
head_dim=4,
causal=True,
causal_offsets=(0,),
sharding=cp_sharding,
reduction=ReductionSpec(),
split_kv=SplitKVSpec.disabled(),
)
with pytest.raises(AttentionContractError, match="injected AG/RS backend"):
AttentionAblationOp(communication_backend="self_owned_cuda_ag_rs")(
q[:, :, :2], k[:, :, :2], v[:, :, :2], contract=contract
)


def test_wrapper_owned_deterministic_core_does_not_require_external_provenance():
q, k, v = _qkv()

Expand Down Expand Up @@ -279,6 +317,13 @@ def __call__(self, q, k, v, *, causal, scale):
assert result.provenance["production_ready"] is True


def test_deterministic_attention_rejects_runtime_split_kv_auto():
q, k, v = _qkv()
contract = _contract(split_kv=SplitKVSpec.auto(strict_consistency=False))
with pytest.raises(AttentionContractError, match="Split-KV"):
AttentionAblationOp()(q, k, v, contract=contract)


@pytest.mark.parametrize(
("missing_field", "replacement"),
[
Expand Down
2 changes: 2 additions & 0 deletions tests/test_attention_preprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ def _inputs():
@requires_h100_preprocess
def test_h100_preprocessor_executes_cuda_qk_norm_and_zigzag_rope():
q, k, q_weight, k_weight, positions = _inputs()
result = H100AttentionPreprocessor()(q, k, q_weight, k_weight, positions)
result = H100AttentionPreprocessor(reuse_transformer_engine_qk_norm=False)(
q, k, q_weight, k_weight, positions
)
Expand Down Expand Up @@ -179,6 +180,7 @@ def test_h100_preprocessor_executes_cuda_qk_norm_and_zigzag_rope():
@requires_h100_preprocess
def test_h100_preprocessor_is_bitwise_batch_invariant_for_2d_positions():
q, k, q_weight, k_weight, positions = _inputs()
op = H100AttentionPreprocessor()
op = H100AttentionPreprocessor(reuse_transformer_engine_qk_norm=False)
full = op(q, k, q_weight, k_weight, positions)

Expand Down
17 changes: 7 additions & 10 deletions tests/test_cp_attention.py
Original file line number Diff line number Diff line change
Expand Up @@ -745,18 +745,15 @@ def test_strict_core_is_bitwise_invariant_to_batch_and_cp_schedule(dtype):
op = DeterministicCPAttentionReferenceOp(strict_bitwise=True)

cp1_out, cp1_lse = op.forward_with_lse(
q,
k,
v,
cp_world_size=1,
kv_chunk_size=None,
q, k, v, cp_world_size=1, kv_chunk_size=3
)
cp2_out, cp2_lse = op.forward_with_lse(
q,
k,
v,
cp_world_size=2,
kv_chunk_size=3,
q, k, v, cp_world_size=2, kv_chunk_size=3
)
cp1_out, cp1_lse = op.forward_with_lse(q, k, v, cp_world_size=1, kv_chunk_size=3)
cp2_out, cp2_lse = op.forward_with_lse(q, k, v, cp_world_size=2, kv_chunk_size=3)
single_out, single_lse = op.forward_with_lse(
q[:1], k[:1], v[:1], cp_world_size=1, kv_chunk_size=3
)
assert torch.equal(cp1_out, cp2_out)
assert torch.equal(cp1_lse, cp2_lse)
Expand Down
15 changes: 11 additions & 4 deletions tests/test_rms_norm.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@
import torch
import torch.nn.functional as F

from rl_engine.kernels.ops.cuda.norm.rmsnorm import rmsnorm_cuda
from rl_engine.kernels.ops.cuda.norm.rmsnorm import RMSNormCudaOp, rmsnorm_cuda
from rl_engine.kernels.ops.pytorch.norm.rms_norm import NativeRMSNormOp
from rl_engine.kernels.ops.triton.rmsnorm_triton import rmsnorm_triton

try:
from rl_engine.kernels.ops.base import _C, _EXT_AVAILABLE

_HAS_CUDA_RMSNORM = _EXT_AVAILABLE and hasattr(_C, "rmsnorm_forward")
_HAS_CUDA_RMSNORM = _EXT_AVAILABLE and all(
hasattr(_C, name)
for name in ("rmsnorm_forward", "rmsnorm_backward_dx", "rmsnorm_backward_dw")
)
except ImportError: # pragma: no cover - import can fail when the extension is not built.
_HAS_CUDA_RMSNORM = False

Expand Down Expand Up @@ -236,8 +239,12 @@ def test_registry_dispatches_rms_norm():
from rl_engine.kernels.registry import kernel_registry

op = kernel_registry.get_op("rms_norm")
assert isinstance(op, NativeRMSNormOp)
assert hasattr(op, "forward") and hasattr(op, "forward_fp32")
if torch.cuda.is_available() and _HAS_CUDA_RMSNORM:
assert isinstance(op, RMSNormCudaOp)
assert hasattr(op, "forward")
else:
assert isinstance(op, NativeRMSNormOp)
assert hasattr(op, "forward") and hasattr(op, "forward_fp32")


@requires_cuda
Expand Down
6 changes: 6 additions & 0 deletions tests/test_tolerance_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@
from __future__ import annotations

import copy
import hashlib
import inspect
import json
import math

import pytest
import torch

from rl_engine.kernels.gtest import tolerance as tolerance_module
from rl_engine.kernels.gtest.tolerance import (
CHAIN_AGGREGATE_METRICS,
JUDGMENTS,
Expand All @@ -26,8 +30,10 @@
resolve_chain_aggregate_thresholds,
resolve_comparison_roles,
resolve_dtype_policy,
resolve_logprob_threshold,
resolve_tolerance,
resolve_tolerance_support,
tolerance_contract_fingerprint,
validate_backend_provenance,
validate_contract_schema,
)
Expand Down
Loading