Skip to content

fix: teacher-force the offset loss in JointPolicyObjective#258

Open
felixmaximilian wants to merge 9 commits into
feat/action-tokenizerfrom
fix/joint-policy-offset-teacher-forcing
Open

fix: teacher-force the offset loss in JointPolicyObjective#258
felixmaximilian wants to merge 9 commits into
feat/action-tokenizerfrom
fix/joint-policy-offset-teacher-forcing

Conversation

@felixmaximilian

Copy link
Copy Markdown
Contributor

Problem

JointPolicyObjective supervises the offset head at codes sampled from the policy's own logits (sample_codes=True during training). A sampled code is independent of the action it is graded against, so every code's offset entry trains on the full conditional action distribution, and under L1 its optimum is the per-dimension conditional median — for every code. The offset table converges to a negative copy of the codebook (measured: regression slope −0.97, corr −0.99 against the decodes) and cancels the code selection entirely (cancellation ratio R ≈ 0.02–0.05 on model-2gqxhjod:v9, i.e. offsets erase 95–99% of cross-code output variance).

Consequences at argmax export: coordinate-wise medians of multimodal action distributions press gas and brake simultaneously in ambiguous states (5.1% of val frames vs 0% in GT on the same frames), and minority behaviors are emitted never, not rarely (majority-mode lock-in — e.g. launching from rest). Data cleaning cannot fix this; the mechanism reproduces identically on conflict-filtered tokenizers.

Fix

Teacher-force the offset loss: gather the offset at target_codes (already computed for the focal loss), so each code's entry only sees residuals of actions that actually quantized to it. Inference paths are unchanged — the offset table is still indexed by predicted codes at serving.

  • teacher_force_offset: bool = True keeps the pre-fix behavior reachable (=false) so the A/B is a config flip, wired through policy_finetune.yaml + the finetune experiment config
  • the pre-fix loss value is still logged as a gradient-free metric (metric/offset_sampled_recon) for train-curve comparability
  • optional offset_scale soft bound (scale·tanh(x/scale), default off): makes codebook-scale cancellation unrepresentable — defense in depth, kept out of the A/B
  • _predict split into _heads/_sample_codes/_gather_offset (behavior-preserving; bit-exact regression test vs the old implementation, seeded multinomial included)
  • diagnostics suite under src/rmind/scripts/: cancellation probe (1a+1b), pedal-conflict probe (1c, incl. activation rates + multi-threshold), residual-unimodality probe (1d), offset-head-only retrain, codebook conflict surface

Evidence (condensed)

check bugged teacher-forced
cancellation R (val, argmax) 0.02–0.05 0.94–0.99
offset table vs codebook slope −0.97 (negative copy) −0.11, offsets 6× smaller
conflict frames (strict thr) 16.7% (A/B ep-0) 5.9% (≈ decode-noise floor)
code focal losses identical to 3 decimals

Offset-head-only retrain of the bugged checkpoint repairs it in place (R 0.02 → 0.79–0.87) while a sampled-code control stays collapsed. Full 10-epoch A/B in flight on tokenizer model-y74asdtd:v9: baseline rpiav4ay vs fix zcglgth4 (identical seed/data/config, only the flag differs).

Deep dive: running report (Notion) · interactive explainer — live simulator + real codebook maps · full evidence page — toy A/B + checkpoint probes

Tests

tests/test_joint_policy.py — 15 passed: gather correctness (hand-computed), bit-exact _predict regression vs the pre-refactor implementation, gradient routing (offset loss → offset_head only; focal → code_head only; tokenizer frozen), teacher_force_offset=false parity with the old loss, metric detachment, offset_scale bound/identity.

🤖 Generated with Claude Code

felixmaximilian and others added 8 commits July 10, 2026 23:53
…set/_sample_codes

Behavior-preserving: forward/predict outputs are byte-identical (regression
test against the pre-refactor implementation lands with the fix commit).
Exposes the full (b, g, c, action_dim) offset table for diagnostics.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The offset head was supervised at codes sampled from the policy's own
logits (sample_codes=True during training). Given the features, a sampled
code is independent of the GT action, so under L1 the offset table's
optimum is offset[c] -> median(action|state) - decode(c) for every c: it
cancels the code selection and pulls all codes toward the per-dimension
conditional median, producing simultaneous gas+brake at argmax decoding
in bimodal states.

