Skip to content

feat(attention): add decode-stage KV-cache CP replay - #260

Open
JLiu4Coding wants to merge 2 commits into
RL-Align:feat/ws2-attention-single-gpu-harness-pr2from
JLiu4Coding:codex/ws2-decode-kv-replay
Open

feat(attention): add decode-stage KV-cache CP replay#260
JLiu4Coding wants to merge 2 commits into
RL-Align:feat/ws2-attention-single-gpu-harness-pr2from
JLiu4Coding:codex/ws2-decode-kv-replay

Conversation

@JLiu4Coding

@JLiu4Coding JLiu4Coding commented Aug 3, 2026

Copy link
Copy Markdown

Implements PR6: Decode-stage KV-cache CP attention replay from #235. This is currently stacked on #253 and will be retargeted to main after #253 is merged.

Summary

  • Add a correctness-first decode-stage KV-cache replay harness for Sq=1 and few-query attention.
  • Compare physically paged replay with the same queries over a fully materialized logical KV sequence.
  • Validate cache position, KV sequence length, block/page mapping, logical token positions, prefix-cache identity, RoPE state, and CP block ownership before execution.
  • Merge FP32 (out, lse) partial states in fixed logical block order and downcast only at final write.
  • Reuse PR2 drift reporting for out, attention-domain lse, and optional active-token selected-logprob drift.
  • Preserve Transformer Engine as an optional merge oracle through PR2's existing capability-probed adapter.

Scope and design

DecodeKVCacheMetadata separates physical page layout from logical sequence identity. The replay reconstructs logical blocks from the page table, computes per-block partial attention state, and merges by global_block_index; physical page order and CP ownership do not select the numerical reduction order.

RoPE identity is explicit for decode queries and cached K. Pre-RoPE inputs are materialized with the declared query/key positions, while post-RoPE inputs are consumed directly. Inconsistent cache/query positions, cached-key positions, duplicate pages, missing logical positions, and invalid prefix-cache provenance fail loudly.

This PR reuses Transformer Engine only as an optional validation oracle for the FP32 (out, lse) merge, through the capability-probed context-parallel correction helpers introduced in #253. Transformer Engine does not construct or interpret the KV cache, determine the logical block order, or replace RL-Kernel’s deterministic reference implementation.

This PR models CP block ownership on one device for attributable validation. A later integration with PR3's transport can gather the same partial states without changing the deterministic merge contract.

Validation

  • pre-commit run --files rl_engine/testing/attention_comparison.py rl_engine/testing/__init__.py tests/test_attention_comparison.py docs/design/ws2-attention-decode-replay.md
  • pytest tests/test_attention_comparison.py -q — 17 passed
  • pytest tests/test_attention_comparison.py rl_engine/tests/test_dispatch.py tests/test_kv_cache_attention.py -q -k "not large and not gpu" — 34 passed, 4 deselected
  • mypy --ignore-missing-imports rl_engine/ — passed for 90 source files
  • pytest tests/test_attention.py -q -k "not large and not gpu" — 24 passed, 2 deselected
  • pytest tests/test_attention_correctness.py -q -rs — 127 GPU-only cases skipped because CUDA/ROCm is unavailable locally
  • mkdocs build --strict -f mkdocs.yaml — passed
  • git diff --check — passed

DCO

  • Commit includes Signed-off-by.

Summary by CodeRabbit

  • New Features

    • Added decode-attention comparison support for paged KV-cache replay.
    • Added validation for cache layouts, logical positions, prefix caching, RoPE state, and context-parallel ownership.
    • Added output, log-sum-exp, and log-probability drift reporting.
    • Added support for grouped-query attention layouts and multiple query configurations.
  • Documentation

    • Documented decode replay validation, cache reconstruction, causal participation, equivalence checks, and distributed ownership boundaries.
  • Tests

    • Added comprehensive coverage for cache layout, RoPE, ownership, and replay accuracy scenarios.

Signed-off-by: JLiu4Coding <lzwgre@126.com>
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

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

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

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6a712d30-223f-4366-84fb-05d9fc173f8c

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

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Changes

