Skip to content

test(kernels): reduce the duplicated CPU operator oracles in the HD256/HD512 QK-RoPE smokes - #1002

Open
BreezyB1n wants to merge 3 commits into
pegainfer-project:mainfrom
BreezyB1n:chore/qk-rope-smoke-oracle-reduction
Open

test(kernels): reduce the duplicated CPU operator oracles in the HD256/HD512 QK-RoPE smokes#1002
BreezyB1n wants to merge 3 commits into
pegainfer-project:mainfrom
BreezyB1n:chore/qk-rope-smoke-oracle-reduction

Conversation

@BreezyB1n

Copy link
Copy Markdown
Contributor

Description

Fixes #943

The HD256/HD512 QK-RoPE device gates carried seven closed-form tests over two independent host copies of the RMSNorm / RoPE / paged-address math (inv_rms, normed, expected_prep, expected_full, expected_pool, assert_pool). That second implementation has to track the .cu contract, and a specification mistake made once can be copied into both the kernel and its local oracle — leaving the gate green while both are wrong.

This keeps three closed-form anchors and derives every other expectation from a GPU arm an anchor already certifies:

  closed-form anchor (host)      the only retained RMSNorm/RoPE formulas
        | certifies
  flat kernel                    hd256: qk_norm_rope_prefill_hd256_plain
  (contiguous, no pool)          hd512: qk_norm_partial_rope_batched_decode_hd512
        | is the value oracle for
  paged prefill <false>          the only retained address derivation
        | is the bitwise oracle for
  paged decode <true>            zero host math, zero addressing

Each link catches a failure the link above it cannot: a wrong formula surfaces at the anchor (which tests the flat kernel directly); a wrong address surfaces as flat values landing in the wrong pool slot; wrong per-token routing surfaces as the two instantiations of one template disagreeing bit for bit.

No production code is touched — kernels, dispatch, and src/ are untouched; the only .cu edits in this branch are the temporary negative-control perturbations, reverted before every commit.

Why the line count goes up

hd256 783 → 882, hd512 951 → 1058. The metric the issue asks for is duplicated semantic code, and that is down; the line count is up because the metamorphic method the issue asks for runs two real kernel arms per gate, and these wrappers take 20–22 arguments, one per line under rustfmt. Roughly 31–37% of each new gate is bare argument lines. Each file is now at three kernel call sites — one per distinct kernel, which is the floor — after run_paged_prefill and device_fixture removed the duplicated arm and setup.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update

The template has no box for a test-semantics change, which is what this primarily is; Documentation update is checked for the new subsystem doc that ships with it.

Inventory: the seven closed-form tests

# File Test Independent failure it catches Disposition
1 hd256 full_rotation_matches_closed_form RMSNorm + RoPE at rotary_dim == HD: the Gemma 4 local-layer production config, where the tail is empty and no thread takes the d >= rotary_dim branch kept, byte-identical
2 hd256 partial_rotation_exercises_tail the same math at rotary_dim < HD: the only config that exercises the pass-through tail kept, byte-identical
3 hd256 pool_write_matches_closed_form_and_touches_nothing_else paged-prefill pool addressing + untouched-slot sentinels paged_prefill_lands_flat_values_at_layout_addresses
4 hd256 batched_decode_prep_matches_closed_form per-token routing: positions[], CSR window, per-token origin paged_decode_equals_paged_prefill_over_the_same_positions
5 hd512 decode_prep_matches_closed_form RMSNorm + partial RoPE; rotary_dim 128 < 512 puts rope-lo, rope-hi and the tail in one run, so a second hd512 anchor would restate the same three paths kept as the anchor
6 hd512 prefill_prep_matches_closed_form paged-prefill pool addressing + sentinels prefill_prep_lands_batched_values_at_layout_addresses
7 hd512 paged_decode_prep_matches_closed_form per-token routing paged_decode_equals_paged_prefill_over_the_same_positions

hd256 keeps two anchors rather than one because rotary_dim == HD and rotary_dim < HD are different control flow, not different data.

The nine non-closed-form tests (three refusal gates, four row-offset metamorphic gates, split_read_row_offset_serves_only_the_suffix, prefill_rejects_undersized_kv_pool) contain no operator math and are untouched.

Before / after

Metric Before After
Closed-form tests restating the operator 7 3
Host helpers reconstructing pool contents (expected_pool, assert_pool) 4 0
Call sites of the host operator reconstruction 24 12
Pool assertion precision 0.02 tolerance bitwise
Kernel call sites per file in the paged gates 4 3 (the floor)
Surviving functions byte-identical to main 32 / 34
Lines (hd256 + hd512) 1734 1940

