CoolFace
Modelpublic

GoodStartLabs/sf-cot-dagger-chess-qwen3-1.7b

sourceHugging Faceapache-2.0updated 1mo agoView on Hugging Face
0likes22downloads
Model Card

SF-CoT + DAgger (round 5) — chess move-commit baseline (LoRA adapter)

A LoRA adapter on Qwen/Qwen3-1.7B, supervised-fine-tuned on verbalized-minimax Stockfish chain-of-thought traces and then refined with 5 rounds of DAgger (on-policy roll-out → Stockfish relabel → retrain). This checkpoint (dag5) is the best-by-committed-move-quality point of that baseline, produced as a control experiment in the Latent Grafting research project.

⚠️ Research / negative-result artifact — not a strong chess engine. It is the least-bad member of a family of models that all commit chess moves worse than a trivial no-search policy baseline on hard positions. It is published for reproducibility of the finding described below, not for play. Do not deploy it as a chess player.

Table of contents

  1. 1.TL;DR
  2. 2.Model details
  3. 3.Intended use & limitations
  4. 4.Prompt & output format
  5. 5.How to use
  6. 6.Training data
  7. 7.Training procedure
  8. 8.Evaluation
  9. 9.Results
  10. 10.The research finding
  11. 11.Reproducibility & artifacts
  12. 12.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

Base model`Qwen/Qwen3-1.7B`
Adapter typeLoRA (PEFT), r = 32, α = 64, dropout 0
Target modulesq_proj, k_proj, v_proj, o_proj, gate_proj, up_proj, down_proj
Trainable params~35 M (adapter only; ~133 MB safetensors)
Precisionbf16
Taskcausal-LM (chess position → verbalized analysis → committed move)
Graft / soft-tokensNone — this is the text-only control (no engine trunk spliced in)
LicenseApache-2.0 (inherits base model license)

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.

Prompt & output format

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

python
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))
# ... parse the trailing "best move: <uci>" to get the committed move.

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

  1. 1.SFT the base corpus (completion-only cross-entropy on the trace tokens; the prompt is masked).
  2. 2.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.
  3. 3.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):

modelcp-lossrecovery
Stockfish best (oracle)92.6+100%
no-search policy prior173.00%
this model — SF-CoT + DAgger round 5226.4−66%
SF-CoT baseline (pre-DAgger)266.4−116%

DAgger trajectory (this model is round 5):

roundcp-lossrecoverySFT eval-loss
pre-DAgger266.4−116%
1263.6−113%
2247.6−93%0.543
3250.4−96%0.519
4255.5−103%0.497
5 (this model)226.4−66%0.477
6233.0−75%0.491
7236.3−79%0.451
8237.0−80%0.446

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):

roundrecoveryeval-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:

bibtex
@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}}
}