--- license: gemma language: - en library_name: mlx tags: - mlx - mlx-lm - lora - gemma - gemma4 - klondike - solitaire - distillation - text-generation base_model: mlx-community/Gemma4-E2B-IT-Text-int4 base_model_relation: adapter pipeline_tag: text-generation datasets: - chayuto/klondike-llm-decisions --- # gemma-4-E2B-it solitaire-advisor LoRA A LoRA adapter that distils a 31B Gemma Klondike Solitaire advisor into the ~2B-effective **Gemma 4 E2B** text model (4-bit, MLX), runnable locally on a 16 GB Apple Silicon Mac. This is the **Gemma 4 E2B successor** to [`chayuto/gemma-3n-e2b-it-solitaire-advisor-lora`](https://huggingface.co/chayuto/gemma-3n-e2b-it-solitaire-advisor-lora). That earlier card promised a Gemma 4 E2B variant "once `mlx-lm` ships the missing architecture support"; the base is now loadable via a small local `sanitize()` patch (see [Usage](#usage)), so this is that promised release, same project, same teacher, same data pipeline. It is the project's **strongest and lead student**: it beats the untuned base under a trusted full-game evaluation both on held-out decks and on fresh, never-seen decks (it generalizes), not just on a single-turn bench. Headline numbers, all adjudicated by exact engine replay plus a sound solver: - **In-distribution** (13 held-out solver-winnable decks): **5 wins vs the base's 1**, mean foundation cards 27.7 vs 14.2. - **Out-of-distribution** (fresh, solver-winnable decks with zero corpus overlap): wins **roughly 1 in 3** (about 33%, 16 of 48 decks pooled across three fresh samples), versus roughly 8% for the untuned base, so about **4x the base win-rate** on unseen deals. The first 12-deck sample read 5 wins vs the base's 1 (+12.9 mean paired foundation-card delta, better on 9 of 12); a later 24-deck sample pinned the broader rate lower (see [Generalization](#generalization-12-fresh-never-seen-solver-winnable-decks)). The teacher is `gemma-4-31b-it` (Google's Gemma 4 31B, accessed through a separate harvester app). The teacher itself wins roughly 31% of games, so this adapter's ceiling is teacher-level imitation, not optimal play. ## Model details | | | |---|---| | Base model | [`mlx-community/Gemma4-E2B-IT-Text-int4`](https://huggingface.co/mlx-community/Gemma4-E2B-IT-Text-int4) (Gemma 4 E2B IT, text-only, int4) | | Adapter type | LoRA over a 4-bit-quantised base | | LoRA rank | 16 (scale 2.0, dropout 0.05) | | LoRA target modules | `self_attn.{q,k,v,o}_proj`, `mlp.{gate,up,down}_proj` | | LoRA layers | top 16 layers | | Training framework | [`mlx-lm`](https://github.com/ml-explore/mlx-lm) | | Hardware | Apple M5, 16 GB unified memory (Metal GPU) | | Adapter size on disk | ~51 MB per checkpoint (bf16 LoRA weights over the 4-bit base) | | Iterations trained | 1,000 (shipped weights = iter 1,000) | | Quantisation | base remains int4; LoRA weights bfloat16 | | Decoding used for eval | greedy | The shipped `adapters.safetensors` is byte-identical to `checkpoints/0001000_adapters.safetensors` (iter 1,000), selected over the earlier checkpoints by the checkpoint-selection pass ([below](#checkpoint-selection-why-iter-1000)). ## Intended use **In scope.** Acting as a move-selection advisor inside a Klondike Solitaire client that already enforces game rules: - Imperfect-information **draw-1** Klondike (one card flipped from stock per draw); the advisor is shown the full visible state plus the count of face-down cards. - Per-turn decisions: given the harvester prompt (with a `LEGAL MOVES` block), emit a single JSON object choosing one offered legal move by index, or `move_index: -1` to resign. - Local inference on Apple Silicon via `mlx-lm`. **Out of scope.** - Open-ended chat or general text generation. The model is fine-tuned to a narrow JSON-emitting role and is expected to be worse than the base at unrelated tasks. - Game-rule enforcement. It selects from a `legalMoves` array supplied in the prompt; it does not verify legality from first principles. - Optimal play. The teacher wins ~31% of games; this adapter inherits that ceiling. - Other Solitaire variants (Spider, FreeCell), out of distribution. ## Evaluation ### Method Unlike the 3n predecessor (scored on a 20-state single-turn tier bench), this adapter is evaluated on **full games**, which is the eval the project now trusts. Each game is played turn-by-turn under the faithful production prompt (`hybrid-v1.6`) on a fixed deck, greedy decoding, cap 200 turns, with a tiered JSON parse-rescue (temp-0.3 retry) matching production. Every finished or resigned game is then **exact-adjudicated**: the recorded decisions are replayed through the engine with zero drift, and the final or resign position is handed to a sound best-first solver (SOLVED / UNSOLVABLE / UNKNOWN at a 300k-node cap). This distinguishes a real win from a cap-truncated one, and a correct resign on a dead board from a quit on a winnable one. The two eval sets: - **In-distribution:** 13 winnable decks held out by seed from the harvester pool that the training data was drawn from. - **Generalization:** 12 freshly dealt, solver-confirmed-winnable decks (seeds 9000002..9000026) with verified zero overlap with the benchmark or any training corpus, so they are guaranteed unseen by teacher and student. `meanFC` below is the mean number of cards on foundations (out of 52) at game end across the deck set; 52 is a win. ### In-distribution (13 held-out winnable decks) | arm | corpus | meanFC | wins | resigns | |---|---|---:|---:|---:| | base (untuned) | none | 14.2 | 1 | 0 | | gate | 2,492 rows, 100% won | 27.5 | 4 | 0 | | allsucc | 2,500 rows, 38% won | 25.5 | 3 | 1 | | **volume (this adapter)** | **6,859 rows, 36% won** | **27.7** | **5** | **1** | Two facts this established. First, training beats the untuned base under trusted eval (the first time in the project). Second, the won-only filter is not the lever; data **volume** is: the won-only `gate` and the natural-mix `allsucc` tie at matched size, and the full-volume arm here is the strongest student, so "collect and train on more data" is the validated recipe. The 1 in-distribution resign (`#4221577640`) is on a deal that was winnable from the deal; whether volume's board at the moment it resigned was already dead is UNKNOWN at the 300k-node cap, so it is neither claimed correct nor false. ### Generalization (12 fresh, never-seen, solver-winnable decks) The decisive question was whether the student learned to play Klondike or memorized the harvester's deck distribution. On fresh decks with zero corpus overlap, paired against base: | arm | wins/12 | meanFC | mean paired delta vs base | better than base | |---|---:|---:|---:|---:| | base | 1 | 15.6 | --- | --- | | gate (won-only) | 3 | 21.2 | +5.6 | 7/12 | | **volume (this adapter)** | **5** | **28.5** | **+12.9** | **9/12** | **Verdict: GENERALIZES, decisively.** Both trained arms show a positive paired delta and win multiple fresh, never-seen decks that base cannot. The student learned to play; it did not memorize the deck pool. Win counts are exact (no cap-truncated win above fc=40). Read the **paired delta**, not absolute fc: the fresh set is biased easy (only deals the solver cracked under a 200k-node cap were kept), which lifts both arms and compresses the gap. Resigns on the fresh set are informative because every fresh deck is winnable. Volume resigned 2, and both were adjudicated **correct**: `#9000021` (fc18/fd3) solved UNSOLVABLE, `#9000024` (fc11/fd12) solved UNSOLVABLE. Both boards were already structurally dead at the moment of resignation, so the resigns saved a 200-turn flail rather than throwing a winnable game. Where there is an error it is earlier play, not the resign itself (judge resign-correctness on the board at resign, not the deck at deal). **Update (2026-07-01): pooled fresh-deck rate pinned at ~33%, and two follow-on programs did not beat this adapter.** The 5/12 above is one of three fresh samples. A larger 24-deck fresh set (2026-06-28) read 6/24 strict (8/24 after adjudication, meanFC 24.4), and pooling all three fresh samples gives **16 of 48, about 33%** (roughly 1 in 3 fresh winnable decks), below the ~42% the two small samples implied but still about 4x the untuned base (~8%). Two subsequent programs to push past this adapter both failed to beat it: static-corpus reweighting and solver-grounding (closed 2026-06-23, every recipe collapses to 3-4 of 12 on fresh decks while volume holds 5) and on-policy SFT on the student's own self-wins (STaR / expert iteration, closed 2026-07-01, the best diverse-pool retrain reaches parity at 4 vs 5, not a lift). So this volume adapter stands as the project's lead student; the only untried lever to beat rather than match it is reward-weighted RFT/GRPO. ### Checkpoint selection (why iter 1,000) Quality across the four saved checkpoints is **non-monotonic**, so the final checkpoint is not automatically the best; it was selected on the 13 held-out decks. JSON discipline is measured as temp-0.3 parse-rescues (lower is cleaner): | checkpoint | wins | meanFC | temp parse-rescues | |---|---:|---:|---:| | iter 250 | 5 | 31.7 | 102 | | iter 500 | 2 | 21.3 | 126 | | **iter 1,000 (shipped)** | **5** | **27.7** | **34** | Iter 1,000 keeps the win count of the best early checkpoint while being roughly 3x cleaner on JSON, and iter 500 is a deep trough. The remaining JSON-discipline gap is best closed by constrained decoding at inference, not by an earlier checkpoint (see [Limitations](#limitations)). The 250/500/750 checkpoints are included under `checkpoints/` for reproducibility of this table. ### Note on comparing to the 3n predecessor The 3n adapter's headline numbers come from a 20-state single-turn tier bench; this adapter's come from full-game play with exact adjudication. They are **not directly comparable**. The shift to full-game adjudicated eval is deliberate: in this project, raw harness scores have repeatedly disagreed with what an exact replay plus a sound solver show, so single-turn or unadjudicated numbers are treated as unreliable. The full-game result here is the stronger and more honest claim. ## Training data **Source.** Per-decision play logs from a Klondike Solitaire client where the 31B `gemma-4-31b-it` teacher chose moves turn-by-turn, published as the [`chayuto/klondike-llm-decisions`](https://huggingface.co/datasets/chayuto/klondike-llm-decisions) dataset (CC-BY-4.0). Logs span multiple app builds and prompt-template versions. **Selection (this adapter, the "volume" arm).** The entire non-eval success pool: **6,859 decisions across 77 games (36% of which were won)**, with the 13 eval seeds held out. Split at the game level into 5,663 train / 531 validation / 665 test. At iters=1,000 with batch size 1 the model sees ~1,000 examples (~0.18 epoch), so this arm tests "more unique, diverse data at a fixed gradient budget", and it won: full volume beat a matched 2,500-row arm on wins (5 vs 3). **Known data-quality issues** the adapter inherits: - Mixed prompt-template formats across the corpus (v1.0 through v1.6); eval is on the v1.6 production template only, so cross-template generalization is a confound that the held-out and fresh-deck results only partially control for. - Lost-game trajectories in the corpus teach a resign capability. On the evidence above this is net useful (correct resigns on dead boards), but it is a behaviour the won-only `gate` arm does not have. - The teacher itself wins ~31% of games, so imitation targets are imperfect. ## Training procedure ```yaml model: mlx-community/Gemma4-E2B-IT-Text-int4 max_seq_length: 2048 batch_size: 1 num_layers: 16 grad_checkpoint: true learning_rate: 2.0e-4 iters: 1000 save_every: 250 val_batches: 25 lora_parameters: rank: 16 scale: 2.0 dropout: 0.05 keys: - self_attn.q_proj - self_attn.k_proj - self_attn.v_proj - self_attn.o_proj - mlp.gate_proj - mlp.up_proj - mlp.down_proj ``` Hyperparameters are identical to the project's other Gemma 4 E2B arms (gate, allsucc, v2, v5), so arm-to-arm differences are attributable to the corpus, not the optimiser. ## Usage ### Install ```bash # Apple Silicon, Python 3.12 venv recommended python3.12 -m venv venv && source venv/bin/activate pip install mlx mlx-lm huggingface-hub ``` ### Loading the base (important) The `Gemma4-E2B-IT-Text-int4` base needs a small `sanitize()` patch to load on current `mlx-lm` (it strips the alternating-attention `k_norm`/`k_proj`/`v_proj` keys that the loader does not yet implement). The 6-line patch used here is `gemma4_finetune/gemma4_text_patch.py` in the source repo ([github.com/chayuto/solitaire-analytics](https://github.com/chayuto/solitaire-analytics)); apply it (or use an `mlx-lm` version that has since merged the support) before loading. Peak memory to load the patched base is ~2.7 GB. ### Quick start ```python from huggingface_hub import snapshot_download from mlx_lm import load, generate # import gemma4_text_patch # apply the base-loading patch first (see above) adapter_path = snapshot_download( repo_id="chayuto/gemma-4-e2b-it-solitaire-advisor-lora", allow_patterns=["adapters.safetensors", "adapter_config.json"], ) model, tokenizer = load( "mlx-community/Gemma4-E2B-IT-Text-int4", adapter_path=adapter_path, ) solitaire_prompt = open("your_solitaire_prompt.txt").read() # the v1.6 harvester prompt wrapped = tokenizer.apply_chat_template( [{"role": "user", "content": solitaire_prompt}], tokenize=False, add_generation_prompt=True, ) print(generate(model, tokenizer, prompt=wrapped, max_tokens=512)) ``` The model emits a JSON object whose `final_decision.move_index` is a 0-based index into the prompt's `legalMoves` array (or `-1` to resign). Trust `move_index`, not the prose. The exact prompt renderer, the play harness (`tournament_A.py`, `play_deck_with_student.py`), and the adjudicator (`adjudicate_final_position.py`) that produced every number in this card are in the source repo. ## Limitations - **Small n, partly mitigated.** The headline in-distribution (13) and primary fresh (12) sets are small with high per-deck variance; read the paired deltas, not a single deck. The fresh-deck win-rate is now pooled over 48 decks (~33%, see Generalization), which tightens the estimate, but individual sub-samples still swing (the two small samples read ~42% before the 24-deck set pulled the pooled rate to ~33%). - **Easy-deck bias on the fresh set.** Only deals the solver cracked under a 200k-node cap were kept, so harder-but-winnable deals are under-represented. - **JSON discipline is imperfect.** The eval used a tiered parse-rescue (temp-0.3 retry); raw greedy output is not 100% valid JSON. For production, use constrained decoding or a JSON grammar at inference. This is a known gap, not yet fixed in the weights. - **Teacher ceiling.** Trained to imitate a teacher that wins ~31% of games; this is an advisor, not a solver, for imperfect-information draw-1 only. - **Greedy eval only.** Behaviour under temperature sampling is untested. - **Base-loading patch required.** See Usage; the int4 Gemma 4 E2B text base does not load on stock `mlx-lm` without the `sanitize()` patch. - **Apple-Silicon / MLX only.** CUDA/CPU inference via `transformers`/PEFT is not validated here. - **Loads only onto this exact base.** The LoRA was trained against `mlx-community/Gemma4-E2B-IT-Text-int4`; applying it to a different quant or the full-precision base is unvalidated. ## License The adapter is released under the **Gemma Terms of Use** (inherited from the base model). Use, redistribution, and modification require compliance with the [Gemma Prohibited Use Policy](https://ai.google.dev/gemma/prohibited_use_policy). The training and evaluation code in the source repository is **MIT**. The training data ([`chayuto/klondike-llm-decisions`](https://huggingface.co/datasets/chayuto/klondike-llm-decisions)) is **CC-BY-4.0**. ## Citation ```bibtex @misc{orapinpatipat2026solitaireadvisorgemma4, title = {Distilling a 31B Klondike Solitaire advisor into Gemma 4 E2B via MLX LoRA}, author = {Orapinpatipat, Chayut}, year = {2026}, month = jun, howpublished = {\url{https://huggingface.co/chayuto/gemma-4-e2b-it-solitaire-advisor-lora}}, note = {LoRA adapter; volume arm, iter-1000 checkpoint}, } ``` ## Acknowledgements - Base model `mlx-community/Gemma4-E2B-IT-Text-int4` from the [`mlx-community`](https://huggingface.co/mlx-community) team. - Training framework `mlx-lm` from Apple Machine Learning Research. - Teacher model `gemma-4-31b-it` from Google DeepMind. ## Project status This is the Gemma 4 E2B milestone of an ongoing project (`solitaire-analytics`): the strongest student so far, beating the untuned base under trusted full-game eval and generalizing to fresh, unseen decks (~33% of fresh winnable deals, about 4x base). It supersedes the [`gemma-3n`](https://huggingface.co/chayuto/gemma-3n-e2b-it-solitaire-advisor-lora) adapter as the project's lead student; the 3n repo remains as the v1 baseline. As of 2026-07-01 this adapter is the confirmed, shipped lead: two programs to improve on it have been run to conclusion and neither beat it, static-corpus engineering (closed 2026-06-23) and on-policy SFT on the student's own self-wins (closed 2026-07-01). The single untried lever to beat rather than match it is reward-weighted RFT/GRPO, which is parked. Standing improvement directions otherwise: constrained decoding for JSON robustness and resign calibration.