The two surviving functions that are not byte-identical are expected_full (dropped a now-unused row_of closure parameter) and its only caller decode_prep_matches_closed_form.

Deleted defences and their successors

Per docs/conventions/migration-defense.md:

Old defence Failure mode it guarded Successor
expected_pool (×2) wrong value or wrong slot in the KV pool replaced — the flat/batched arm's K values, asserted at addresses read off the production PagedKvLayout, with exact-zero sentinels elsewhere
inline q_exp / pool reconstruction (×3) per-token routing errors replaced — bitwise prefill-vs-decode comparison; strictly tighter than the 0.02 tolerance it replaces
assert_pool's 0.02 tolerance value drift replacedassert_bits_eq, exact
origins = [1, 0] in test #4 per-token origin silently defaulted inherited — the per-token arm compresses each row's window to that row's own page, giving origins 0, 1, 1, 2 over four different table spans. Handing every row the full table at origin 0 would agree whether or not the metadata is read at all; control 2b below is what proves this is not vacuous.
closed-form RMSNorm/RoPE in tests #3, #4, #6, #7 operator formula errors inherited by the three retained anchors, which certify the very arms those gates now measure against

Verification

One NVIDIA A40 (sm_86), CUDA 12.8, driver 570.195.03.

PEGAINFER_REQUIRE_GPU=1 cargo test --release -p pegainfer-kernels \
  --test hd256_qk_rope_plain_smoke --test hd512_qk_rope_smoke

PEGAINFER_REQUIRE_GPU=1 is not optional: without it a missing device is a skip and the suite passes having executed nothing.

hd256_qk_rope_plain_smoke   8 passed; 0 failed
  rejects_bad_rotary_dim, partial_rotation_exercises_tail,
  decode_prep_row_offset_serves_only_the_suffix, full_rotation_matches_closed_form,
  paged_decode_equals_paged_prefill_over_the_same_positions,
  rejects_position_beyond_cos_table,
  paged_prefill_lands_flat_values_at_layout_addresses,
  prefill_prep_row_offset_serves_only_the_suffix

hd512_qk_rope_smoke         9 passed; 0 failed
  prefill_rejects_position_beyond_cos_table, prefill_rejects_undersized_kv_pool,
  decode_prep_matches_closed_form, rejects_bad_rotary_dim,
  decode_prep_row_offset_serves_only_the_suffix,
  prefill_prep_lands_batched_values_at_layout_addresses,
  paged_decode_equals_paged_prefill_over_the_same_positions,
  prefill_prep_row_offset_serves_only_the_suffix,
  split_read_row_offset_serves_only_the_suffix

cargo fmt --all --check clean; cargo clippy --release -p pegainfer-kernels --all-targets -- -D warnings exit 0.

Negative controls

A GPU-vs-GPU gate can pass because both arms are equally broken, so each retained gate was made to fail on purpose. All three perturbations were reverted afterwards (git status on csrc/ clean).

# Perturbation Result
1 + 1 on the kv_head * HD term in paged_kv_offset_hd256_plain / paged_kv_offset_hd512 both address gates FAILED — pool[59392]: got 0, expected 129 (hd256), pool[14848]: got 0, expected 65 (hd512)
2 page_origins[token]0 in the PER_TOKEN_META branch both decode gates FAILED — CUDA_ERROR_LAUNCH_FAILED at the first D2H
2b csr_page_row_checked(..., token, ...)..., 0, ... both decode gates FAILED — per-token pool writes vs the whole-window run[57344]: got -129, expected 0 (hd256), [14336]: got 1, expected 0 (hd512)

2b is the one that matters. With the origin ignored, the last row computes a row index past the end of its own window, so the kernel's own __trap() fires and the launch dies before anything is compared — red, but it is the device guard doing the work, not the assertion. Pinning every row to row 0's window stays in bounds, no guard fires, and only the bitwise pool comparison can catch it. A control that only ever trips a __trap() proves the kernel guards itself, not that the gate would notice a wrong-but-in-bounds page.

Control runs need --no-fail-fast: cargo test stops after the first failing binary, so without it hd512 never executes and the run looks exactly like a control that passed on both.

