gtest usage guide (operator candidate vs gold)¶
Audience: contributors implementing train–inference / batch-invariant operators Entry point:
scripts/check_operator.py+rl_engine/kernels/gtest/*Numerical SSOT: #267 four-judgment contract
This is the official how-to for the gtest harness: register an op, build inputs, run the CLI for forward/backward checks, and obtain tolerances from the shared contract (not private atol/rtol).
1. What gtest is for¶
gtest validates a single operator:
| Capability | Meaning |
|---|---|
| Gold | Usually a PyTorch / forward_fp32 reference path |
| Candidate | CUDA / Triton / arch-specific implementation |
| Forward check | Outputs within contract tolerance (forward_accuracy) |
| Backward check | Selected input gradients within contract tolerance (gradient_accuracy, independent of forward) |
It is not:
- The full Qwen3-8B model-level gate (#266 C9/C10)
- The final cross-config invariance harness (C3/C4 build on the same contract)
- Real vLLM vs Megatron engine alignment
The CLI primarily covers accuracy (candidate vs gold). Invariance (bitwise across configs) and train/infer aggregates use the contract APIs / later harnesses—do not invent private gate thresholds in tests.
2. End-to-end flow¶
1) (Optional) register the op in the runtime registry
↓
2) gtest/operator_specs.py → OP_SPECS: gold + candidates
↓
3) gtest/operator_inputs.py → build input shapes / values
↓
4) scripts/check_operator.py → run suite, load tolerance_contract.json
↓
5) report max_abs / tol / passed
2.1 Key files¶
| Path | Role |
|---|---|
rl_engine/kernels/gtest/operator_specs.py |
OP_SPECS: name, op_class, gold, candidates, grad inputs |
rl_engine/kernels/gtest/operator_inputs.py |
Default Qwen3-8B dims + make_operator_inputs |
rl_engine/kernels/gtest/op_checks.py |
Suite execution and comparison |
rl_engine/kernels/gtest/tolerance_contract.json |
Numerical contract SSOT |
rl_engine/kernels/gtest/tolerance.py |
load_contract / resolve_tolerance / chain aggregates |
scripts/check_operator.py |
CLI entry (accuracy) |
rl_engine/kernels/gtest/gradient_invariance.py |
C4 gradient invariance API |
rl_engine/kernels/gtest/gradient_adapters.py |
C4 enumerable adapters + status matrix |
rl_engine/kernels/gtest/elementwise_inventory.py |
C5 elementwise / RoPE inventory |
rl_engine/kernels/gtest/four_judgment_matrix.py |
C8 four-judgment matrix schema |
scripts/check_gradient_invariance.py |
C4 GPU evidence CLI |
rl_engine/kernels/gtest/kv_consistency.py |
C6/C7 decode–prefill + stateful KV |
rl_engine/alignment/qwen3_dense.py |
C9 full Qwen3-8B Dense BI model |
rl_engine/kernels/gtest/chain_gate.py |
C10 model-level #150 + train/infer gate |
scripts/check_decode_prefill.py |
C6 GPU CLI |
scripts/check_stateful_kv.py |
C7 GPU CLI |
scripts/ws1_chain_fwd_bwd.py |
C9 one-command fwd+bwd (assembly only) |
scripts/ws1_chain_gate.py |
C10/C11 full-model required gate |
3. Step 1: register the op in OP_SPECS¶
Edit rl_engine/kernels/gtest/operator_specs.py and add an entry to OP_SPECS. Example shape (logp / linear_logp):
"logp": OperatorSpec(
name="logp",
op_class="logprob", # selects the contract op_class row
gold_path="rl_engine.kernels.ops.pytorch.loss.logp.NativeLogpOp",
gold_method="forward_fp32", # method invoked on the gold instance
candidate_paths={
"pytorch": "rl_engine.kernels.ops.pytorch.loss.logp.NativeLogpOp",
"cuda": "rl_engine.kernels.ops.cuda.loss.logp.FusedLogpGenericOp",
"cuda-sm90": "rl_engine.kernels.ops.cuda.loss.logp.FusedLogpSM90Op",
},
grad_input_names=("logits",), # inputs compared under --check-grad
),
3.1 OperatorSpec fields¶
| Field | Meaning |
|---|---|
name |
Value for CLI --op |
op_class |
Contract class: elementwise / reduction / logprob / attention |
gold_path |
Gold class path module.Class |
gold_method |
Method name, e.g. forward_fp32, apply, __call__ |
candidate_paths |
Map candidate name → implementation class; CLI --candidate cuda looks up this map |
grad_input_names |
With --check-grad, enable grads and compare these inputs; missing config errors |
Only ops registered in OP_SPECS can be invoked via check_operator.py.
Currently registered (source of truth is the code):
rms_norm, qk_norm, attention, logp, linear_logp, embedding, lm_head,
det_gemm, rope, silu, swiglu, batch_invariant_logp, pack
qk_norm is a first-class OP_SPECS key that reuses the RMSNorm kernels on
head_dim (per-head), not the full hidden width.
pack is a WS1 layout helper, not a per-profile CUDA/Triton kernel. It is in
OP_SPECS so check_operator.py --op pack and the C3/C4 CPU adapters can
prove logical packing/unpacking. C8 marks every pack cell N/A with that
C2/C4 reason.
linear_logp is registered for the CLI but is not a WS1 required chain
node. C2 status is optional_fused_path; C4 is optional_fused; C8 does not
require a four-judgment row.
4. Step 2: build inputs¶
File: rl_engine/kernels/gtest/operator_inputs.py.
4.1 Default model dims (Qwen3-8B Dense semantics)¶
Macros at the top of the file (local experiments may change them; WS1 full-model EXIT uses the official config fingerprint):
DEFAULT_HIDDEN = 4096
DEFAULT_N_HEADS = 32
DEFAULT_N_KV_HEADS = 8
DEFAULT_HEAD_DIM = 128
DEFAULT_INTERMEDIATE = 12288
DEFAULT_VOCAB = 151936
DEFAULT_ROPE_THETA = 1.0e6
DEFAULT_RMS_EPS = 1.0e-6
4.2 Shape names and input builders¶
operator_shape_name(op_name, args)— human-readable case name (e.g.2x16x257)_make_*_inputs/make_operator_inputs— build the input dict from--opand CLI argsrandom: reproducible randomness (--seedplus per-tensor offsets)constant: fixed values for debugging (--constant-value/--token-value)
When adding an op: extend the shape map and implement the matching _make_xxx_inputs.
4.3 Suggested GRPO-oriented shapes (local sweeps)¶
For GRPO, B = P × G. With G=8, batch is often a multiple of 8.
B=1 is fine for smoke; fuller sweeps may use:
Prefer short S when VRAM is tight; full-model gates are owned by #266 / C2.
8. C6–C11 closeout commands¶
C6 (direct decode, both profiles; chunked-prefill is not a substitute):
python scripts/check_decode_prefill.py --backend-profile cuda_bf16
python scripts/check_decode_prefill.py --backend-profile triton_cuda_bf16
C7 (B1 stateful allocate→write→read→decode + generate-rescore). Concat-only
NativeKVCacheAttnOp is not B1. B2 is explicitly absent.
python scripts/check_stateful_kv.py --backend-profile cuda_bf16
python scripts/check_stateful_kv.py --backend-profile triton_cuda_bf16
C9 (assembly only; not EXIT). Official 36-layer Qwen3-8B Dense, pinned weights:
python scripts/prepare_ws1_weights.py --output "$QWEN3_8B" --verify-only
python scripts/ws1_chain_fwd_bwd.py --backend-profile cuda_bf16 --weights hf --weights-path $QWEN3_8B
python scripts/ws1_chain_fwd_bwd.py --backend-profile triton_cuda_bf16 --weights hf --weights-path $QWEN3_8B
C10/C11 required full-model gate (H20; no skip / xfail / synthetic-as-pass):
python scripts/ws1_chain_gate.py --backend-profile cuda_bf16 --model qwen3-8b-dense --dtype bfloat16 --weights required --weights-path $QWEN3_8B --json
python scripts/ws1_chain_gate.py --backend-profile triton_cuda_bf16 --model qwen3-8b-dense --dtype bfloat16 --weights required --weights-path $QWEN3_8B --json
Omitting --seed uses the manifest-pinned execution seed. A supplied seed is
applied to both PyTorch and CUDA and recorded separately from workload_seed.
The weight loader verifies the pinned index SHA-256, every shard size/SHA-256,
and the aggregate content hash before allocating the 8B model.
C10 compares tensor.grad after a real training-style backward over every
official Qwen3-8B Dense trainable leaf
(gradient_scope=all_required_trainable_parameters,
all_parameter_gradients=true). Logprob accuracy vs FP32 gold uses only
max_abs_dlogp / approx_kl0 / clipfrac0. The JSON also records GPU name,
representative case_ids, workflow URL, C8 evidence path, and backward
runtime kernel identities.
To keep the full-model gate within Hopper device memory, leaf-gradient snapshots are transferred to CPU without FP32 expansion and released after comparison. The The chain GPU job writes C8 outside the checkout; the artifact validator requires clean C8 evidence from the exact C10 commit and explicit packed-versus-FP32 forward and gradient accuracy rows.
5. Step 3: run the CLI¶
# From the repo root; prefer an editable install: pip install -e .
python scripts/check_operator.py --op logp --candidate pytorch --device cpu --dtype fp32 --batch 1 --seq 2 --vocab 17
5.1 Common examples¶
Smoke (CPU / PyTorch self-check)
python scripts/check_operator.py \
--op logp --candidate pytorch --device cpu --dtype fp32 \
--batch 1 --seq 2 --vocab 17
Triton linear_logp + backward (BF16)
python scripts/check_operator.py \
--op linear_logp --candidate triton --device cuda --dtype bf16 \
--batch 1 --seq 2 --vocab 1024 --normalized-dim 4096 \
--check-grad
CUDA deterministic attention + gradients
python scripts/check_operator.py \
--op attention --candidate cuda --device cuda --dtype bf16 \
--batch 2 --seq 64 --check-grad --grad-mode random
Full JSON report
5.2 CLI flags¶
| Flag | Meaning |
|---|---|
--op |
Operator name from OP_SPECS |
--candidate |
Backend: pytorch / cuda / cuda-generic / cuda-sm90 / triton / … (see that op’s candidate_paths) |
--dtype |
fp32 / bf16 / fp16; selects input dtype and contract row |
--device |
auto / cpu / cuda |
--batch / --seq |
Batch size and sequence length for inputs |
--vocab |
Vocab size; logp logits [B,S,V]; linear_logp weight [V,H] |
--input-mode |
random (default) or constant |
--constant-value |
Float fill in constant mode |
--token-value |
Token id in constant mode |
--normalized-dim |
Hidden dim for rms_norm / linear_logp, etc. |
--k-dim / --n-dim |
Matmul / det_gemm dims |
--theta |
RoPE theta |
--eps |
RMSNorm epsilon |
--seed |
Input RNG seed (per-tensor offsets still apply) |
--arch-key |
Arch override key, e.g. sm90 (contract arch_overrides) |
--check-grad |
Also compare gradients (requires grad_input_names) |
--grad-mode |
random (default, stricter) / ones (≈ output.sum().backward()) |
--grad-seed |
Seed for random upstream gradients |
--json |
Print the full structured report |
6. Where tolerances come from (after #267)¶
6.1 Before vs after C1¶
| Before | After (C1 / #267) |
|---|---|
Mostly accuracy[op_class][dtype] |
Four judgments: forward/gradient × accuracy/invariance |
| Forward and grad often shared one tol | Grad uses gradient_accuracy only (no silent forward inheritance) |
| Flat threshold table | Plus dtype policy, comparison roles, chain logprob aggregates |
6.2 Which judgments the CLI / op_checks use¶
run_operator_suite / check_operator.py:
| Comparison | Judgment |
|---|---|
| Output vs gold | forward_accuracy |
| Gradient vs gold | gradient_accuracy |
Batch/chunk bitwise invariance and train/infer three aggregates are not separate check_operator.py switches. Use C3/C4. C3 now runs the same enumerable WS1 ops as C4 (make_forward_runner):
from rl_engine.kernels.gtest import (
assert_forward_batch_invariant,
assert_gradient_batch_invariant,
)
from rl_engine.kernels.gtest.gradient_adapters import get_adapter
# C4: training-style gradient accuracy + invariance (thresholds from C1 only)
adapter = get_adapter("rms_norm")
report = assert_gradient_batch_invariant(
op,
contract=contract,
backend_profile="cuda_bf16",
provenance=provenance,
gold_fn=gold_fn,
grad_tensors=adapter.tensors,
op_class=adapter.op_class,
)
max_abs_dlogp, approx_kl0, and clipfrac0 are the sole chain-level
logprob / ablation aggregates. The three aggregates judge outputs only.
Gradient pass/fail uses only independent gradient_accuracy /
gradient_invariance verdicts. GPU evidence:
python scripts/check_gradient_invariance.py \
--op rms_norm --candidate cuda --backend-profile cuda_bf16
C4 does not claim the full-model C10 gate. Use:
from rl_engine.kernels.gtest.tolerance import (
load_contract,
resolve_tolerance,
compute_logprob_aggregates,
judge_logprob_aggregates,
default_clip_interval,
)
contract = load_contract()
# Cross-config invariance (gate path)
inv = resolve_tolerance(
contract,
judgment="forward_invariance", # or gradient_invariance
op_class="attention",
dtype="bfloat16",
backend_profile="cuda_bf16",
)
# inv.mode == "bitwise", inv.atol == inv.rtol == 0
# Train vs infer selected-logprob
agg = compute_logprob_aggregates(
train_logp,
rollout_logp,
active_mask,
contract=contract,
report_kind="train_infer_logprob_parity",
clip_interval=default_clip_interval(contract),
comparison_lhs_role="training_style_teacher_forcing",
comparison_rhs_role="inference_style_rollout_decode",
)
verdict = judge_logprob_aggregates(agg, contract, execution_dtype="bfloat16")
6.3 Policy locks (WS1)¶
| Item | Value |
|---|---|
| Execution | BF16 mandatory for EXIT (CLI may still exercise fp32/fp16) |
| Reference / accumulation | FP32 |
| FP8 | Out of scope (resolve hard-fails) |
| TF32 | Disabled |
| Profiles | cuda_bf16 and triton_cuda_bf16 share the same thresholds |
WS1 evidence must attach checked provenance to its candidate report:
from rl_engine.kernels.gtest import BackendProvenance, CandidateSpec
provenance = BackendProvenance(
backend_profile="cuda_bf16", # use triton_cuda_bf16 + triton for Triton
requested_backend="cuda",
actual_backend="cuda",
execution_dtype="bfloat16",
accumulation_dtype="float32",
output_dtype="bfloat16",
reference_dtype="float32",
candidate_tf32_enabled=False,
reference_tf32_enabled=False,
)
candidate = CandidateSpec(
name="cuda-candidate",
backend="cuda",
fn=op,
provenance=provenance,
)
The suite rejects backend fallback, dtype drift, TF32 enablement, and observed output dtypes that disagree with this provenance before producing a passing report.
check_operator.py is a local debugging CLI and does not construct provenance on its
own. A WS1 gate must create CandidateSpec(..., provenance=provenance) in its harness;
use --json to retain the resolved judgment and comparison-role fields in CLI reports.
Do not use private atol=1e-5 (etc.) as WS1 gate evidence. Migrate a gate to
the shared resolver before using it as WS1 evidence.
6.4 Report tol=(atol=..., rtol=...)¶
The CLI summary line:
comes from the shared resolver—not hard-coded constants inside check_operator.py.
7. Recommended local test order¶
1. --candidate pytorch --device cpu --dtype fp32
→ registration / inputs / plumbing smoke
2. Same shape with --dtype bf16 --device cuda --candidate triton|cuda
→ real candidate forward
3. Add --check-grad --grad-mode random
→ gradients (random upstream grads catch more bugs than ones)
4. --arch-key sm90 only when you need arch-specific contract overrides
5. Cross batch/layout: C3 `check_forward_invariance.py` / C4 `check_gradient_invariance.py`
8. Common failures¶
| Symptom | Likely cause |
|---|---|
Unsupported / missing --op choice |
Not registered in OP_SPECS |
--check-grad missing grad inputs |
Empty/wrong grad_input_names vs input keys |
| Candidate import error | Bad candidate_paths or extension not built |
| BF16 over tolerance | Confirm gold is forward_fp32; check contract row; do not loosen private atol |
| Missing SM90 symbols | Build without SM90 / non-sm90 GPU; pick another candidate or rebuild |
| Want FP8 | Hard-fail under WS1 contract; out of scope |
9. Relationship to pytest¶
| Path | Use |
|---|---|
python scripts/check_operator.py ... |
Fast single-op shape/debug loops |
pytest tests/test_*.py |
Regression, invariance, integration |
pytest tests/test_tolerance_contract.py |
Contract schema / resolver |
Both paths should take thresholds from tolerance_contract.json.
New pytest code should call resolve_tolerance instead of copying magic numbers.
10. Minimal checklist for a new operator¶
- [ ] Implementation under
rl_engine/kernels/ops/{pytorch,cuda,triton}/... - [ ] (Optional) runtime
registryregistration - [ ]
OP_SPECSentry: gold + candidates +op_class+grad_input_names - [ ]
operator_inputsshape name + input builder - [ ]
check_operator.pysmoke + bf16 +--check-gradgreen - [ ] Contract already has the
op_classrow (extend schema +test_tolerance_contractif not) - [ ] No new private
atol/rtolas gate evidence - [ ] Operator docs point at the contract for thresholds (do not restate ad-hoc numbers)
11. Further reading¶
| Doc | Content |
|---|---|
| testing.md | Short testing entry points |
| Issues #266 / #267 | WS1 closeout and C1 contract |
12. Changelog¶
| Date | Notes |
|---|---|
| 2026-08-11 | Initial English guide aligned with C1; documents CLI, OP_SPECS, inputs, and contract usage |
| 2026-08-13 | Document C4 assert_gradient_batch_invariant and check_gradient_invariance.py |
| 2026-08-13 | C4 adapters run on config.physical_layout (packed / chunked / padded / permuted) and return physical tensors restored through the C2 map; a new adapter must vary with the layout or its bitwise verdicts are tautologies |
| 2026-08-13 | C3 check_forward_invariance.py / make_forward_runner cover every C2 required chain op plus pack, not only logp |
| 2026-08-13 | C5 inventory + C8 sweep_ws1_four_judgments.py; C2 v5 adds remaining operator case_ids |