WS1 C4 (#270) Landing Plan — Gradient-invariance harness & adapters¶
Parent: #266 · Issue: #270 · Depends on: C1 (#267), C2 (#268)
Branch: feat/ws1-c4-gradient-invariance-270 (from feat/ws1-c3-forward-invariance-269)
C4 is a hard prerequisite of C10. C4 green alone is not WS1 EXIT.
1. Goal¶
Give every differentiable WS1 op — and later the full chain — one training-style gradient comparison semantic under the C2 Batch/Chunk matrix, so tests do not invent their own upstream grads, loss reduction, or active-token denominator.
2. Locks from #270 and #266¶
| Item | Lock |
|---|---|
| API | assert_gradient_batch_invariant(op, configs, contract) -> GradientInvarianceReport |
| Judgments | gradient_accuracy (vs FP32 VJP) and gradient_invariance (cross-config) are separate; no silent forward inheritance; no private atol/rtol |
| Batch/Chunk invariance | bitwise after logical aggregation (atol=0, rtol=0) |
| Accuracy | only FP32-reference gradient_accuracy rows from C1 |
| Logical identity | C2 (sample_id, token_position); compare only after restore |
| B1 vs BN | same sample/token multiset; N× B=1 singleton_aggregate in fixed sample order vs one B=N |
| Shared across configs | same upstream grad (keyed by logical identity), same loss_reduction, same global active_token_count_across_all_samples |
| Naming | singleton_aggregate is a C2 execution mode only — never a C1 comparison_*_role |
| Profiles | cuda_bf16 and triton_cuda_bf16 are independent; missing required Triton bwd is red, not N/A or fallback; neither profile may borrow the other |
| Adapters | real registered adapters (GRADIENT_ADAPTERS / OP_SPECS); name-only mention in a chain report does not count |
| Pack / KV | adapter required only if declared supported and differentiable |
| Defects | do not reopen #145–#151 / #153; open a Blocker if a sweep finds an untracked red |
| Out of C4 | full-model e2e (C9/C10), KV path (C6/C7), four-judgment evidence matrix (C8), CI gates (C11), new kernels |
C2 already pins Triton embedding / lm_head / plain logp as
missing_required. C4 must surface those as tracked red. It must not
implement the missing kernels and must not treat them as skip/N/A.
3. Deliverables¶
| Path | Role |
|---|---|
rl_engine/kernels/gtest/gradient_invariance.py |
Shared API, report schema, B1 aggregate, C1 thresholds |
rl_engine/kernels/gtest/gradient_adapters.py |
Enumerable adapters + stable grad names + status matrix + bwd audit list |
scripts/check_gradient_invariance.py |
One GPU command per required profile |
tests/test_gradient_invariance.py |
CPU contract tests (no GPU required) |
docs/design/ws1-c4-270-gradient-plan.md |
This plan |
docs/contributing/gtest-usage.md |
Point C4 at the shared API (no private thresholds) |
Reuse, do not fork: C1 resolver, C2 workload / build_config_matrix, C3
ConfigSpec / comparison helpers / provenance checks.
4. API and report¶
assert_gradient_batch_invariant(
op, configs=None, contract=None, *,
grad_tensors, backend_profile, provenance, gold_fn, ...
) -> GradientInvarianceReport
op(config, **op_kwargs) returns either:
{grad_name: token_map | parameter_tensor}GradientObservation(grads=..., actual_backend, kernel_id, output_dtype, device)
Token maps are { (sample_id, token_position): Tensor } or a physical tensor
that the harness restores via C2. Parameter tensors are compared after the
singleton aggregate described below.
GradientInvarianceReport (C10 must reuse this schema):
accuracy_reports— judgmentgradient_accuracyinvariance_reports— token VJPs and non-singleton parameter gradssingleton_aggregate_reports— N× B=1 parameter grads vs BN- provenance / candidate / device / CC / seed / fallback
loss_reduction,active_token_denominator,grad_tensor_namesfirst_failing_op,first_failing_tensor,first_failing_config_pairpassedrequires accuracy + invariance + aggregate + provenance + metadata
Logprob aggregates (max_abs_dlogp / approx_kl0 / clipfrac0) judge
outputs only. They do not appear in this report.
5. Training-style VJP (fixed across configs)¶
From the C2 manifest:
loss_reduction = sum_over_active_tokens_then_optional_mean_by_active_count- denominator =
active_token_count_across_all_samplesof the full BN logical batch (not the local B=1 count) - upstream
g[sample_id, token_position, ...]is a pure function of logical identity (no layout-order RNG) - inactive / pad tokens contribute 0
Then sum_i ∇_θ L(B=1 sample i) equals ∇_θ L(B=N) for a batch-invariant op.
Each B=1 dweight is not compared to BN dweight by itself.
6. Required adapters (stable names)¶
| Op | Grad names | Kind |
|---|---|---|
rms_norm / qk_norm |
dx, dweight |
token, parameter |
det_gemm |
dX, dW |
token, parameter |
attention |
dQ, dK, dV |
token |
embedding |
dweight |
parameter |
lm_head |
dhidden, dweight |
token, parameter |
logp / batch_invariant_logp |
dlogits |
token |
linear_logp |
dhidden, dW |
token, parameter (optional fused path) |
rope / silu |
dx |
token |
swiglu |
dgate, dup |
token |
pack |
dx |
token (packing is C2 supported and differentiable) |
kv_cache_attention |
— | absent_not_required (not declared supported+differentiable on the C2 training path; C6/C7 own KV) |
Attention / RoPE adapters must not mix samples into one flattened sequence. They materialize per-sample (or padded) logical rows so Batch/Chunk compares the same token multiset.
7. Status matrix and Blocker rule¶
For each (backend_profile, adapter):
| C2 / capability | C4 status |
|---|---|
declared + adapter + matching family candidate |
runnable |
missing_required (Triton embedding / lm_head / plain logp) |
tracked red |
| required + no adapter, or declared + borrowed other profile | untracked red → C4 fails; open Blocker, do not reopen closed op issues |
| pack supported + differentiable | adapter required; not a C2 profile node |
| KV not declared supported | absent_not_required |
C4 unit tests fail on any untracked red. Tracked C2 missing_required
rows stay visible and keep the CLI red if someone tries to run them.
8. Bwd contract audit¶
Every BI candidate adapter lists its source files. Tests forbid atomicAdd
and record shape_dependent_bwd_accum=forbidden. This is an audit of
declared candidates, not a kernel rewrite.
9. Test plan (tests/test_gradient_invariance.py)¶
CPU-only, synthetic ops plus one real PyTorch rms_norm adapter:
- accuracy vs invariance use different C1 judgments
- invariance is bitwise; accuracy uses
gradient_accuracy - B1/BN share sample set, upstream identity, global denominator, fixed order
- parameter grads pass only after singleton aggregate
- missing active token / missing gold_fn hard-fail
- provenance + cross-profile fallback fail closed
- both profiles share the report schema
- every required differentiable op is enumerable with stable names
- status matrix: tracked red vs untracked red; pack present; KV absent
- CUDA and Triton declared candidates are distinct paths
- no
atomicAddin listed BI candidate sources - no private thresholds; no
singleton_aggregatecomparison role
10. GPU evidence command (not EXIT)¶
python scripts/check_gradient_invariance.py \
--op rms_norm --candidate cuda \
--backend-profile cuda_bf16 --json
python scripts/check_gradient_invariance.py \
--op rms_norm --candidate triton \
--backend-profile triton_cuda_bf16 --json
CUDA unavailable, missing candidate, missing_required, SM90-on-non-SM90,
or provenance mismatch → exit red. This is C4 harness evidence, not C8/C10.
11. Explicit non-claims¶
C4 does not claim: full Qwen3-8B model, #150 matrix on the full model, stateful KV / generate-rescore, C8 four-judgment greens, or WS1 EXIT.