Notes for review

  • docs/subsystems/kernels/qk-rope-smoke-oracles.md records the chain, the exactness argument behind the bitwise assertions, and the negative controls with their observed output, so the next person to touch these gates does not have to re-derive why they are shaped this way.
  • pegainfer-gemma4/src/layer_oracle.rs is corroboration, not a successor. It replays the HF golden through the real implementation for both layer types, but it is #[ignore]d and needs the pinned 12B checkpoint, so it never runs by default, and it only ever sees the production rotary_dim. Retiring an anchor in its favour would move a routinely-runnable check onto one nobody executes.
  • No shared CPU QK/RoPE implementation was added. The formulas were not relocated; their call sites went from 24 to 12 and expected_pool is gone from both files. pool_k_offset deliberately stays duplicated per file rather than moving to tests/common/: paged-KV addressing is one of the three things the issue names, and relocating it would be the move the issue rules out.
  • assert_bits_eq / row_bits also stay per file. tests/common/mod.rs is compiled into ten test binaries and all ten use device_or_skip; helpers used by two of them would need #[allow(dead_code)] in a module shared by the other eight, which is worse than 15 duplicated lines.
  • Two host expressions survive outside the anchors, both one line, both the weightless V norm: hd256's V band reduces a separate v_batch input and hd512's V is the K=V fork, so neither flat kernel emits a V to compare against.

Checklist

  • My code follows the style guidelines of this project (see docs/conventions/coding-style.md).
  • I have performed a self-review of my own code.
  • I have formatted my commits according to Commitizen conventions.
  • I have run the local test suite and all tests pass (see CLAUDE.md).

The two QK-RoPE device gates carry seven closed-form tests backed by two
independent host copies of the RMSNorm/RoPE/paged-address math. That
second implementation has to track the .cu contract, and a specification
mistake made once can be copied into both the kernel and its local
oracle, leaving the gate green while both are wrong.

Record the discipline that replaces it: three closed-form anchors, and
every other expectation derived from a GPU arm an anchor already
certifies (anchor -> flat kernel -> paged prefill -> paged decode).

The rest of the doc is the part that is easy to get wrong when reworking
these gates, all of it measured on an A40 rather than argued:

- why the gemma4 layer oracle is corroboration and not a successor: it is
  #[ignore]d and needs the pinned 12B checkpoint, so it never runs by
  default (docs/conventions/migration-defense.md wants a successor that
  actually runs)
- why PEGAINFER_REQUIRE_GPU=1 is mandatory: without it a missing device
  is a skip and the suite passes having executed nothing
- why the pool assertions are bitwise, and the three conditions that make
  the one host-computed value exact
- three negative controls with their observed output, and why the
  in-bounds one (2b) is the one that proves the assertion is load-bearing
  rather than the kernel's own __trap() guard
- why the decode arm must keep distinct non-zero per-token origins, or it
  agrees with the prefill arm whether or not the metadata is read
- why control runs need --no-fail-fast: cargo stops after the first
  failing binary, so hd512 silently never runs

Refs pegainfer-project#943

Signed-off-by: B1n_FreeToRoAm <z2662728664@gmail.com>
The file carried three closed-form tests over a host copy of the operator
(inv_rms/normed/expected_prep/expected_full/expected_pool). Keep one
anchor and derive the rest from a GPU arm it certifies.

decode_prep_matches_closed_form stays as the anchor: rotary_dim 128 < 512
puts rope-lo, rope-hi and the pass-through tail in a single run, so a
second anchor would restate the same three paths.

prefill_prep_matches_closed_form becomes
prefill_prep_lands_batched_values_at_layout_addresses: the batched arm the
anchor certifies produces the Q and K at the same positions, and the test
asks only whether the paged form lands them at the addresses
PagedKvLayout names, with exact-zero sentinels everywhere else.

paged_decode_prep_matches_closed_form becomes
paged_decode_equals_paged_prefill_over_the_same_positions: both are
instantiations of one template, so equivalent metadata must give identical
bits. Everything shared cancels and only the per-token routing is left
under test. The per-token arm compresses each row's window to start at
that row's own page - what a caller that released the front passes - so
the rows carry distinct non-zero origins (0, 1, 1, 2) over four different
table spans. Handing every row the full table at origin 0 would agree
whether or not page_origins[token] is read at all; the replaced test used
origins [1, 0] for exactly this reason and that coverage is kept.

assert_pool/expected_pool give way to one assert_bits_eq that takes a
label and reports the first differing element. assert_eq! on the whole
slice dumps two vectors of tens of thousands of values, which makes a
real failure unreadable - control 2b below was invisible until this
landed.

The paged prefill arm is spelled once, in run_paged_prefill: both gates need
it - one as the subject, one as the oracle the decode form must reproduce -
and the wrapper takes twenty-plus arguments, so a second copy is a second
place to edit when the signature moves. device_fixture does the same for the
norm weights and RoPE tables. Each file is now down to three kernel call
sites, one per distinct kernel, which is the floor.

assert_bits_eq's label is just "pool"; the zero-means-untouched semantics
moved into the helper's doc comment, where it is not appended to every
unrelated failure message.

