Table of contents
- TL;DR
- Model details
- Intended use & limitations
- Prompt & output format
- How to use
- Training data
- Training procedure
- Evaluation
- Results
- The research finding
- Reproducibility & artifacts
- Citation
TL;DR
- What: Qwen3-1.7B + LoRA that writes a Stockfish-style verbalized-minimax analysis for a chess
position and then commits
best move: <uci>. No engine graft, no soft-tokens — a pure-text
control.
- Why it exists: to test whether plain text chain-of-thought (plus on-policy DAgger correction)
can turn chess reasoning into a good committed move at the 1.7B scale.
- Headline result: it can't. DAgger improved the text baseline from −116% to a −66% peak
(this checkpoint) recovery of the search headroom, but that peak still commits worse than a
no-search policy prior, and neither more DAgger rounds nor fresher, more-diverse on-policy data
pushed past it.
Model details
Table | |
|---|
| Base model | Qwen/Qwen3-1.7B |
| Adapter type | LoRA (PEFT), r = 32, α = 64, dropout 0 |
| Target modules | q_proj, k_proj, v_proj, o_proj, gate_proj, up_proj, down_proj |
| Trainable params | ~35 M (adapter only; ~133 MB safetensors) |
| Precision | bf16 |
| Task | causal-LM (chess position → verbalized analysis → committed move) |
Intended use & limitations
Intended use. Research reproducibility for the Latent Grafting "read → commit wall" study; a
baseline to compare against latent-value approaches; an example of a verbalized-minimax CoT format.
Out of scope. Actual chess play or move recommendation. On adversarially-selected hard positions
this model's committed move is, on average, worse than simply taking a policy prior's top move
without any search. It also has not been evaluated for full-game Elo.
Known limitations.
- Commits worse than the no-search prior on the search-required suite (see Results).
- Trained/evaluated only on chess; single game.
- The evaluation suite is selected around a particular prior's errors — absolute recovery % is
calibrated to that frame (the direction of the result is robust; the magnitude is frame-relative).
- Occasionally the verbalized value reads saturate; commit is by parsing
best move: from the trace.
Input:
FEN: <fen> moves: <space-separated recent uci moves>
Analyze:
Output (the model continues from Analyze:): a verbalized-minimax trace — top candidate moves
with evaluations and short principal-variation lines — terminating in a legal committed move:
<uci>: <eval> (line: <pv> ) ; <uci>: <eval> (line: <pv> ) ; ... ; best move: <uci>
How to use
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel
import torch
REPO = "GoodStartLabs/sf-cot-dagger-chess-qwen3-1.7b"
tok = AutoTokenizer.from_pretrained("Qwen/Qwen3-1.7B")
base = AutoModelForCausalLM.from_pretrained("Qwen/Qwen3-1.7B", torch_dtype=torch.bfloat16)
model = PeftModel.from_pretrained(base, REPO).eval()
fen = "r1bqkbnr/pppp1ppp/2n5/4p3/2B1P3/5Q2/PPPP1PPP/RNB1K1NR w KQkq - 4 4"
prompt = f"FEN: {fen} moves: \nAnalyze:"
ids = tok(prompt, return_tensors="pt")
out = model.generate(**ids, max_new_tokens=200, do_sample=False)
print(tok.decode(out[0][ids.input_ids.shape[1]:], skip_special_tokens=True))
Training data
- Base SFT corpus: ~40,000 verbalized-minimax Stockfish traces (Stockfish 17.1). Each root
position is analysed with top-6 multipv plus principal-variation lines, at adaptive depth
(12, deepened to 20 when the top-2 evaluations are close), then serialized into the text format
above. ~38.9k unique positions (highly diverse).
- DAgger corpora (rounds 1–5): on-policy positions the model reached or mis-committed during
roll-out, each relabeled with a fresh correct Stockfish trace (wrong-commits duplicated 2×).
Accumulated cumulatively across rounds. Root positions mined from a self-play/tactical shard pool.
Training procedure
- SFT the base corpus (completion-only cross-entropy on the trace tokens; the prompt is masked).
- DAgger loop, 5 rounds: roll the current model out over ~2,000 root positions → flag the
positions where its committed move disagrees with Stockfish + the position it reaches after its
move → generate correct Stockfish traces for those → append to the corpus → retrain from the
cumulative corpus (3,000 steps). Round r's model seeds round r+1's roll-out.
- Checkpoint selection:
dag5 = the round with the best committed-move recovery (and, on the
original loop, the minimum SFT eval-loss simultaneously).
Hyperparameters: LoRA r32/α64; bf16; completion-only CE; per-round 3,000 steps.
Evaluation
Metric. Committed-move centipawn loss (cp-loss): the model commits one move per position; the
loss is how many centipawns worse that move is than Stockfish's best (per Stockfish evaluation).
Suite. A search-required suite of 150 positions, selected so a no-search policy prior is
materially wrong (prior cp-loss 173.0; Stockfish-best "oracle" 92.6). This isolates positions where
reasoning/search should help.
Recovery % = (prior − model) / (prior − oracle) — the fraction of the 173→92.6 headroom the
model closes. 0% = matches the prior; +100% = matches Stockfish; negative = worse than the prior.
Definitions of the reference points:
- base LLM — Qwen3-1.7B reading the raw FEN, no adapter (built-in ability, ~619 Elo elsewhere).
- prior — a no-search policy baseline's top move (already a strong reference: it beats this
model). Not the bare base model.
- oracle — Stockfish's actual best move on the suite.
Results
Committed-move cp-loss on the search-required suite (prior 173.0, oracle 92.6):
Table with columns: model, cp-loss, recovery| model | cp-loss | recovery |
|---|
| Stockfish best (oracle) | 92.6 | +100% |
| no-search policy prior | 173.0 | 0% |
| this model — SF-CoT + DAgger round 5 | 226.4 | −66% |
| SF-CoT baseline (pre-DAgger) | 266.4 | −116% |
DAgger trajectory (this model is round 5):
Table with columns: round, cp-loss, recovery, SFT eval-loss| round | cp-loss | recovery | SFT eval-loss |
|---|
| pre-DAgger | 266.4 | −116% | — |
| 1 | 263.6 | −113% | — |
| 2 | 247.6 | −93% | 0.543 |
| 3 | 250.4 | −96% | 0.519 |
| 4 |
Past round 5, SFT eval-loss kept falling to new minima while committed-move recovery worsened —
imitation loss and move quality decouple.
Diversity control (fresh disjoint on-policy positions each round, restarted from round 5):
Table with columns: round, recovery, eval-loss| round | recovery | eval-loss |
|---|
| fresh 1 | −96% | 0.489 |
| fresh 2 | −84% | 0.495 |
| fresh 3 | −101% | 0.507 |
| fresh 4 | −77% | 0.497 |
Fresh, more-diverse positions (eval-loss rises, confirming they are not merely memorized) still land
in the −77…−101% band — they do not recover the round-5 peak. So the plateau is not an
artifact of DAgger re-mining a stale position set; it is a genuine commit ceiling.
The research finding (why this model exists)
This model is the text-only control in a study of where a value signal is lost between a chess engine
and a language model's committed move. The broader project finds:
- The engine's position value is abundant and linearly readable upstream (a ridge probe recovers
position value at R² 0.92 from the engine trunk), and a dedicated latent reader can commit at
+95% recovery.
- But every path that turns that value into a generated committed move at 1.7B loses it: latent
graft generative commit ≈ −42%; this text-CoT baseline ≈ −116%, improved by DAgger to a
−66% peak — all still worse than the no-search prior.
Conclusion: at this scale the bottleneck is move selection / commit, not the availability or
readability of the value signal. Plain text chain-of-thought + DAgger does not overcome it (and this
model is its best point). See the project write-up for the full ladder (base → prior → oracle) and the
latent-value approaches.
Reproducibility & artifacts
- Base model:
Qwen/Qwen3-1.7B.
- This adapter:
runs/graft/sf_baseline_dag5/lora_best (round-5 DAgger checkpoint).
- Format: PEFT LoRA (
adapter_config.json + adapter_model.safetensors).
- Eval: committed-move cp-loss on a 150-position search-required suite (prior 173.0, oracle 92.6).
Citation
If you reference this artifact:
@misc{goodstartlabs_sfcot_dagger_2026,
title = {SF-CoT + DAgger: a text chain-of-thought chess move-commit baseline (Qwen3-1.7B LoRA)},
author = {GoodStart Labs},
year = {2026},
note = {Latent Grafting project; research negative-result artifact.},
howpublished = {\url{https://huggingface.co/GoodStartLabs/sf-cot-dagger-chess-qwen3-1.7b}}
}