YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
ptcg-gpt
A 30M-parameter GPT trained on PokΓ©mon TCG game-event streams from the Kaggle
pokemon-tcg-ai-battle
ladder replays. The backbone is the unmodified transformer block stack from
Andrej Karpathy's nanoGPT (see NOTICE.md); the input embedder and
output heads are replaced to consume/produce structured game-stream tokens
instead of text. Two checkpoints ship:
| checkpoint | analogy | trained with | use it for |
|---|---|---|---|
models/pretrain_30m_v1 |
foundation model | world-model pretraining: next-token prediction over entire game streams (events, board snapshots, menus) + outcome value | continued pretraining-style finetunes, probing what the model knows about game dynamics, init for new SFT runs |
models/sft_30m_v1 |
instruct model | action SFT on top of the foundation: pointer-head cross-entropy against the expert's chosen menu option at decision points only | picking actions β this is the "plays the game" checkpoint, and the default --init-from for further finetuning |
The training data is per-seat, append-only token streams serialized from real
ladder episodes (stream format v2, 18 float features per token β
path9_gpt/STREAM_SPEC.md is the normative contract). One document = one
(game, seat), averaging ~2.4β2.7k tokens per seat-game across the shipped
corpora (individual seat-games range from ~150 to ~6k).
What this repo can and cannot do. It can ingest replays, train, finetune,
and evaluate on held-out data. It cannot play games: the competition game
engine (cg.dll), the self-play driver, the Kaggle submission exporter, and
the arena all deliberately stay in the parent research repo. path9_gpt/agent.py
ships as the reference inference implementation (incremental serializer +
KV-cache), but there is nothing here to plug it into.
Results
Training (both on one RTX 3060):
| run | data | tokens seen | wall time | headline val metrics (shared 525-episode holdout + temporal tail) |
|---|---|---|---|---|
pretrain_30m_v1 |
mid+top tiers, 114,756 seat-docs / 293.8M train tokens | 704.9M (3 epochs) | 4.75 h | composite val loss 0.5545 (next-card CE 0.179, next-token-type CE 0.003, next-enum CE 0.035, value MSE 0.676) β still improving at schedule end |
sft_30m_v1 |
top tier only, 17,948 docs / 1.42M decisions (+10% pretrain-replay mixture) | 85.5M (2 epochs) | 34 min | top-1 single 0.704 Β· multi exact-set 0.619 Β· multi per-pick 0.816 Β· value MSE 0.637 |
Playing strength, measured in the parent repo's arena (not reproducible here β no engine):
| opponent | win rate |
|---|---|
develop_first (rule-based baseline) |
97.3% |
| prior best search-distilled agent | 82.8% [78.3, 86.6] over 320 games |
| path5 board-snapshot behavior-cloning model | 70.0% [62.5, 76.6] |
Caveats that apply to any number you produce yourself: 100-game win rates carry roughly Β±10 pp of noise β use 300+ games before claiming anything; and argmax-vs-argmax mirror matches between behavior-cloned siblings are a degenerate evaluation (they swing wildly and mean nothing).
Quickstart
pip install -r requirements.txt
python -m pytest # from the repo root; needs torch
Load a checkpoint:
from path9_gpt.model import load_model
model = load_model("models/sft_30m_v1/best.pt").eval()
# checkpoints are self-describing {"state_dict", "config"} dicts (fp32, model-only)
Smoke-train on the bundled sample corpus (108 episodes, 522k tokens, mid tier). Commands are kept on one line so they paste into PowerShell as well as bash:
python -m path9_gpt.pack --data data/sample # regenerate data/sample/packed/
python -m path9_gpt.train_pretrain --data data/sample --out runs/smoke --preset 7M --epochs 1 --lr 3e-4 --min-lr 3e-5 --batch-tokens 16384 --micro-batch-tokens 8192 --ctx-dropout 0.35 --device auto
(Add --n-layer 2 --n-embd 64 --n-head 2 to override the preset and make it
near-instant on CPU.)
Finetune from a shipped checkpoint on your own data:
python -m path9_gpt.train_sft --init-from models/sft_30m_v1 --data <your_top_tier_dir> --out runs/sft_v2 --epochs 2 --lr 1e-4 --min-lr 1e-5 --warmup-frac 0.03 --batch-tokens 65536 --micro-batch-tokens 8192 --ctx-dropout 0.35 --val-episodes models/val_episodes_v1.txt --device cuda
Always pass --val-episodes models/val_episodes_v1.txt β it pins the
shared holdout the shipped numbers were measured on, so those episodes never
leak into your training set and your val numbers stay comparable. See
docs/FINETUNING.md for the full ingest β pack β train
walkthrough and docs/DESIGN.md for the architecture and the
lessons/pitfalls list.
Repo layout
| path | what |
|---|---|
path9_gpt/ |
the package (name kept from the parent repo for checkpoint/tooling compatibility) |
path9_gpt/STREAM_SPEC.md |
normative wire-format contract (stream v2): token grammar, feature layouts, ingest record schema |
path9_gpt/BRIEF.md |
historical working notes from the parent repo |
path9_gpt/vocab.py |
vocab constants + card-attribute feature matrix loader (card_features_v1.npz ships in-package; --build needs the parent repo's engine, see docs) |
path9_gpt/stream.py |
StreamSerializer β single implementation shared by offline serialization and live inference |
path9_gpt/ingest.py |
raw episode JSONs (or datagen shards) β stream shards, with dedup, tier/day stamping; --oracle stores hidden-info labels in the records (unused by the v1 trainers β reserved for future aux losses) |
path9_gpt/replay_compat.py |
vendored replay-parsing helpers from the parent repo (keeps this repo engine-free) |
path9_gpt/pack.py / dataset.py |
one-time columnar pack of a dataset dir; memmap-backed loading, length-bucketed batching |
path9_gpt/model.py |
nanoGPT blocks (vendored, unmodified) + structured embedder + pointer/value/world-model heads + KV-cache path |
path9_gpt/train_pretrain.py / train_sft.py |
stage 1 / stage 2 training CLIs |
path9_gpt/agent.py |
reference inference agent (serializer + KV cache + fallback chain) β no engine here to run it against |
path9_gpt/audit_value.py / deck_stats.py / bench_latency.py |
value-head calibration audit Β· corpus deck statistics Β· CPU latency bench |
tests/ |
pytest suite (engine-free, fully self-contained: data/sample feeds the pack tests, data/raw_episodes_sample feeds the golden-parity/grammar tests) |
models/pretrain_30m_v1/, models/sft_30m_v1/ |
best.pt (fp32, model-only) + report.json + log.jsonl each |
models/val_episodes_v1.txt |
the shared 525-episode holdout list β pass to every training run |
data/sample/ |
108-episode sample corpus (216 seat-docs, 15,657 decisions, 522,262 tokens); packed/ is regenerable |
data/raw_episodes_sample/ |
4 raw ladder episode JSONs (gzipped, with visualize) β standalone input for the serializer contract tests and a tiny ingest --src smoke |
scripts/shrink_fp16.py |
fp32 β fp16 checkpoint shrink (see below) |
docs/ |
DESIGN.md, FINETUNING.md, the original 2026-07-20 planning doc |
Note for GitHub
Each best.pt is 121 MB fp32, which is over GitHub's 100 MB per-file
hard limit. This repo is therefore initialized with
git-lfs tracking models/**/*.pt (see
.gitattributes), so pushing to GitHub works as-is β but anyone cloning
needs git-lfs installed to get real checkpoints instead of LFS pointer
files. If you would rather not depend on LFS, run scripts/shrink_fp16.py
to produce an fp16 copy (~61 MB, fits under the limit; fp16 rounding changes
logits marginally β keep the fp32 original around for training resumes and
exact reproduction) and commit that instead.
Licensing
- This repo has no license yet. Default copyright applies (all rights reserved) β pick a license before publishing anywhere public.
- The transformer blocks inside
path9_gpt/model.pyderive from nanoGPT and are MIT-licensed β attribution and license text in NOTICE.md. - The training data derives from Kaggle ladder replays of the
pokemon-tcg-ai-battlecompetition (this includesdata/sample/anddata/raw_episodes_sample/). Check the competition's data/terms of use before redistributing any of it or models trained on it beyond this circle.