Successors for what was deleted:
- expected_pool -> the batched arm's K values, at layout addresses
- inline q_exp/pool reconstruction -> the bitwise prefill/decode gate
- 0.02 pool tolerance -> assert_bits_eq, exact
- per-token origin coverage -> the compressed windows described above

Refusal gates and all three row-offset metamorphic gates are byte-identical
to before; the anchor's only edit is dropping expected_full's now-unused
row_of closure.

Verified on one A40 (sm_86, CUDA 12.8, driver 570.211.01):
- PEGAINFER_REQUIRE_GPU=1 cargo test --release -p pegainfer-kernels
  --test hd512_qk_rope_smoke -> 9 passed; 0 failed
- control 1 (paged address +1) -> prefill_prep_lands_batched_values_at_
  layout_addresses FAILED at pool[14848]: got 0, expected 65
- control 2b (CSR window pinned to row 0) -> paged_decode_equals_paged_
  prefill FAILED at pool[14336]: got 1, expected 0
- cargo fmt --all --check and cargo clippy --release -p pegainfer-kernels
  --all-targets -- -D warnings both clean

Refs pegainfer-project#943

Signed-off-by: B1n_FreeToRoAm <z2662728664@gmail.com>
Same reduction as the hd512 sibling: four closed-form tests over a host
copy of the operator become two anchors plus two gates measured against a
GPU arm those anchors certify.

Two anchors rather than one because rotary_dim == HD and rotary_dim < HD
are different control flow, not different data: at full width no thread
reaches the pass-through tail at all, and full width is the Gemma 4
local-layer production config. full_rotation_matches_closed_form and
partial_rotation_exercises_tail both stay, byte-identical.

pool_write_matches_closed_form_and_touches_nothing_else becomes
paged_prefill_lands_flat_values_at_layout_addresses, measured against the
contiguous kernel the anchors certify.

batched_decode_prep_matches_closed_form becomes
paged_decode_equals_paged_prefill_over_the_same_positions - a bitwise
comparison of the two instantiations of one template, with each row's
window compressed to start at its own page so the rows carry distinct
non-zero origins (0, 1, 1, 2). That keeps what the replaced test proved
with origins [1, 0]: the per-token origin is read per row, not defaulted.

assert_pool/expected_pool give way to one assert_bits_eq that takes a
label and reports the first differing element, rather than dumping two
vectors of tens of thousands of values on failure.

The paged prefill arm is spelled once, in run_paged_prefill: both gates need
it - one as the subject, one as the oracle the decode form must reproduce -
and the wrapper takes twenty-plus arguments, so a second copy is a second
place to edit when the signature moves. device_fixture does the same for the
norm weights and RoPE tables. Each file is now down to three kernel call
sites, one per distinct kernel, which is the floor.

assert_bits_eq's label is just "pool"; the zero-means-untouched semantics
moved into the helper's doc comment, where it is not appended to every
unrelated failure message.

Successors for what was deleted:
- expected_pool -> the contiguous arm's K values, at layout addresses
- inline q_exp/pool reconstruction -> the bitwise prefill/decode gate
- 0.02 pool tolerance -> assert_bits_eq, exact
- per-token origin coverage -> the compressed windows described above

V keeps a one-line weightless norm: the contiguous kernel has no V band,
so there is no arm to borrow it from. Both refusal gates and both
row-offset metamorphic gates are byte-identical to before.

Verified on one A40 (sm_86, CUDA 12.8, driver 570.211.01):
- PEGAINFER_REQUIRE_GPU=1 cargo test --release -p pegainfer-kernels
  --test hd256_qk_rope_plain_smoke -> 8 passed; 0 failed
- control 1 (paged address +1) -> paged_prefill_lands_flat_values_at_
  layout_addresses FAILED at pool[59392]: got 0, expected 129
- control 2b (CSR window pinned to row 0) -> paged_decode_equals_paged_
  prefill FAILED at pool[57344]: got -129, expected 0
- cargo fmt --all --check and cargo clippy --release -p pegainfer-kernels
  --all-targets -- -D warnings both clean

Refs pegainfer-project#943

Signed-off-by: B1n_FreeToRoAm <z2662728664@gmail.com>
@xiaguan

xiaguan commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Please rebase this PR onto the current main: both QK-RoPE smoke files have substantive conflicts with changes that already removed several of the old closed-form tests. After resolving the conflicts, please update the before/after description for the new baseline and rerun the HD256/HD512 GPU gates on the rebased exact head.

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.

Reduce duplicated CPU operator oracles in the HD256/HD512 QK-RoPE smokes

2 participants