The testing package adds decode-attention metadata, paged-KV replay, logical full-prefill reference execution, drift comparison, cache validation, RoPE handling, context-parallel ownership tracking, and comprehensive replay tests. The design documentation defines the corresponding contracts and validation rules.

Decode attention contracts and validation

Layer / File(s) Summary
Decode contracts and validation
rl_engine/testing/attention_comparison.py, docs/design/ws2-attention-decode-replay.md
Defines decode cache metadata, attention inputs, RoPE states, cache layout rules, ownership rules, position checks, and validation failures.

Reference and paged replay execution

Layer / File(s) Summary
Reference and paged replay execution
rl_engine/testing/attention_comparison.py, rl_engine/testing/__init__.py
Reconstructs logical KV sequences, runs teacher-forcing and paged replay paths, merges partial states, computes drift metrics, and exports the new APIs.

Replay fixtures and behavioral coverage

Layer / File(s) Summary
Replay fixtures and behavioral coverage
tests/test_attention_comparison.py
Tests multi-query and single-query replay, physical-page and prefix-cache invariance, context-parallel ownership, pre- and post-RoPE equivalence, validation failures, and Qwen3 grouped-query attention.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant compare_decode_kv_replay
  participant run_decode_full_prefill_reference
  participant run_decode_kv_replay
  participant MergeBackend
  compare_decode_kv_replay->>run_decode_full_prefill_reference: reconstruct and execute logical KV reference
  compare_decode_kv_replay->>run_decode_kv_replay: execute visible paged KV blocks
  run_decode_kv_replay->>MergeBackend: merge FP32 partial attention states
  compare_decode_kv_replay-->>compare_decode_kv_replay: compare output, LSE, and selected log-prob drift
Loading

Possibly related PRs

Suggested reviewers: inaniloquentee, kjldefeated, flink-ddd

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding decode-stage KV-cache context-parallel replay.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@JLiu4Coding

Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
docs/design/ws2-attention-decode-replay.md (1)

23-25: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document the RoPE restrictions enforced by validation.

_validate_decode_inputs rejects rope_cast_at != "after_rope" and any rope_rotary_dim that differs from head_dim. Partial rotary is therefore unsupported today. Add these two constraints to the validation list so callers learn the limit from the design document.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/design/ws2-attention-decode-replay.md` around lines 23 - 25, Update the
metadata validation list in the design document to state that rope_cast_at must
be "after_rope" and rope_rotary_dim must equal head_dim. Explicitly indicate
that partial rotary is unsupported, alongside the existing validation
constraints.
rl_engine/testing/attention_comparison.py (2)

690-709: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Vectorize the slot reconstruction.

The nested loop calls .item() once per cached token. On CUDA each call is a host sync. The mapping is a pure function of block_table, page_size, and kv_seq_lens, so it can be built with tensor ops. Validation already guarantees the logical positions equal arange(sequence_length).

♻️ Proposed vectorized slot index
-    physical_slots: list[int] = []
-    logical_positions: list[int] = []
-    logical_block_count = math.ceil(sequence_length / metadata.page_size)
-    for logical_block in range(logical_block_count):
-        physical_page = int(metadata.block_table[batch_index, logical_block].item())
-        tokens_in_block = min(
-            metadata.page_size,
-            sequence_length - logical_block * metadata.page_size,
-        )
-        for page_offset in range(tokens_in_block):
-            slot = physical_page * metadata.page_size + page_offset
-            physical_slots.append(slot)
-            logical_positions.append(int(metadata.global_token_positions[batch_index, slot].item()))
-
-    slot_index = torch.tensor(physical_slots, device=inputs.k_cache.device, dtype=torch.long)
-    logical_position_tensor = torch.tensor(
-        logical_positions,
-        device=inputs.k_cache.device,
-        dtype=torch.long,
-    )
+    logical_block_count = math.ceil(sequence_length / metadata.page_size)
+    device = inputs.k_cache.device
+    logical_index = torch.arange(sequence_length, device=device, dtype=torch.long)
+    pages = metadata.block_table[batch_index, :logical_block_count].to(device=device).long()
+    slot_index = (
+        pages[logical_index // metadata.page_size] * metadata.page_size
+        + logical_index % metadata.page_size
+    )
+    logical_position_tensor = metadata.global_token_positions[batch_index, slot_index].long()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rl_engine/testing/attention_comparison.py` around lines 690 - 709, Replace
the nested Python loops in the slot reconstruction with device-side tensor
operations derived from block_table, page_size, and sequence_length/kv_seq_lens,
eliminating per-token .item() calls and CUDA synchronization. Construct
slot_index in logical token order and derive logical_position_tensor as the
corresponding arange(sequence_length) while preserving the existing device and
dtype behavior.

