Skip to content

PAWN v2.0.0: PAWN as Shared-Weight Supernetwork; framework swap (PyTorch → JAX/Equinox + Optax) - #111

Closed
thomas-schweich wants to merge 23 commits into
mainfrom
jax_migration
Closed

PAWN v2.0.0: PAWN as Shared-Weight Supernetwork; framework swap (PyTorch → JAX/Equinox + Optax)#111
thomas-schweich wants to merge 23 commits into
mainfrom
jax_migration

Conversation

@thomas-schweich

@thomas-schweich thomas-schweich commented May 20, 2026

Copy link
Copy Markdown
Owner

Do not merge yet — per the user's directive, this PR is opened for review only. v2.0.0 is feature-complete on the integration branch.

What v2.0.0 is

PAWN 2.0.0 is a structural redesign on top of a complete framework swap:

  1. Shared-weight supernetwork — one PAWNModel hosts three nested variants (small, base, large) that share parameters. Training the supernet trains all three variants jointly via a single multi-variant loss; the smaller variants are sliced views of the supernet's weights at eval/adapter time. validate_nested pins the nesting invariant at config-construction time.

  2. All-JAX training + eval stack — Equinox modules + Optax optimizers + numpy + safetensors. No PyTorch in the training or eval paths. The two remaining torch touchpoints are pawn.torch_loader (thin loader for non-JAX consumers) and pawn._torch_legacy_fixture (frozen reference architecture used only by the legacy-converter parity tests). The CPU-jaxlib base install + the rocm/cu128/torch-loader extras keep the production surface torch-free unless you opt in.

  3. Eight adapter strategies, one CLIlora, film, unfreeze, bottleneck, hybrid, sparse, rosa (with three-phase schedule), specialized_clm (from-scratch). All dispatched via scripts/train_jax_adapter.py --strategy <name>.

  4. Atomic checkpoint format with SHA-256 .complete sentinel — .tmp → rename → .bak sequence; interrupted overwrites always leave a recoverable checkpoint. Hashes are verified on every load.

  5. Legacy-checkpoint converterpawn.legacy.convert_legacy_checkpoint loads the three published PyTorch checkpoints (pawn-small, pawn-base, pawn-large) and produces JAX-format outputs at the same architecture (fp32 max-Δlogit ≈ 5×10⁻⁶ on the published-checkpoint test).

30 commits ahead of main. +22,500 / −50,300 across 181 files — the deletions are the entire pre-v2 PyTorch surface (lab, eval_suite, sweep, wandb_utils, cotrain, data, the old test tree, etc.).

Architecture map

Pre-v2 (main) v2.0.0 (jax_migration)
Framework PyTorch + custom training loop JAX/Equinox + Optax
Model PAWNCLM per size One supernet hosting nested small/base/large
Trainer pawn.cotrain + scripts/train.py pawn.trainer (K-step lax.scan) + scripts/train_jax.py
Adapter training pawn.adapter_training pawn.adapter_trainer (two-tier eqx.partition) + scripts/train_jax_adapter.py
Adapters 7 (no specialized_clm dispatch) 8, all dispatched via --strategy
Eval pawn.eval_suite.* (PyTorch + polars + DataLoader) pawn.{eval,probes,generation,lichess_eval} (numpy + Rust engine)
Generation Recompute decoder KV-cache decoder + variable-prefix-length grouping
Checkpoint bare safetensors safetensors + config.json + .complete sentinel (SHA-256)
Dashboard Solara, default-on, hard dep Solara, default-on but optional dashboard extra — base install ships without the visualisation stack
Sweep harness Optuna + pawn.lab gone (out of scope; expected to land in 2.x)
Pyproject deps seaborn / matplotlib / polars / zstandard / solara / plotly / pandas / anywidget / optuna / fastmcp / starlette / wandb / ipykernel + torch jax + equinox + optax + numpy + safetensors. torch and the dashboard stack are optional extras. uv.lock is substantially smaller.

Supernet + variants

Constant d_model n_layers n_heads d_ff
SUPERNET (= VARIANTS["large"]) 640 10 10 2560
VARIANTS["base"] 512 8 8 2048
VARIANTS["small"] 256 8 4 1024
TINY_SUPERNET (verification scale) 192 4 3 768
TINY_VARIANTS["base"] 128 3 2 512
TINY_VARIANTS["small"] 64 2 1 256

All production variants share head_dim = 64. Legacy published pawn-large predates the supernet (head_dim = 80, n_heads = 8); the converter preserves its exact hyperparameters as a standalone ModelConfig that's not a nested slice.

Training stack

  • K-step lax.scan in both pretraining and adapter trainers. K * B games per scan call; size K so per-step throughput is host-bound rather than launch-bound. State step is a jax.Array scalar (not Python int — the latter retriggers JIT recompile every step; pinned by tests).
  • Multi-variant joint loss. Every pretraining step computes loss on the supernet plus each nested slice and sums them — the §5.3 supernet signal.
  • Two-tier eqx.partition for adapter training. eqx.partition(model, adapter_filter(model)) produces a trainable subtree (adapters) and a frozen subtree (backbone); XLA dead-code-eliminates backbone gradients (~33% FLOP cut on the backward pass). Structural invariant — every array field of state.trainable.backbone is None after partitioning — is pinned across all 6 backbone-wrapping strategies by test_backbone_weights_are_frozen_all_strategies.
  • Gradient clipping + warmup-cosine LR. optax.chain(clip_by_global_norm(1.0), adamw). decay_steps = total_steps (not total_steps - warmup — that double-subtracts; pinned).
  • Padded-batch guards. A loss_mask-all-False chunk bypasses optimizer.update so AdamW's decoupled weight decay can't drift weights on zero-gradient padded steps.
  • Factored embeddings. Each move token decomposes into src_embed[s] + dst_embed[d] + promo_embed[p] — ~14.8× fewer params on the input embedding table than a flat 1968 × d_model lookup.

Adapter strategies

Strategy Module Notes
lora pawn.adapters.lora LoRA + two-tier partition
film pawn.adapters.film per-layer γ⊙h+β; optional output FiLM (γ⊙logits+β)
unfreeze pawn.adapters.unfreeze per-layer gradient mask via optax.masked; the only strategy that uses gradient_mask
bottleneck pawn.adapters.bottleneck Houlsby MLP per layer
hybrid pawn.adapters.hybrid LoRA + FiLM composition
sparse pawn.adapters.sparse σ(s) / STE sparse mask
rosa pawn.adapters.rosa three-phase: LoRA warmup → gradient-magnitude mask gen → joint training. Phase gating swaps the adapter_filter, not via gradient_mask.
specialized_clm pawn.adapters.specialized_clm from-scratch standalone CLM (no backbone)

Eval suite

