Skip to content

Add legal-move-renormalized + distribution-level hard negatives (FSQ label_dims) - #18

Closed
trashhalo wants to merge 11 commits into
mainfrom
feat/legal-move-negatives
Closed

trashhalo wants to merge 11 commits into
mainfrom
feat/legal-move-negatives

Conversation

@trashhalo

Copy link
Copy Markdown
Contributor

Summary

Three new/changed loss terms in the FSQ label_dims path, operating directly on the reserved-digit softmax (dim_lg) so a hard negative steers the move distribution, not the reconstruction matrix.

HardNegTerm — bugfix (the negative trained nothing on the move)

The FSQ path pushed on c.recon = head.encode(stop-grad EMA target) = a function of up_proj/down_proj only. The move lives in dim_lg = level_proj(hid) — a separate Linear with no shared graph edge. So loss_hard_neg.backward() filled up_proj/down_proj and left dim_lg/level_proj at exactly zero — "don't emit this blunder" could not change which move the model commits to at any weight. The continuous (non-FSQ) path is unchanged.

Reproduced deterministically in tests/test_hardneg_reaches_move.py: positive control (loss_dims) puts grad 1.75 on dim_lg; the old HardNegTerm put 0.00 on dim_lg (1105 on up_proj). Now routes the query through the predicted emission (soft-argmax + straight-through) so grad reaches dim_lg/level_proj.

MoveNegTerm — raw-codeword distribution penalty (building block)

Penalizes P(blunder) = Π softmax(dim_lg[digit])[code] directly. Empirically weak on its own — the raw codeword probability is ~0.002 (mass spread across 8 levels × several digits), ~60× too small to move the legal-move mass the decode metric reads. Kept as a building block (shares the codeword→index precompute) and documented as superseded.

LegalMoveNegTerm — the win ✅

Renormalizes over the per-row legal-move support set and penalizes the blunder's share = Σ_neg P(neg) / Σ_legal P(legal) — the same space the decode metric reads (~10–35% blunder share). The renormalized gradient is ~50× larger than MoveNegTerm's and actually reshapes mass.

Generic (no chess logic): both the neg-subset and legal-support codeword→index tensors are precomputed once in Trainer.__init__ (padded [N_rows, max_K, n_reserved] long + count masks). The step loop does one batched gather+prod per set — no Python row loop. Self-skips when off → byte-identical to before.

Validation

Tests (deterministic, CPU, tiny model):

  • test_hardneg_reaches_move — the fix reaches dim_lg (positive control guards the rig)
  • test_moveneg_reaches_mass — blunder digit-level grad push-down, non-blunder levels push-up, non-reserved + STOP untouched
  • test_legalneg_reaches_legal_mass — loss in the legal-renorm range [0.05, 0.95] (not the raw ~0.002), blunder grad ~6× MoveNegTerm, one SGD step lowers legal-renorm P(blunder) 0.85→0.03

Full 14-test suite green. golden_trainer_multi.npz regenerated (the HardNegTerm fix changed the FSQ math — intentional, documented flow); identity + sensitivity pass.

Empirical (chess, SmolLM2-135M full-FT, held-out shard_1, n=400):

The legal-move blunder mass drops monotonically with weight — the first negative form to move mass outside the noise floor (latent-cosine HardNegTerm and raw MoveNegTerm both left it flat):

weight P-mass-on-blunders argmax-blunder ≥250cp cp-regret mean
0.0 (control) 34.5% 34% 546
0.3 31.3% 27% 523
0.6 29.9% 24% 435
1.0 28.7% 24% 482

0.6 is the operating point (best cp-regret AND argmax-blunder); at 1.0 the argmax regresses as the term over-regularizes the base digit CE.

Breaking changes

None. All three terms self-skip when their weight ≤ 0 / field is None / dim_lg is absent (non-FSQ), so existing configs are byte-identical. The one behavior change is HardNegTerm on the FSQ path (it now actually trains the move) — that's the bugfix, and the golden reflects it.

Out of scope (not in this PR)

  • kv_cache rollout — already on main (separate prior PR)
  • target_meanpool / frontier_field / text_neg_field — unrelated prior-session work, not included

trashhalo added 11 commits July 24, 2026 16:01
…label_dims)