264-266: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Validation runs three times per comparison.

compare_decode_kv_replay validates, then run_decode_full_prefill_reference and run_decode_kv_replay validate again. _validate_decode_inputs uses per-batch Python loops and scalar .item() reads, so the cost is paid three times. Consider an internal _validated flag or a private unvalidated entry point if the harness is used on larger batches.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rl_engine/testing/attention_comparison.py` around lines 264 - 266, Eliminate
the repeated input validation in compare_decode_kv_replay by adding an internal
validated path or _validated flag to run_decode_full_prefill_reference and
run_decode_kv_replay. Validate inputs once in compare_decode_kv_replay, then
have both execution functions skip _validate_decode_inputs while preserving
normal validation for standalone callers.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@rl_engine/testing/attention_comparison.py`:
- Line 339: Update the merge-order collection around merge_orders so it is
nested per batch, matching the batch structure of cp_block_owners. Initialize
one inner list per batch, append each query’s order to that batch’s list, and
expose the resulting B × Sq structure through logical_merge_orders at all
affected locations.
- Around line 714-724: The pre-RoPE K path currently casts using the query dtype
from _decode_rope_output_dtype. Update the dtype handling around
_decode_rope_output_dtype and _validate_decode_inputs so K uses the K-cache
dtype, either by adding a K-cache dtype default or validating q.dtype ==
k_cache.dtype; ensure the pre-RoPE Q path continues using the query dtype and
the pre-RoPE K path preserves the cache dtype.

In `@tests/test_attention_comparison.py`:
- Around line 383-385: Update the output drift assertion in
compare_decode_kv_replay to use a bfloat16-scale tolerance rather than 1.0e-6,
since report.drifts[0].out compares bfloat16 results that may round differently.
Keep the existing tight 1.0e-6 tolerance for report.drifts[0].lse.

---

Nitpick comments:
In `@docs/design/ws2-attention-decode-replay.md`:
- Around line 23-25: Update the metadata validation list in the design document
to state that rope_cast_at must be "after_rope" and rope_rotary_dim must equal
head_dim. Explicitly indicate that partial rotary is unsupported, alongside the
existing validation constraints.

In `@rl_engine/testing/attention_comparison.py`:
- Around line 690-709: Replace the nested Python loops in the slot
reconstruction with device-side tensor operations derived from block_table,
page_size, and sequence_length/kv_seq_lens, eliminating per-token .item() calls
and CUDA synchronization. Construct slot_index in logical token order and derive
logical_position_tensor as the corresponding arange(sequence_length) while
preserving the existing device and dtype behavior.
- Around line 264-266: Eliminate the repeated input validation in
compare_decode_kv_replay by adding an internal validated path or _validated flag
to run_decode_full_prefill_reference and run_decode_kv_replay. Validate inputs
once in compare_decode_kv_replay, then have both execution functions skip
_validate_decode_inputs while preserving normal validation for standalone
callers.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: cf3bf3d9-a32f-420f-a386-a20ea4905588

📥 Commits

Reviewing files that changed from the base of the PR and between cbb5e2c and 7054d99.

📒 Files selected for processing (4)
  • docs/design/ws2-attention-decode-replay.md
  • rl_engine/testing/__init__.py
  • rl_engine/testing/attention_comparison.py
  • tests/test_attention_comparison.py