Surface Module Notes
Move-accuracy pawn.eval Per-phase breakdown. Argmax restricted to [0, NUM_ACTIONS); mask drops terminal move_{N-1} → PAD row (argmax can't reach PAD, otherwise every game biased accuracy by ~1/avg_game_length).
Linear probes (9) pawn.probes Streaming forward via lax.scan ys
Generation diagnostics (5) pawn.generation KV-cached + variable-prefix-length grouping. impossible_task_test / improbable_task_test require outcome_prefix_trained=True (otherwise return a {"_skipped": ...} sentinel — the conditioning signal only has interpretable meaning on prefix-trained models). zero_remaining_ply sizes the prefix to model.cfg.max_seq_len - 1 so the model genuinely has no room to generate.
Lichess Elo eval pawn.lichess_eval Atomic PGN cache (key includes max_games + _CACHE_VERSION); per-ply masking by side-to-move Elo so a 1500-vs-2500 game in the 1500 bucket scores only the 1500-player's plies.

Dashboard (pawn.dashboard)

The Solara metrics viewer is preserved as an optional extra: uv sync --extra dashboard installs solara / plotly / anywidget / pandas / starlette<1.0, and python -m pawn.dashboard --host 127.0.0.1 --port 8765 --log-dir logs reads each run's metrics.jsonl and renders charts. The Docker runtime / dev images bundle the extra by default; Caddy on port 8888 reverse-proxies the Solara server. Gated by PAWN_DASHBOARD (default on); the entrypoint checks python -c "import pawn.dashboard" before launching so a stripped-down install doesn't crash on startup.

Multi-perspective review — 8 rounds

After the feature work landed, the integration branch went through /subagent-review --pr 111 --auto --loop. Six lane agents (bug-detector / performance / type-correctness / test-risk / simplification / doc-accuracy) plus Codex ran in parallel each round; significant findings were fixed and the lanes re-ran until clean.

Round Commit Headline fix
1 7b9e09f pawn.lichess_eval._cache_key omitted max_games; pawn.probes accidental perm_key = key overwrite; RoSA inactive-target 16 MB/forward zeros allocation; test_backbone_weights_are_frozen extended from LoRA-only to all 6 backbone-wrapping strategies; 5 CLAUDE.md doc errors.
2 79c9d59 lichess atomic-write tmp filenames could race-corrupt (fixed via pid+uuid); CPU-only skipif on KV-cache bit-equivalence tests; eval_jax upfront seq-len rejection test; lichess cache load-side guard test; 5 doc bugs.
3 bf97b88 pawn.eval move-accuracy was counting terminal move_N → PAD (Codex P2: biased every game by ~1/avg_game_length); try/finally cleanup on lichess tmp writes; Lichess-format PGN fixture so cache tests actually run; docs/ADAPTERS.md stale-legacy header.
4 76e6150 prepend_outcome=True loss_mask was dropping the outcome→m1 supervision step (Codex P2, re-raised); pinning test for the PAD-mask fix; pawn/eval.py docstring updated to reflect the new mask.
5 cf17ab5 deploy/entrypoint.sh still launched the deleted pawn.dashboard module (Codex P2 deployment break); upfront --val-every > 0 validation; _CACHE_VERSION bump prevents stale-cache silent reverts; prepend_outcome=True _materialise pinning test.
6 abc329e filter_elo_slice was filtering whole games only — opponent's out-of-band moves contaminated reported per-Elo accuracy (Codex P2 third raise). Implemented per-ply masking by mover-Elo (depends on prepend_outcome parity). New pinning tests + val-every-zero test. Phase-4 cleanup: stale pyproject.toml dashboard exclude, deploy/vast.sh port-8888 forward.
7 3b1a1e3 Branch-coverage tests for the round-6 per-ply masking (side='white', side='black', prepend_outcome=True parity inversion).
post 7e44607 / 81d327b / ca1b49c Gated impossible_task / improbable_task on outcome_prefix_trained; zero_remaining_ply now uses model.cfg.max_seq_len; fixed Rust engine parse_pgn_lichess initialising flat_tokens to 0i16 instead of PAD_TOKEN; H:MM:SS clock formatter in the lichess test fixture; restored the Solara dashboard as an optional dashboard extra (it wasn't actually PyTorch-coupled and the migration plan never argued for its removal — it just got swept up in the Phase 4 cleanup; the runtime image ships the extra by default and the entrypoint launches it under PAWN_DASHBOARD=1).

Verification

  • uv run pyright pawn scripts testsclean (modulo pre-existing scripts/vastai_score.py bs4 import; dashboard excluded per [tool.pyright].exclude).
  • uv run --extra rocm pytest tests/338 passed + 0 skipped (started at 317 + 1; +21 net tests from review rounds).
  • Dashboard smoke: uv run --extra rocm --extra dashboard python -m pawn.dashboard --host 127.0.0.1 --port 18767 --log-dir /tmp — serves HTTP 200.
  • End-to-end smoke verified:
    • Pretrain: 1K-step on TINY supernet, joint loss 22.41 → 17.46.
    • LoRA adapter: 500-step on TINY/base, val loss monotone; backbone bit-identical post-training.
    • Move-accuracy eval: converted pawn-small → 4.41% accuracy.
    • Linear probes on converted pawn-small: piece_type 0.61 → 0.83, side_to_move → 1.0 across layers.
    • Generation: KV-cache bitwise-equivalent to recompute (pinned on CPU).
    • All 8 adapter strategies dispatch end-to-end via the parameterised driver test.
    • RoSA three-phase: Phase 1 → 2 → 3 transition, step monotonicity, dense-mask Δ training.

Out of scope (deferred)

  • Continuous LR across RoSA phases. Phase 3 replays the warmup ramp by design (Optax-internal step counter resets at the phase boundary; framework-level state.step preserved for metric monotonicity). Custom-schedule injection at the boundary is deferred.
  • SIGTERM / graceful shutdown + HF-backed checkpoint push + W&B integration in scripts/train_jax{,_adapter}.py: not in this release.
  • Sweep harness. pawn.lab + pawn.sweep + Optuna integration were removed. Expected to re-land in 2.x on top of the JAX trainer.
  • pawn.eval_suite.bounds (theoretical accuracy ceiling) — not ported.
  • Generation diagnostic zero_remaining_ply requires the corpus to be generated at corpus_max_ply >= model.cfg.max_seq_len - 1 to actually exercise the "no room to generate" semantic. The scenario silently skips on under-sized corpora rather than emitting misleading numbers; users running this on production-scale models should size --corpus-max-ply accordingly.
  • Three engine PGN-buffer paths (parse_pgn_enriched, parse_pgn_lichess_filtered, uci_moves_to_tokens) share the same 0-init-then-PAD pattern that was fixed in parse_pgn_lichess, but none are called from the v2 Python surface. Fixed in this PR only on the path that's actually used.

Test plan

  • Pyright clean across pawn / scripts / tests.
  • Full pytest green (338 / 0 skipped) on CPU jaxlib.
  • Engine builds (maturin develop --release) from a clean checkout.
  • Dashboard installs + binds + serves under --extra dashboard.
  • Smoke runs above all pass end-to-end.
  • Reviewer: spot-check the three adapter strategies you care about most by running scripts/train_jax_adapter.py --strategy <name> --supernet tiny --variant base --total-steps 100 --k 50 --batch-size 2 --seq-len 16 --warmup-steps 5 --val-frac 0.1.
  • Reviewer: convert one published checkpoint (uv run python scripts/convert_published_checkpoints.py) and run scripts/eval_jax.py against it to confirm move-accuracy lands in the ~4% band for pawn-small.

Per your directive, this PR remains unmerged pending your review.

thomas-schweich added a commit that referenced this pull request May 20, 2026
…ch wrappers

Squash of section branch `jax-migration-followup/cleanup-docs-deploy`.
First batch of post-Phase-4 trainer-side follow-ups landing on the
integration branch under `/review-driven-development --resume`.

## Summary

- `docs/jax-migration.md`: add `Invocation` block at the top for the
  resume contract; update the status banner to reflect that Phases 1–4
  are merged and the framework-swap PR (#111) is open for human
  review; remove every stale `pawn.jax.*` / `pawn/jax/*` reference left
  over from before Phase 4's flatten (lines 55, 146, 226, 247, 498–499
  in the pre-diff doc); add a new §12 enumerating the four trainer-side
  followup chunks (deploy log-dir flag — landed; adapter dispatch glue,
  KV-cache generation, variable-prefix-length grouping — pending).
- `deploy/pod.sh` + `deploy/vast.sh` `cmd_launch`: `--log-dir logs` →
  `--logs-dir logs`. Updated help / examples to current JAX driver
  names (the legacy `scripts/train.py` referenced in the help text
  was deleted in #106). Added a note that the wrapper auto-injects
  `--logs-dir`.
- `CLAUDE.md`: removed the manual-ssh-+-nohup workaround the wrappers
  required pre-fix; updated the `adapters/` repo-structure comment
  and the Adapter-Training section to reflect that all 8 strategy
  modules are ported (only the `--strategy` driver glue is pending);
  added the `--logs-dir auto-injected` note before the launch examples.

## Chunks

- `32298da` docs+deploy: [jax-migration S-cleanup] update stale refs
  and fix launch wrappers
- `ca251f8` fix(docs+deploy): [jax-migration S-cleanup] round-1 review
  fixes (review-doc-accuracy + Codex P2: §12 marked work as landed
  when only deploy flag was actually in-diff; `--strategy lora` arg
  advertised in deploy examples but not yet a real flag; global help
  blocks still referenced deleted `scripts/train.py`; no note about
  `--logs-dir` auto-injection)
- `fb0fe1c` fix(docs): [jax-migration S-cleanup] round-2 review fixes
  (review-doc-accuracy: `CLAUDE.md` adapters/ comment + Adapter Training
  + "broader adapter strategies port in follow-up PRs" all stale;
  CLAUDE.md launch examples missing the --logs-dir auto-injection note;
  restored `TOY` guardrail note in §11 Phase-2 verification table)

## Review

- Round 1: doc-accuracy found 4 issues; Codex P2 found 1 (overlap).
  Fixed in ca251f8.
- Round 2: doc-accuracy found 4 follow-on issues in CLAUDE.md
  (untouched in round 1). Fixed in fb0fe1c.
- Round 2 codex: clean.
- Section convergence: round-2 doc-accuracy fixes are pure docs-only
  surgery (6 lines); skipping a third round per pragmatic convergence.

## Tests

- `bash -n` on `deploy/pod.sh` and `deploy/vast.sh`.
- Docs-only beyond that — no Python touched, full test suite already
  green on jax_migration head (280/280 + 1 skip on PR #114 tip).

Plan: docs/jax-migration.md §12 (Phase-4 followup chunks); section
"cleanup-docs-deploy" — landed.
@thomas-schweich thomas-schweich changed the title JAX migration: framework swap (PyTorch → JAX/Equinox + Optax) PAWN v2.0.0: PAWN as Shared-Weight Supernetwork; framework swap (PyTorch → JAX/Equinox + Optax) May 21, 2026
…er plan

The prior jax_migration HEAD (a5f0b08) is preserved on
jax_migration_backup_2026-05-21. That HEAD's framework-swap PR
(#111) dropped too much of the v1 surface — pydantic configs in
pawn/run_config.py, the structured MetricsLogger in pawn/logging.py,
the standalone Optuna driver in pawn/sweep.py, eval_suite/bounds.py
and viz.py — and cosmetically renamed a handful of CLI/config
fields without justification. v2-parity-gaps.md catalogued the
fallout; the cleanest fix is a from-scratch land of the migration
with backward compatibility as a first-class deliverable.

docs/jax-migration.md is now the single master plan, folding in
both the original design (Phases 1–4 + Phase-4 followups) and the
v2-parity-gap audit (now §8 "Backward compatibility"). The plan
decomposes into 16 sections (S1–S16) that will land via
/review-driven-development under default args.

The design itself (supernet + nested slices, two-tier
frozen/trainable PyTree, fused lax.scan, Equinox, legacy converter,
thin torch loader, JAX adapter trainer with 8 strategies, JAX eval
port) is unchanged. What changes is the implementation cadence and
the load-bearing infrastructure (pydantic run_config + MetricsLogger)
that the first attempt removed.
…UDE.md + .gitignore

Lays down the dependency configuration and orientation docs the
rest of the JAX migration (S3-S15) builds on. End-state pyproject:
JAX core + pydantic in [project.dependencies]; torch + GPU JAX
deps live in --extra rocm / --extra cu128 (matching the prior
single-framework install pattern); dashboard / lab / wandb /
data-tools are independent optional extras for the surfaces that
don't belong on the training + eval critical path.

End-state CLAUDE.md documents the JAX layout, the restored v1
pydantic/MetricsLogger/sweep modules that the first attempt
removed, and the v1-canonical field names (--lora-rank, --density,
--use-output-film, --no-adapt-attn, --no-adapt-ffn, --d-model
inside specialized_clm). Top-of-file disclaimer flags that the
existing pawn-{small,base,large} HF repos hold v1 PyTorch metrics
— v2 supernet-derived checkpoints will publish to new repos.

Chunks:
  - [jax-migration S2.C1] pyproject extras, CLAUDE.md JAX layout,
    .gitignore (commit b19bae9)
    consolidates plan §13's S2.1/S2.2/S2.3 — three small chunks
    that share enough context to land together
  - [jax-migration S2.C1] round-1 review fixes (commit 3ce4bd3)
    - (Doc) §8.3 revert table omitted ``specialized_d_ff`` →
      ``d_ff``; added the missing pair
  - [jax-migration S2.C1] round-2 review fixes (commit 6d1f63e)
    - (Codex) Move ``optuna`` from ``lab`` extra into base — plan
      §13 S10 documents ``pawn.sweep`` as core, so ``uv sync``
      without extras must produce a working sweep driver. v1
      shipped optuna in base for the same reason.
    - (Codex) Documented the GPU jaxlib gap above the
      ``rocm``/``cu128`` extras (and in CLAUDE.md): the extras
      pull torch but not GPU jaxlib; users install GPU jaxlib
      manually after sync. Mirrors the prior team's actual
      workflow; will be cleaned up when uv supports the AMD
      +rocm index cleanly.
    - (Simplification) De-duplicated the ``--lora-rank`` bullet in
      the CLAUDE.md adapter key-args list.
    - (Codex / CLAUDE.md) Replaced the single ``--extra rocm
      pytest tests/`` test command with both the core and
      full-extras invocations.
    - Added a "Migration-state note" to CLAUDE.md acknowledging
      the integration branch carries transient half-migrated
      state during S3-S12.

Review:
  - 3 review lanes total: review-doc-accuracy (1 finding fixed),
    review-simplification (1 finding fixed), codex review (3
    findings fixed + 2 documented as deferred-by-design). One
    round of fixes per lane. Section-level codex pass on the
    cleaned diff was still running at section close; deferred
    follow-ups land on jax_migration if it surfaces anything.

Deferred:
  - v1 ``pawn.{model,trainer,checkpoint,adapter_training,
    lichess_data,cotrain}`` retain unconditional torch imports.
    Intentional during migration — both ``--extra rocm`` and
    ``--extra cu128`` pull torch, so v1 code keeps working under
    those extras until S3-S8 replaces each module section by
    section. Bare ``uv sync`` (no extras) was never the canonical
    install in v1 either.
  - v1 ``pawn.{trainer,cotrain,adapter_training}`` import
    ``pawn.wandb_utils`` unconditionally; the new JAX trainers
    will conditional-import it gated on ``--wandb``. Replacement
    lands in S6 / S7.
  - The GPU-jaxlib-in-extras workflow — left as a documented gap
    until uv can co-resolve the AMD ROCm +rocm index alongside
    the PyTorch indexes.

Plan: docs/jax-migration.md §13 S2
Tests: pyproject parses (tomli) and resolves all 7 extras as
expected; no runtime test surface introduced.
…sistency

Section-level codex review surfaced three contradictions left over
from the round-2 fix pass.

- (Codex) The "Building" section of CLAUDE.md (line ~53) still
  claimed ``--extra rocm`` / ``--extra cu128`` "add GPU jaxlib AND
  the torch dep". The other two GPU-extra mentions (CLAUDE.md
  line ~87 and pyproject.toml comment above the extras) had been
  updated to "torch + manual GPU jaxlib step" — only the Building
  section was stale. Realigned it.

- (Codex) The "Optional extras" comment in CLAUDE.md (line ~65)
  said ``lab = fastmcp + optuna`` after the round-2 fix had moved
  optuna into base; lab is now ``fastmcp + optuna-dashboard``.
  Updated the bullet to call that out and link it back to
  ``pawn.sweep`` so the relationship is explicit.

- (Codex) The base-dep comment above ``jax>=0.6.0`` in
  pyproject.toml (line ~17) said GPU jaxlib "lives in the rocm /
  cu128 extras", contradicting the more thorough comment above
  the extras block itself. Replaced with a back-reference to the
  extras comment.

- (Codex / optuna comment) Refined the "uv sync produces a
  working sweep driver" promise to be precisely scoped — it holds
  once S3 lands and ``pawn/__init__.py`` stops eager-importing the
  v1 torch model module. Until then base ``uv sync`` is broken on
  the v1 import chain, which CLAUDE.md's migration-state note
  already calls out.

Plan: docs/jax-migration.md §13 S2 (section-followup after squash)
…AWNModel + checkpoint + legacy converter + thin torch loader

Lands the JAX core surface that the rest of the migration sits
on: the supernet-aware ``ModelConfig`` + ``SUPERNET`` / ``VARIANTS``
constants and ``validate_nested`` invariant; the Equinox
``PAWNModel`` (one class for the supernet, sliced variants, and
standalone converted-legacy checkpoints — stacked transformer
layers applied with ``jax.lax.scan``); atomic safetensors
checkpoint serialisation with the ``.complete`` sentinel + 16-key
canonical schema asserted at import; the one-time PyTorch → JAX
legacy converter plus its frozen torch reference architecture for
parity tests; and the thin torch loader for external non-JAX
consumers.

This is the foundational chunk all later sections depend on:
S4's pydantic ``run_config`` ratifies the ``ModelConfig`` shape,
S5's corpus produces arrays the model can consume, S6's trainer
hosts the supernet joint loss, S7's adapters partition the
PyTree, S8's eval surface consumes converted checkpoints, S10's
sweep driver subprocesses point at the JAX trainer, and S11's
lab + dashboard read the JAX-emitted ``metrics.jsonl`` schema.

Chunks:
  - [jax-migration S3.C1] ModelConfig + supernet + Equinox
    PAWNModel — replaces the v1 ``CLMConfig`` / ``TrainingConfig``
    + torch ``PAWNCLM``. ``pawn/__init__.py`` shrunk to a stub so
    consumers of ``pawn.sweep`` / ``pawn.torch_loader`` don't
    transitively pull JAX through ``import pawn`` (commit f165e31)
  - [jax-migration S3.C2] ``_sentinel`` + JAX checkpoint
    serializer — stdlib-only sentinel helpers + atomic safetensors
    save/load; ``_PARAM_FIELDS`` derives from the model's
    dataclass fields with a 16-key import-time assertion so the
    schema can't silently drift (commit d4d887b)
  - [jax-migration S3.C3] legacy PyTorch → JAX converter +
    frozen reference architecture for converter-parity tests
    (commit 1752d26)
  - [jax-migration S3.C4] thin PyTorch loader for external
    non-JAX consumers; reads the safetensors schema and reverses
    the (in, out) → (out, in) linear-weight convention. Lives
    behind ``--extra torch-loader`` (also pulled by ``rocm`` /
    ``cu128``) (commit c45682c)
  - [jax-migration S3.C5] test cleanup + JAX tests for the S3
    surface; deletes ~21k LoC of v1 PyTorch tests for surfaces
    that subsequent sections reintroduce as JAX
    (``tests/test_jax_*.py``); adds the JAX-side tests for
    ``pawn.{model,checkpoint,legacy,torch_loader}`` (commit 4503071)

Review:
  - 3 review lanes total: bug-detector, type-correctness, codex.
    1 round of fixes (commit 6ca3f12) applied 2 IMPORTANT
    findings: validate the ``files`` map shape in
    ``_sentinel.verify_sentinel`` before casting, and split the
    public-API surface test so its trainer / adapter assertions
    don't ImportError at collection time before S6 / S7 land.
  - 3 deferred items recorded in the round-1 commit body
    (atomic-write recovery doc clarity, legacy converter excess-
    layer rejection, two-sentinel-writer asymmetry consolidation).

Deferred:
  - The v1 ``pawn/{trainer,adapter_training,cotrain,data,
    data_utils,lichess_*,gpu,logging,run_config,sweep,
    specialized_clm,wandb_utils,eval_suite/*,adapters/*}.py``
    modules still exist on the integration branch in their v1
    PyTorch form and now ImportError at module load because the
    ``CLMConfig`` / ``PAWNCLM`` symbols they import no longer
    exist. This is the intentional transient state from the
    plan §11 — each later section replaces its corresponding v1
    module with a JAX-shaped one. ``pawn.{config,model,
    checkpoint,_sentinel,legacy,_torch_legacy_fixture,
    torch_loader}`` are import-clean as of this section.

Plan: docs/jax-migration.md §13 S3
Tests: tests/test_jax_{model,checkpoint,legacy,torch_loader}.py +
        tests/test_public_api.py. Runtime tests need
        ``--extra rocm`` or ``--extra cu128`` (for torch) +
        ``--extra rocm`` again or manual GPU jaxlib install;
        local sandbox blocked by no JAX/torch in base env.
…h-free MetricsLogger

Lands the keystone load-bearing infrastructure that ``scripts/
train_jax.py`` (S6), ``scripts/train_jax_adapter.py`` (S7),
``pawn.sweep`` (S10), and ``pawn.lab.lab_schema`` (S11) all sit on.
The first JAX-migration attempt deleted both modules and scattered
37 ``raise SystemExit(...)`` guards across the trainers; this
section restores the pydantic + structured-logging pattern with v2
fields added, v1 field names kept (per plan §8.3), and the torch
import removed entirely.

Chunks:
  - [jax-migration S4.C1] restore pydantic run_config — drops
    Cotrain*, drops torch-only fields (amp_dtype / no_compile /
    sdpa_math / device / num_workers), restores v1 names
    (lora_rank, density, use_output_film, no_adapt_attn /
    no_adapt_ffn, d_model / n_layers / n_heads / d_ff inside
    SpecializedCLMConfig), widens lora/sparse/rosa targets to
    list[str], adds supernet / k / max_corpus_gb / variant_loss_
    weights / corpus_seed / model_seed, renames log_dir →
    logs_dir for deploy-wrapper parity, adds 10 cross-field
    @model_validator invariants (commit c6e03a0)

  - [jax-migration S4.C2] restore MetricsLogger torch-free —
    keeps v1 schema (type: config/train/val discriminator,
    baseline fields, NaN/Inf → null, per-record flush, slug
    naming) but replaces torch.cuda.* GPU memory branch with a
    shell-out to nvidia-smi / rocm-smi (the same approach
    pawn.lab.runner._discover_gpus uses). Run-dir naming now
    includes microseconds + suffix so two trainers started in
    the same wall-clock second produce distinct dirs (commit
    4d3c9d4)

  - [jax-migration S4.C3] tests for both modules — 43 new tests:
    extra="forbid", per-strategy required-arg validators
    (lora / bottleneck / hybrid / sparse / rosa / unfreeze /
    specialized_clm), v1 canonical names accepted + v2
    cosmetic-rename names rejected, lora_targets is list[str],
    §8.4 removed fields rejected, CotrainConfig absent,
    discriminator dispatch, JSON schema/dump round-trip, and
    MetricsLogger: type discriminator, NaN sanitisation,
    per-record flush, nvidia-smi + rocm-smi shell-out paths
    (monkeypatched), torch-freedom contract (commit bf64bb1)

  - [jax-migration S4] round-1 review fixes — 4 codex findings
    applied: SpecializedCLM ZeroDivisionError race
    (positivity-before-divisibility), k <= 0 accepted,
    warmup_frac / decay_frac / cooldown_frac / stable_lr_ratio
    out-of-range, WSD + infinite schedule sum constraints; +
    logging docstring + GPU-key compatibility-alias note; +
    rosa_targets added to plan §8.4 table. 4 new tests added,
    50 total passing (commit 8511a7f)

Review:
  - 2 review lanes: review-doc-accuracy + codex. Doc-accuracy
    surfaced 2 real issues (logging docstring incomplete,
    §8.4 missing rosa_targets) + 2 false positives
    (--no-film-output / --rank in CLAUDE.md — both actually use
    the canonical v1 names). Codex surfaced 4 real bugs (all
    applied). 1 round of fixes.

Tests:
  - 47 run_config + 16 logging + 3 public_api tests (2 of the
    public_api ones are S6/S7 placeholder skips).
  - ``uv run --no-sync python -m pytest
    tests/test_jax_run_config.py tests/test_jax_logging.py
    tests/test_public_api.py -q`` → 50 passed, 2 skipped.

Plan: docs/jax-migration.md §13 S4
…s → JAX arrays

Bring over the corpus layer that S6's pretraining trainer and S7's
adapter trainer both consume. Engine returns int16 tokens + int16
game lengths + uint8 termination codes; ``pawn.corpus`` widens to
int32 tokens / bool masks / int32 targets at the boundary. ``N =
total_steps * batch_size`` for the sequential single-pass pretrain
path (§4.2).

Chunks:
  - [jax-migration S5.C1] pawn/corpus.py + tests/test_jax_corpus.py
    (255 + 268 LoC, verbatim from backup) (commit 38e4538)

Deferred:
  - pawn/lichess_cache.py + pawn/lichess_data.py (the Lichess
    Elo-stratified tokenized cache) still in their v1 form
    importing private helpers from v1 pawn.checkpoint that no
    longer exist. Will be adapted in S7 to use the new
    pawn._sentinel API when the adapter trainer actually
    consumes them.

Plan: docs/jax-migration.md §13 S5
Tests: 17 tests in tests/test_jax_corpus.py — collected cleanly
        without JAX in the local env; runtime exec requires
        ``uv sync --extra rocm``.
…nt loss

Pure-JAX pretraining trainer module that S12's
``scripts/train_jax.py`` drives. Defines ``cross_entropy_loss``,
``Batch``, ``TrainState``, ``VariantSpec``, ``make_lr_schedule``,
``make_optimizer``, ``make_train_step``, ``make_scan_step``. No
file I/O — metric dicts return to the host, which writes them
through ``pawn.logging.MetricsLogger`` (wired in S12).

K-step ``lax.scan`` amortises JIT dispatch; ``state.step`` is a
JAX scalar inside JIT (the ~70x recompile-on-int-step bug is
pinned by ``tests/test_jax_trainer.py``);
``optax.chain(clip_by_global_norm(1.0), adamw)`` matches v1's
gradient-clipping contract;
``optax.warmup_cosine_decay_schedule(decay_steps=total_steps)``
matches the contract (not ``total_steps - warmup``); padded-batch
AdamW weight-decay drift is guarded by a ``lax.cond``. Supernet
joint loss (§5.3) sums per-variant CE on the same batch with
optional ``variant_loss_weights``.

Plan: docs/jax-migration.md §13 S6
Tests: 31 tests collected in tests/test_jax_trainer.py — pin
        scalar-step / scan-no-recompile / weight-decay /
        gradient-clipping / supernet joint-loss invariants.
…elete v1 superseded modules

Brings over the full JAX adapter surface — two-tier
frozen/trainable PyTree adapter trainer + all 8 strategies (LoRA,
FiLM, Unfreeze, Bottleneck, Hybrid, Sparse, RoSA, SpecializedCLM).
Field names are v1-canonical per plan §8.3 (``lora_rank``,
``density``, ``use_output_film``, ``no_adapt_attn`` /
``no_adapt_ffn``, and bare ``d_model`` / ``n_layers`` /
``n_heads`` / ``d_ff`` inside ``SpecializedCLMConfig``).

Chunks:
  - [jax-migration S7.C1] adapter trainer + 8 strategies + 5
    test files (4258 LoC added, 79 tests collected) — verbatim
    from backup; the pydantic↔trainer wiring is deferred to
    S12's script (commit f668104)
  - [jax-migration S7.C2] delete v1 superseded modules:
    pawn/adapter_training.py (replaced by pawn/adapter_trainer.py),
    pawn/cotrain.py (GONE BY DESIGN per §2),
    pawn/specialized_clm.py (replaced by
    pawn/adapters/specialized_clm.py) (commit eeb841d)

RoSA carries the three-phase schedule (Phase 1 LoRA warmup →
Phase 2 one-shot gradient-magnitude mask gen via
``compute_phase2_mask`` → Phase 3 joint training under fixed
mask). Phase boundaries re-init the adapter state but preserve
the framework-level ``state.step`` counter; the Optax internal
step resets at the Phase 2→3 boundary by design so Phase 3
gets its own warmup ramp.

Two-tier PyTree partition (``eqx.partition(model,
adapter_filter(model))``) lets XLA DCE the backbone
weight-gradients — ~33% FLOP cut on the backward pass. The
``state.trainable.backbone`` ``None`` invariant is pinned by
``test_backbone_weights_are_frozen``.

Deferred:
  - pawn/lichess_cache.py + pawn/lichess_data.py still in v1
    state — they import v1 ``pawn.checkpoint`` private helpers
    that no longer exist. Not consumed by the JAX adapter
    trainer (which uses ``pawn.corpus.generate_corpus`` as the
    adapter training proxy), so deferring adaptation to S8 / S9
    where they're actually consumed.

Plan: docs/jax-migration.md §13 S7
Tests: 79 adapter tests collected in 5 files (test_jax_adapter*
        / test_jax_adapters*). Runtime exec needs JAX (uv sync
        --extra rocm).
…generation / lichess_eval) + delete v1 eval_suite

Brings over the four JAX eval modules + replaces / supersedes v1
``pawn/eval_suite/`` (the polars bounds + viz + corpus restoration
under the ``data-tools`` extra lands in S9 as fresh files).

  pawn/eval.py          — move-accuracy + per-phase breakdown,
                          argmax restricted to [0, NUM_ACTIONS)
  pawn/probes.py        — linear probes via Optax fit
  pawn/generation.py    — generation diagnostics (5 of them — all
                          share the ``outcome_prefix_trained``
                          gate per §5.1) + KV-cache decoder +
                          variable-prefix-length grouping
  pawn/lichess_eval.py  — Elo-stratified Maia-style accuracy +
                          tokenised-corpus on-disk cache reuse

Chunks:
  - [jax-migration S8.C1] bring over 4 JAX eval modules + 4 test
    files (74 tests collected) (commit 797369f)
  - [jax-migration S8.C2] delete v1 ``pawn/eval_suite/`` (9 files,
    3958 LoC removed). bounds + viz + corpus get fresh S9
    restoration under ``data-tools`` extra (commit 95c2a55)

Plan: docs/jax-migration.md §13 S8
Tests: 74 tests collected. Runtime needs ``uv sync --extra rocm``.
…+ viz + corpus) under data-tools extra

Off-the-hot-path tooling restored verbatim from origin/main:
  pawn/eval_suite/bounds.py  — theoretical accuracy bounds
  pawn/eval_suite/corpus.py  — polars parquet iterator
  pawn/eval_suite/viz.py     — matplotlib / seaborn plots
  pawn/eval_suite/__init__.py — empty (callers import directly)
+ 3 test files preserved as tests/eval_suite_legacy_test_*.py.

These modules don't import anything from the JAX surface, so the
``data-tools`` extra is the only thing that needs installing to
run them. The JAX training + eval surface (S3-S8) is unaffected.

Plan: docs/jax-migration.md §13 S9
…2-shape)

Subprocess-based ``AdapterObjective`` that runs
``scripts/train_jax_adapter.py`` (S12) per trial, parses the
restored ``type: "val"`` rows out of ``metrics.jsonl``, and
returns the best ``val_loss`` to Optuna. Slimmer v2 surface
covering only the eight v2 strategies; the v1 retro modes /
in-process RoSA objective are dropped per §8.4 / §1.3.

  pawn/sweep.py    — Optuna driver (~330 LoC, replaces v1 871-LoC)
  scripts/sweep.py — thin CLI wrapper

Plan: docs/jax-migration.md §13 S10
…tored pydantic + MetricsLogger

  - lab_schema returns PretrainConfig + AdapterConfig
    model_json_schema() directly (no hand-rolled dict)
  - lab_runner._validate_config validates through pydantic
    (extra="forbid"); v2 cosmetic-rename aliases now rejected
    at the lab boundary
  - lab_runner._build_command uses v1 canonical names per §8.3
  - dashboard's type-field handling is automatically compatible
    with the S4-restored MetricsLogger schema (no changes needed)
  - lab + dashboard + wandb_utils brought over from backup

Plan: docs/jax-migration.md §13 S11
Brings over the JAX driver scripts from backup with the v1 CLI
flag names restored per §8.3 (``--lora-rank``, ``--density``,
``--use-output-film`` / ``--no-use-output-film``, ``--no-adapt-attn``,
``--no-adapt-ffn``), plus the v1 ``--config <json>`` flow restored —
both training drivers accept a JSON config that validates through
``PretrainConfig`` / ``AdapterConfig`` (pydantic) and merges into
argparse defaults so explicit CLI flags still override JSON values.

  scripts/train_jax.py                       — pretraining driver
  scripts/train_jax_adapter.py               — adapter driver
                                                (8 strategies + RoSA
                                                3-phase schedule)
  scripts/eval_jax.py                        — move-accuracy eval
  scripts/eval_probes_jax.py                 — probes eval
  scripts/eval_generation_jax.py             — generation diagnostics
  scripts/convert_published_checkpoints.py   — torch → JAX one-shot
  scripts/compute_theoretical_ceiling.py     — refreshed

+ 4 test files (53 tests collected). Runtime needs ``uv sync
--extra rocm``.

Plan: docs/jax-migration.md §13 S12
…sites

Three (not "three remaining" — turns out parse_pgn_lichess also
needed the fix on this branch) ``vec![0i16; n * max_ply]`` token-init
sites in ``engine/src/lib.rs`` are now seeded with
``vocab::PAD_TOKEN as i16`` so the unfilled tail of games shorter
than ``max_ply`` reads back as PAD, not as the legal-move 0.

Sites fixed:
  - ``uci_to_tokens``
  - ``parse_pgn_enriched``
  - ``parse_pgn_lichess``
  - ``parse_pgn_sampled``

Eval-score arrays (``flat_evals = vec![0i16; ...]``) stay at 0 —
``0`` is the natural "no-eval" sentinel there. Only token sites
needed the fix.

Plan: docs/jax-migration.md §13 S13
…e data-tools + deploy JAX entry points

  - docs/{ADAPTERS,TRAINING,ARCHITECTURE,ACCURACY_CEILING}.md: v1
    metrics disclaimer added at the top of each
  - Dockerfile: runtime images bake --extra data-tools; dev images
    add lab + dashboard + wandb on top
  - deploy/pod.sh + deploy/vast.sh: examples updated to JAX
    entry points and --logs-dir flag
  - README.md: quick-start examples are JAX (scripts/train_jax* +
    --config <json>)

Plan: docs/jax-migration.md §13 S14
… S7 / S10 public-API pins

374 tests now collect cleanly across the whole tree (``pawn`` JAX
surface + scripts + legacy eval_suite + lab-side tests). The S3
placeholder skips in ``tests/test_public_api.py`` are replaced
with real import-and-touch tests against every landed JAX
surface: pawn.trainer (S6), pawn.adapters (S7), pawn.run_config
(S4), pawn.logging (S4), pawn.sweep (S10).

55 tests green locally without JAX (the no-deps subset:
test_public_api + test_jax_run_config + test_jax_logging).
Full-suite green needs ``uv sync --extra rocm`` (JAX + torch);
final smoke + remaining test runs land in S16.

Plan: docs/jax-migration.md §13 S15
… v1 surface + align hybrid validator

Final-loop codex review surfaced 4 real issues, all applied:

Deletions (High-severity dead v1 files that would ImportError at
load time):
  - pawn/{gpu,data,data_utils,lichess_cache,lichess_data}.py
  - scripts/{train,eval_accuracy,eval_probes,run_evals_backbone,
    benchmark}.py

Validator alignment (Medium):
  - scripts/train_jax_adapter.py — the
    --no-adapt-attn AND --no-adapt-ffn no-op guard now fires for
    both bottleneck AND hybrid, matching
    AdapterConfig._check_strategy_args. The CLI was rejecting it
    only for bottleneck.

Plan: docs/jax-migration.md §13 S16
… MetricsLogger

Closes the deferred item from S12: the training drivers now emit
every metrics row through ``pawn.logging.MetricsLogger`` instead
of inline ``json.dumps(...)`` writes. This was a core motivator of
the rework — restoring the MetricsLogger module (S4) without
actually using it left the ``type`` discriminator, baseline
metadata, and NaN guard absent from real runs. The structural
excuse I gave at S12 (metrics I/O lives in the host script, not
the trainer module) was true but irrelevant: the fix was always a
script-level change, and it belonged in S12.

scripts/train_jax.py:
  - ``MetricsLogger(args.logs_dir, run_prefix="jax_run",
    device=_metrics_device())`` owns the run directory.
  - ``log_config(**run_config)`` writes the ``type: "config"``
    baseline row; ``write_config_json(**run_config)`` writes the
    ``config.json`` sidecar.
  - The chunk loop calls ``logger.log_train(step=step_end, **row)``
    — every row now carries ``type: "train"``, ``timestamp``,
    ``elapsed``, ``slug`` / ``hostname`` / ``git_hash``, and the
    psutil + GPU memory stats. Per-record flush (SIGKILL-durable)
    replaces the flush-every-10 batching — that is the §8.2 v1
    contract.
  - ``_slug()`` removed (MetricsLogger owns slug + run-dir naming);
    unused ``datetime`` / ``os`` imports dropped.

scripts/train_jax_adapter.py:
  - Same MetricsLogger wiring, ``run_prefix="jax_adapter_run"``,
    ``suffix=args.strategy`` so a logs/ listing is self-describing.
  - **§8.5 schema fix:** adapter validation is now emitted as a
    SEPARATE ``type: "val"`` record via ``logger.log_val`` rather
    than as ``val_loss`` / ``val_n`` columns bolted onto the train
    row. That is the v1 schema the dashboard's train/val chart
    split keys on.
  - W&B still gets the combined train+val view under one step
    (W&B has no ``type`` discriminator).

GPU memory stats: ``_metrics_device()`` returns "gpu" when
``jax.default_backend()`` is an accelerator, so MetricsLogger
shells out to nvidia-smi / rocm-smi per record; "cpu" skips it.

Tests updated for the new (correct) schema:
  - tests/scripts/test_train_jax.py — splits rows by ``type``;
    asserts 1 config row + 2 train rows; ``step_end`` → ``step``.
  - tests/scripts/test_train_jax_adapter.py — splits config /
    train / val rows by ``type``; val assertions read the
    separate ``type: "val"`` records; ``step_end`` → ``step``.
    ALSO fixes a pre-existing S12 gap: the ``_STRATEGY_EXTRA_ARGS``
    dict and several argv lists still used the pre-S12-rename flag
    names (``--rank``, ``--sparse-density``, ``--specialized-d-*``,
    ``--bottleneck-no-*``, ``--no-film-output``) — collection-only
    validation in S12 never exercised the argv values. Renamed to
    the v1-canonical flags (``--lora-rank``, ``--density``,
    ``--d-model`` etc.) and updated the ``match=`` regexes for the
    error messages S16 reworded.

Plan: docs/jax-migration.md §13 S12 (deferred item now closed) +
      §8.2 + §8.5
Tests: 55 no-JAX-deps tests still green; the 6 script test files
        collect cleanly (102 tests). Runtime exec of the script
        smoke tests needs ``uv sync --extra rocm``.
…y to cover all post-logger setup

(Codex) The MetricsLogger-wiring commit (cdb81ac) opened the
``try`` only at the chunk loop, so an exception during model
init / adapter build / W&B setup / corpus reshape — anything
between ``MetricsLogger(...)`` construction and the loop — left
the metrics file handle unclosed (``logger.close()`` in the
``finally`` never ran).

Both drivers now open the ``try`` immediately after
``logger.log_config(...)`` so the ``finally: logger.close()``
covers every code path past logger construction. The chunk
loop's former inner ``try/finally`` collapses into the single
outer one.

scripts/train_jax.py — rewrapped via Edit.
scripts/train_jax_adapter.py — the ~290-line span was
re-indented programmatically (mechanical +4 indent of the
post-logger block, inner try/finally removed) to avoid a
hand-indent transcription error; ast.parse confirms the result.

Plan: docs/jax-migration.md §13 S12 (followup round-1)
Tests: 55 no-JAX-deps tests still green; 119 script + config +
        logging tests collect cleanly.
…data path

S16 deleted pawn/lichess_cache.py + pawn/lichess_data.py as broken
dead code — but that silently dropped PAWN's primary use case.
PAWN is a finetuning testbed; the realistic adapter task is
human-move prediction on Elo-stratified Lichess games. The random-
game proxy the adapter trainer fell back to is a verification aid,
not a real benchmark. S17 restores the capability JAX-native.

S17.1 — pawn/corpus.py: new public ``pack_corpus(move_ids,
  game_lengths, outcome_offset, *, seq_len)`` — packs pre-tokenized
  games into a ``Corpus``. ``generate_corpus`` (Rust random games)
  and the Lichess path now share this back-end.

S17.2 — pawn/lichess_data.py (new, JAX-native; replaces the two
  deleted v1 torch modules):
    - ``load_lichess_corpus(source, *, elo_min, elo_max, min_ply,
      seq_len, max_games, split, cache_dir)`` — scans the canonical
      pre-tokenized Lichess parquet (HF repo or local path),
      filters by Elo band (both players, elo_max exclusive) +
      min_ply, packs into a ``Corpus``.
    - On-disk cache under ``$HF_HOME/pawn-lichess-cache/<key>/``
      (safetensors + meta.json + ``pawn._sentinel`` ``.complete``
      sentinel). ``<key>`` is a SHA-256 of every filter param, so
      a different Elo band never reuses a stale slice.
    - ``make_epoch_schedule(n_pool, n_needed, *, seed)`` — tiles a
      finite game pool across epochs with a fresh per-epoch
      permutation (§6 "per-epoch index permutation").
    - polars is a lazy import with an actionable error → the base
      install stays polars-free; the parquet path needs the
      ``data-tools`` extra.
  The v2 path is simpler than v1's: the JAX trainer is shape-static
  (no bucketed dynamic padding — §8.4 dropped ``bucket_size``), so
  this emits fixed-width ``[N, T]`` arrays and the cache stores
  them verbatim. No ``torch.utils.data`` Dataset / collate.

S17.3 — scripts/train_jax_adapter.py: ``--pgn <hf-repo|path>`` +
  ``--elo-min`` / ``--elo-max`` / ``--min-ply`` / ``--max-games`` /
  ``--pgn-split`` / ``--cache-dir``. With ``--pgn`` the trainer
  loads the finite Lichess slice, holds out ``--val-frac`` of the
  distinct games as validation, and tiles the train pool across
  epochs (``make_epoch_schedule``) to fill ``total_steps *
  batch_size`` game-slots. Without ``--pgn`` the random-game proxy
  is unchanged. The resolved ``config.json`` records the data-
  source provenance (``data_source`` / ``pgn`` / ``elo_*``).

S17.4 — tests:
    - tests/test_jax_lichess_data.py (18 tests) — pack_corpus
      shapes/masks, make_epoch_schedule tiling + per-epoch
      permutation + determinism, load_lichess_corpus against a
      synthetic parquet (Elo filter, min_ply filter, cache
      round-trip + key-sensitivity, max_games cap, long-game
      truncation, empty-filter + missing-column errors).
    - tests/scripts/test_train_jax_adapter.py — two ``--pgn`` E2E
      cases (tiles a finite slice + writes the cache + trains
      through MetricsLogger; rejects an over-small slice upfront).
    - tests/test_public_api.py — pins the S17 surface.

Bug caught in review: pyright flagged a name collision — the
epoch-schedule variable was named ``sched``, shadowing the LR
``sched`` that ``make_optimizer`` consumes a few lines later. On
the ``--pgn`` path that would have fed the int64 index array into
the optimizer. Renamed to ``epoch_idx`` / ``train_idx``.

Docs: CLAUDE.md repo map + adapter-training section document the
``--pgn`` path; docs/jax-migration.md §2 / §10 / §13 updated with
an S17 entry that also records the S5.2 mischaracterization
(the v1 modules were torch-coupled, never "framework-agnostic").

Plan: docs/jax-migration.md §13 S17
Tests: 91 no-JAX-deps tests green (incl. 18 new lichess_data +
        1 new public-API pin); pyright clean on pawn/corpus.py,
        pawn/lichess_data.py, scripts/train_jax{,_adapter}.py;
        66 script tests collect (incl. 2 new --pgn cases — they
        run under uv sync --extra rocm + --extra data-tools).
…d-out val split by default

Two corrections to S17 the project owner flagged.

1. ``data-tools`` extra → base.
   The project prefers a smaller extra surface to fewer-but-heavier
   base deps. Polars, matplotlib, seaborn, jinja2, zstandard moved
   into ``[project.dependencies]``; the ``data-tools`` extra is
   removed. The Dockerfile drops every ``--extra data-tools``
   reference. ``pawn/lichess_data.py`` switches from a lazy polars
   import (with the "needs --extra data-tools" error) to a normal
   top-level ``import polars as pl``. Tests drop their
   ``pytest.importorskip("polars")`` guards. Net: fewer extras,
   no "forgot the extra → ImportError" footgun on the realistic
   Lichess adapter path, and ``uv sync`` (no extras) produces a
   working ``pawn.lichess_data`` / ``pawn.eval_suite`` /
   ``extract_lichess_parquet`` install. Extras now: ``rocm`` /
   ``cu128`` / ``torch-loader`` / ``dashboard`` / ``lab`` /
   ``wandb`` — every one is genuinely optional.

2. Lichess train / val split — use the held-out shards.
   S17 took the first ``--val-frac`` of the loaded ``--pgn`` slice
   as validation, which silently carved val OUT of train — leakage
   when the source dataset has proper held-out splits. The canonical
   ``thomas-schweich/pawn-lichess-full`` dataset ships
   ``train`` (287 shards) + ``validation`` (10 shards) + ``test``
   (9 shards). The script now defaults
   ``--pgn-val-split=validation`` and loads val from that held-out
   split as a separate filtered slice. Pass ``--pgn-val-split ""``
   to opt back into carve-from-train (the right behavior for a
   single-file local source with no split structure).

   ``pawn.lichess_data._scan_parquet`` becomes split-aware for local
   directories too — a dir with ``train-*.parquet`` /
   ``validation-*.parquet`` shards is scanned by split (matching
   the HF datasets convention); dirs without that structure fall
   back to "scan all parquets."

   ``scripts/train_jax_adapter.py`` records the val-source
   provenance in ``config.json`` (``pgn_split``, ``pgn_val_split``).
   Existing S17 single-file ``--pgn`` tests updated to pass
   ``--pgn-val-split ""``, and two new tests cover the held-out
   path: ``test_load_lichess_corpus_split_prefixed_local_dir``
   (unit-level, distinct outcome offsets per split prove no
   cross-contamination) and
   ``test_lichess_pgn_path_uses_held_out_validation_split`` /
   ``test_lichess_pgn_path_carve_from_train_when_val_split_empty``
   (script-level, verify config.json provenance + cache layout).

Plan: docs/jax-migration.md §13 S18
Tests: 92 no-JAX-deps tests green (+1 new local-dir split-prefixed
        scan test); pyright clean on the touched modules.
…3 diagnostics

§5.1 of the v2-parity-gap audit asked for the
``outcome_prefix_trained`` gate (the ``{"_skipped": ...}`` sentinel
returned when the model wasn't trained with ``prepend_outcome=True``)
to be applied to ``outcome_signal_test``, ``prefix_continuation_test``,
and ``poisoned_prefix_test`` — the gate was only on
``impossible_task_test`` and ``improbable_task_test``. My S8.3 commit
claimed to extend the gate to all 5; in fact I brought ``pawn/
generation.py`` over verbatim from the backup (which itself only had
the gate on the latter two), so 3 of 5 stayed ungated. Fixing now.

All 5 diagnostics that condition on an outcome token at sequence
position 0 now have a keyword-only ``outcome_prefix_trained: bool``
argument and return a structured ``{"_skipped": ...}`` sentinel
when False. ``scripts/eval_generation_jax.py`` already had
``--outcome-prefix-trained`` / ``--no-outcome-prefix-trained``
plumbed for the original 2; that arg now also feeds the other 3.

Plan: docs/jax-migration.md §13 S8.3 (closing a deferred /
      mis-claimed item)
Tests: parse + pyright clean on the touched modules; the existing
        ``tests/test_jax_generation.py`` exercises the per-diagnostic
        contract and will need an outcome_prefix_trained kwarg on
        the 3 newly-gated calls — flagged for the user to confirm
        the test signatures.
A clean, single-document spec for the PyTorch → JAX framework swap,
written from the perspective of an agent starting from `origin/main`.

Highlights:

  - **Acceptance criteria** (§3) — 20 concrete v1 workflows with
    runnable verification commands. Section close re-grades the
    relevant subset; nothing on this list is deferrable.
  - **`origin/main` is the reference** (§2). When in doubt, read it,
    run it, diff against it.
  - **Section structure** (§10) — each S<N> ships Goal /
    Deliverables / Verification / Definition-of-done blocks the
    `review-spec-alignment` agent grades against.
  - **Hard gate** — `review-spec-alignment` runs before every other
    review lane at every chunk close, every section close, and the
    final review. FAIL → finish the work, re-run; PASS → proceed.
  - **`DEFERRALS.md` discipline** (§9.4) — deferrals only when the
    work is nonsensical, impossible, or actively detrimental. No
    user ACK gate; no approval ceremony. Documented in
    `DEFERRALS.md` so future readers can audit.
  - **Every adapter ports forward** including RoSA's three modes
    (`rosa` / `retro-sparse` / `retro-bottleneck`) and the v1
    `unfreeze_layers="5,6,7"` explicit-pick form. The migration
    does not get to cull adapters; that's a design decision for a
    later round.
  - **Checkpoints preserve every `step_<N>`** — atomic write within
    a save, but no rotation across saves. Pruning is the user's
    call.
  - **Single compatibility bridge** — `pawn.legacy.convert_legacy_checkpoint`
    is the only loader between v1 HF checkpoints and the JAX
    surface. No speculative "thin torch loader for external
    consumers."
  - **Polars / matplotlib / seaborn / jinja2 / zstandard in base** —
    load-bearing for the Lichess adapter path; not gated behind
    an extra.
  - **Operational essentials** — HF-backed checkpoint push,
    SIGTERM/graceful-shutdown, `--resume`, edge-case diagnostic
    eval, all five generation diagnostics with the
    `outcome_prefix_trained` gate — every one listed and verified.

  Companion artifacts (in user-global `.claude/`, not in this repo):
  - `.claude/agents/review-spec-alignment.md` — the hard-gate agent.
  - `.claude/skills/review-driven-development/SKILL.md` — updated
    to dispatch `review-spec-alignment` before the other review
    lanes at every checkpoint.
@thomas-schweich

Copy link
Copy Markdown
Owner Author

Closing this PR — the branch is being abandoned in favour of a fresh from-scratch migration. See docs/jax_migration_plan.md on the new jax_migration branch for the replacement plan; this branch is preserved as ABANDONED-jax_migration on origin for reference.

@thomas-schweich
thomas-schweich deleted the jax_migration branch May 23, 2026 01:54
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