- teacher_force_offset: bool = True gathers the offset at target_codes so
  each entry only sees residuals of actions that quantized to it; false
  reproduces the previous behavior (A/B is a config flip via
  config/experiment/yaak/control_transformer/finetune.yaml)
- the sampled-code reconstruction error (the old loss value) is logged as
  a gradient-free metric (metric/offset_sampled_recon) for curve
  comparability across the A/B
- tests: gather correctness, exact regression vs the pre-refactor
  _predict (seeded multinomial included), gradient routing (offset loss
  -> offset_head only; focal -> code_head only; tokenizer frozen),
  teacher_force_offset=false parity with the old loss

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lt off)

Defense-in-depth for the offset head: when offset_scale is set, the
gathered offset becomes scale * tanh(x / scale) - identity for
|x| << scale (sub-cell residuals pass through unchanged), saturating at
+-scale (cross-cell excursions capped). Off by default so the
teacher-forcing A/B measures the fix in isolation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- offset_diag.py: shared harness (checkpoint loading with export-time
  argmax decoding, hydra-composed 3hz dataloaders, per-batch tensor
  iterator exposing features/code_logits/offset table/GT codes)
- offset_cancellation_probe.py (1a+1b): cancellation ratio
  R = Var_c(decoded+offset)/Var_c(decoded) by entropy quartile, and the
  sampled-vs-GT contamination audit
- pedal_conflict_probe.py (1c): model-side predicted gas+brake conflict
  rate at the sensor-data thresholds, by entropy quartile, vs GT floor
- residual_unimodality_probe.py (1d): within-code residual histograms +
  tail-mass flag on the action tokenizer
- pedal_conflict_stats.py: point CONFIG_ROOT at this checkout (was a
  hardcoded rmind-main sibling) and print the previously-computed-but-
  dropped conflict totals

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…heckpoint

Freezes everything except offset_head and retrains it from the bugged
checkpoint's weights under both supervision variants on cached features,
then evaluates cancellation ratio, pedal-conflict rate, and offset
magnitudes side by side. Fast (hours) real-data evidence that teacher
forcing alone decontaminates the offset table while continued
sampled-code training does not.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
surface: decode all 65,536 combos per tokenizer, report conflict fraction
(unweighted + real-usage-weighted at sensor and 0.02 thresholds), GT-decode
rate, policy argmax hit rate, and usage concentration. Key finding: the
conflict-filtered tokenizers have the same decode surface as the dirty one
(42-47% of combos at 0.02) - most combos are off-manifold, so training-data
filtering cannot constrain them.

tier2b: co-retrain code+offset heads on cached frozen features against a
clean codebook, teacher-forced vs sampled; the cancellation mechanism
reproduces identically (R 0.86-0.95 vs 0.01), so the bug is
codebook-independent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…probe

Collect raw pred/GT pedal values so one pass reports, per side: gas/brake
activation rates at sensor/0.02/0.05 thresholds (overall and by entropy
bucket) and conflict rates at all three thresholds. A median-collapsed
offset head suppresses minority-mode activations, so a predicted
gas-activation deficit vs GT is the majority-mode lock-in signature the
A/B should show on the bug arm and not the fix arm.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- uv format in CI floats to the newest ruff (0.15.21), which reformats
  inference/backends/tensorrt.py (whitespace-only) — apply it
- repo-wide 'ruff check' auto-fixes (import order in train.py, logger
  callbacks, tensorrt.py) plus targeted noqas for the five remaining
  pre-existing violations (__init__.py eval resolver, vq.py acronym
  import / arg count / private codebook access)

No functional changes; all files outside the fix's scope were only
touched to keep 'just check' green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
felixmaximilian added a commit that referenced this pull request Jul 12, 2026
Required by the PR #258 hparams_jq interpolation; true = the fixed
teacher-forced offset loss, matching the finetune experiment default.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Under argmax any mode-committing policy shows a gas-activation deficit vs
GT in ambiguous states, so argmax activation rates cannot separate
majority-mode lock-in (bug) from legitimate mode commitment. Sampled
decoding is the discriminator: a healthy multimodal head draws modes in
proportion and matches GT activation rates; a median-collapsed head
cannot, regardless of decoding.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

1 participant