_validate_decode_inputs(inputs)
outs: list[torch.Tensor] = []
lses: list[torch.Tensor] = []
merge_orders: list[list[int]] = []

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

logical_merge_orders loses the batch dimension.

merge_orders.append(order) runs inside the query loop, so the list is flat with B * Sq entries. cp_block_owners next to it is nested per batch. A provenance consumer cannot map a merge order back to its batch when Sq > 1, and the two fields disagree in shape. Nest the merge orders per batch.

🐛 Proposed fix for per-batch merge-order provenance
-    merge_orders: list[list[int]] = []
+    merge_orders: list[list[list[int]]] = []
     cp_block_owners: list[list[int]] = []
     for batch_index in range(inputs.q.size(0)):
         q, k, v, logical_positions = _decode_logical_qkv(inputs, batch_index)
         owners = _logical_block_owners(inputs, batch_index)
         cp_block_owners.append(owners)
         batch_out: list[torch.Tensor] = []
         batch_lse: list[torch.Tensor] = []
+        batch_orders: list[list[int]] = []
             out, lse = _merge_partial_states(states, backend=merge_backend)
             batch_out.append(out.to(inputs.output_dtype))
             batch_lse.append(lse)
-            merge_orders.append(order)
+            batch_orders.append(order)
+        merge_orders.append(batch_orders)
         outs.append(torch.cat(batch_out, dim=2))
         lses.append(torch.cat(batch_lse, dim=2))