Three new loss terms in the FSQ label_dims path, operating directly on the
reserved-digit softmax (dim_lg) so the negative steers the move distribution,
not the reconstruction matrix:

- HardNegTerm: FIXED to route through the predicted emission (dim_lg soft-argmax
  + STE) on the FSQ path; the old version pushed on the frozen target recon
  (up_proj/down_proj), leaving ZERO gradient on the move logits (dim_lg /
  level_proj) so 'don't emit this blunder' trained nothing at any weight.
  Continuous path unchanged. Golden regenerated (the FSQ math changed).

- MoveNegTerm: penalizes P(blunder) = Pi over reserved digits of
  softmax(dim_lg[digit])[code]. Building block (shares the codeword->index
  precompute); empirically weak alone (raw codeword P~0.002).

- LegalMoveNegTerm: renormalizes over the per-row legal-move support set and
  penalizes the blunder's SHARE = sum_neg P(neg) / sum_legal P(legal) — the same
  space the decode metric reads (~10-35% blunder share), so the gradient is
  ~50x larger than MoveNegTerm's and actually reshapes mass.

Both terms read precomputed padded [N_rows, max_K, n_reserved] index tensors
built once in Trainer.__init__ — the step loop does ONE batched gather+prod per
set, no Python row loop.

Validated (chess, SmolLM2-135M full-FT, held-out shard_1 n=400): the legal-move
blunder MASS drops monotonically with weight — 34.5% (no-neg) -> 31.3% (0.3) ->
29.9% (0.6) -> 28.7% (1.0) — the first negative form to move mass outside the
noise floor (latent-cosine HardNegTerm and raw MoveNegTerm both left it flat).
0.6 is the operating point (best cp-regret 435 AND argmax-blunder 24%; at 1.0
the argmax regresses as the term over-regularizes the base digit CE).

Tests: test_hardneg_reaches_move (the fix reaches dim_lg), test_moveneg_reaches_mass,
test_legalneg_reaches_legal_mass (loss in legal-renorm range, grad ~6x MoveNegTerm,
one SGD step lowers legal-renorm P(blunder) 0.85->0.03). Full 14-test suite green.
…MoveNegTerm->SupportNegTerm

The terms are generic langset _LossTerm machinery (operate on the reserved-
digit softmax for any label_dims facet); chess was only the use case, so the
chess-flavored names were misleading. Renamed:
  MoveNegTerm        -> LabelNegTerm  (raw negative-label codeword penalty)
  LegalMoveNegTerm   -> SupportNegTerm (renormalized over a support set)
plus the matching config fields / trainer attrs / loss keys / test files.
  move_neg_field     -> label_neg_field
  legal_move_field   -> support_field
  lam_move_neg       -> lam_label_neg
  lam_legal_neg      -> lam_support_neg
No behavior change; the chess consumer (separate repo) passes the renamed
field names.
…share)

The missing positive loss: LabelDimsTerm trains per-digit MARGINALS only, but the decode reads the
JOINT-RENORMALIZED P(best)/Σ_legal P(legal) — never directly trained, so SF-best sits at rank 17-37
with <1% mass on the trained models (the 'barely considers best' failure, validated on 4 boards).

SupportPosTerm = cross-entropy toward best over the legal-move renormalized distribution:
    loss = -log( P(best) / Σ_legal P(legal) )
Same machinery as SupportNegTerm (the precomputed legal-move support index tensor), opposite sign.
Grad flows into dim_lg / level_proj; mechanistically concentrates on the WEAKER (unsolved) marginal
(the to-square, where TO-acc < FROM-acc) because the joint product bottlenecks on the smaller factor.

Test (test_supportpos_raises_best): loss in legal-renorm CE range, one SGD step raises best's share
0.15->1.0, grad reaches reserved digits only (not non-reserved/STOP), and concentrates on the weaker
(to) marginal when from is more solved. Full 15-test suite green; golden unaffected (self-skips off).

Copy link
Copy Markdown
Contributor Author

Closing as superseded by #20. The FSQ emitter, label_dims, and the associated chess-era hard-negative/legal-support geometry terms have been removed; named geometry now belongs in concepts/codebook state emission.

@trashhalo trashhalo closed this Jul 29, 2026
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