Also applies to: 386-386, 409-411

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rl_engine/testing/attention_comparison.py` at line 339, Update the
merge-order collection around merge_orders so it is nested per batch, matching
the batch structure of cp_block_owners. Initialize one inner list per batch,
append each query’s order to that batch’s list, and expose the resulting B × Sq
structure through logical_merge_orders at all affected locations.

Comment on lines +714 to +724
rope = NativeRoPEOp()
output_dtype = _decode_rope_output_dtype(inputs)
if metadata.q_rope_state == "pre_rope":
q = rope.forward_fp32(
q,
metadata.query_position_ids[batch_index : batch_index + 1],
theta=inputs.rope_theta,
).to(output_dtype)
if metadata.k_cache_rope_state == "pre_rope":
key_positions = metadata.key_position_ids[batch_index, slot_index].unsqueeze(0)
k = rope.forward_fp32(k, key_positions, theta=inputs.rope_theta).to(output_dtype)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check whether any caller builds decode inputs with mixed q / k_cache dtypes.
rg -n -C 6 'DecodeAttentionInputs\(' --type=py

Repository: RL-Align/RL-Kernel

Length of output: 156


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and inspect the target file around the relevant lines, plus related helpers/tests.
printf 'Files with attention_comparison.py:\n'
fd -a 'attention_comparison\.py$' . || true

printf '\nTarget outline:\n'
ast-grep outline rl_engine/testing/attention_comparison.py --match "_decode_rope_output_dtype" --view expanded || true
ast-grep outline rl_engine/testing/attention_comparison.py --match "DecodeAttentionInputs" --view expanded || true

printf '\nRelevant snippets:\n'
sed -n '680,750p' rl_engine/testing/attention_comparison.py
printf '\n--- helper region ---\n'
sed -n '1120,1290p' rl_engine/testing/attention_comparison.py

printf '\nDecodeAttentionInputs references (all files):\n'
rg -n -C 8 'DecodeAttentionInputs|_decode_rope_output_dtype|rope_state' . --glob '*.py' || true

printf '\nInput dtype construction references:\n'
rg -n -C 5 'q_dtype|k_cache_dtype|dtype|q_cache|rope_theta|batch_index|slot_index' rl_engine/testing/attention_comparison.py --glob '*.py' | sed -n '1,260p'

Repository: RL-Align/RL-Kernel

Length of output: 50374


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Imports at top of core file:\n'
sed -n '1,45p' rl_engine/testing/attention_comparison.py

printf '\nDecode inputs builder in tests and dtype helpers:\n'
sed -n '35,95p' tests/test_attention_comparison.py

printf '\nAll tensor dtype constructors/replacements in compare files/tests:\n'
rg -n -C 3 '(float32|float64|bfloat16|half|float16)\)|dtype=.*torch|torch\.empty_like|torch\.randn_like|torch\.empty|torch\.randn' rl_engine/testing/attention_comparison.py tests/test_attention_comparison.py \
  | sed -n '1,260p'

printf '\nAttentionComparisonInputs fields and constructors:\n'
rg -n -C 8 'class AttentionComparisonInputs|rope_output_dtype|rope_cast_at|q: torch.Tensor|k: torch.Tensor' rl_engine/testing/attention_comparison.py tests/test_attention_comparison.py | sed -n '1,240p'

Repository: RL-Align/RL-Kernel

Length of output: 37149


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from dataclasses import dataclass
from typing import Any, Literal

RoPEState = Literal["pre_rope", "post_rope"]

`@dataclass`(frozen=True)
class TensorLike:
    dtype: Any
    device: Any

`@dataclass`(frozen=True)
class DecodeAttentionInputs:
    q: TensorLike
    k_cache: TensorLike
    v_cache: TensorLike
    rope_output_dtype: Any = None

def _decode_rope_output_dtype(inputs):
    return inputs.q.dtype if inputs.rope_output_dtype is None else inputs.rope_output_dtype

for case in (
    ("matching bfloat16", "bfloat16", "bfloat16"),
    ("mixed k_cache bfloat16", "float32", "bfloat16"),
    ("mixed k_cache float16", "float32", "float16"),
):
    name, q_dtype, k_dtype = case
    inputs = DecodeAttentionInputs(
        q=TensorLike(q_dtype, "cpu"),
        k_cache=TensorLike(k_dtype, "cpu"),
        v_cache=TensorLike(k_dtype, "cpu"),
    )
    print(name, "q_dtype=", q_dtype, "k_cache_dtype=", k_dtype, "_decode_rope_output_dtype=", _decode_rope_output_dtype(inputs))
PY

Repository: RL-Align/RL-Kernel

Length of output: 448


Use the K cache dtype for pre-RoPE K casting.

_decode_rope_output_dtype currently returns inputs.q.dtype, so a pre-RoPE K cache is cast to the query dtype before rotation. Add a separate K cache dtype default for _decode_rope_output_dtype, validate q.dtype == k_cache.dtype in _validate_decode_inputs, or split query/K output dtype semantics so the K path does not mix dtypes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rl_engine/testing/attention_comparison.py` around lines 714 - 724, The
pre-RoPE K path currently casts using the query dtype from
_decode_rope_output_dtype. Update the dtype handling around
_decode_rope_output_dtype and _validate_decode_inputs so K uses the K-cache
dtype, either by adding a K-cache dtype default or validating q.dtype ==
k_cache.dtype; ensure the pre-RoPE Q path continues using the query dtype and
the pre-RoPE K path preserves the cache dtype.

Comment on lines +383 to +385
report = compare_decode_kv_replay(inputs)
assert report.drifts[0].out.max_abs <= 1.0e-6
assert report.drifts[0].lse.max_abs <= 1.0e-6

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The bfloat16 output tolerance is below the bfloat16 resolution.

output_dtype is torch.bfloat16, so report.drifts[0].out compares two bfloat16 tensors. bfloat16 keeps 8 mantissa bits, so neighbouring values near 1.0 differ by about 7.8e-3. The reference downcasts each per-query result, while the replay accumulates in fp32 and downcasts once. The two paths can round to different bfloat16 values. The 1.0e-6 bound then requires bit-identical output and passes only for this seed. Use a bfloat16-scale bound for out. The lse bound can stay tight because both paths keep LSE in fp32.

💚 Proposed tolerance fix
     report = compare_decode_kv_replay(inputs)
-    assert report.drifts[0].out.max_abs <= 1.0e-6
+    assert report.drifts[0].out.max_abs <= 1.0e-2
     assert report.drifts[0].lse.max_abs <= 1.0e-6
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
report = compare_decode_kv_replay(inputs)
assert report.drifts[0].out.max_abs <= 1.0e-6
assert report.drifts[0].lse.max_abs <= 1.0e-6
report = compare_decode_kv_replay(inputs)
assert report.drifts[0].out.max_abs <= 1.0e-2
assert report.drifts[0].lse.max_abs <= 1.0e-6
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_attention_comparison.py` around lines 383 - 385, Update the output
drift assertion in compare_decode_kv_replay to use a bfloat16-scale tolerance
rather than 1.0e-6, since report.drifts[0].out compares bfloat16 results that
may round differently. Keep the existing tight 1.0e-6 tolerance for
report.drifts[0].lse.

).to(output_dtype)
if metadata.k_cache_rope_state == "pre_rope":
key_positions = metadata.key_position_ids[batch_index, slot_index].unsqueeze(0)
k = rope.forward_fp32(k, key_positions, theta=inputs.rope_theta).to(output_dtype)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This casts pre-RoPE k_cache with the Q RoPE output dtype. If rollout stores KV in a different dtype from Q, the replay no longer checks the real cache boundary. Can we keep Q/K RoPE output dtypes separate or validate they are intentionally the same?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot. I have fixed in 2eb4362. I split the decode RoPE dtype contract into q_rope_output_dtype and k_cache_rope_output_dtype. They default independently to q.dtype and k_cache.dtype, respectively, so a pre-RoPE cached K no longer inherits the query dtype. I also added mixed FP32-Q/BF16-KV pre/post-RoPE equivalence coverage and provenance checks.

raise ValueError("q_rope_state must be 'pre_rope' or 'post_rope'")
if metadata.k_cache_rope_state not in {"pre_rope", "post_rope"}:
raise ValueError("k_cache_rope_state must be 'pre_rope' or 'post_rope'")
if metadata.prefix_cache_enabled and not metadata.prefix_cache_key:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefix cache validation only checks that a key exists. It does not verify that the reused prefix actually has the same token positions/pages/content identity, so a bad shared prefix can pass the harness. Can we add a real prefix identity check or narrow the doc/contract?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also fixed in 2eb4362. Prefix-cache validation now requires prefix_length and prefix_cache_fingerprint, in addition to the cache key. The harness recomputes a physical-layout-invariant SHA-256 fingerprint over the logical prefix positions and cached K/V content, including the cached-K RoPE state and tensor dtypes. A stale or mismatched prefix now fails before replay. Tests cover equivalent physical page layouts and modified prefix content.


_validate_decode_inputs(inputs)
reference = run_decode_full_prefill_reference(inputs)
candidate = run_decode_kv_replay(inputs, merge_backend=merge_backend)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The decode TE path is not covered like the PR2 TE path. Please add a decode fake-TE test that proves the same sorted partial states go through the TE merge helpers and that unavailable TE is reported/handled consistently.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 2eb4362 as well. compare_decode_kv_replay now always runs the RL-Kernel reference candidate and optionally runs the capability-probed TE merge oracle through include_transformer_engine=True. I added fake-TE coverage verifying that sorted logical partial states pass through the TE correction helpers and match RL-Kernel, plus an unavailable-TE test confirming that the core result is preserved and the reason is recorded in report.unavailable.

Signed-off-by: JLiu4Coding <lzwgre@126.com>
@JLiu4Coding

Copy link
Copy Markdown
Author

Hi @inaniloquentee, thank you for the detailed review. I’ve addressed all three requested changes in 2eb4362:
separated Q and cached-K RoPE output dtype contracts;
added real prefix-cache identity validation over logical positions and cached K/V content;
added decode-path Transformer Engine success and unavailable-path tests.
I also addressed the related review findings concerning nested merge-order provenance, vectorized cache-slot reconstruction, repeated validation, BF16-aware tolerance, and documentation of the current RoPE restrictions.
Local validation passes: 21 focused comparison tests, 38 combined attention/dispatch/KV-cache tests, 24 existing attention tests, pre-commit, mypy, and strict documentation build. GPU-only correctness tests remain skipped locally because CUDA/ROCm is unavailable.
Could you please re-review the latest commit when convenient? Thank you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants