Bestemshe ResMLP β€” value and policy network distilled from an exact oracle

Authors: Ansar Zeinulla & Murat Manassov Β· Nazarbayev University Oracle (tablebase): ansarzeinulla/bestemshe-tablebase (8.3 GiB, 1.26Γ—10ΒΉΒΉ positions) Code: github.com/ansarzeinulla/Bestemshe

A dual-headed residual MLP trained by supervised distillation on positions labelled by the exact Bestemshe tablebase. It predicts, for the side to move:

  • Value: loss / draw / win.
  • Policy: logits over the 5 own pits; a move is a target if it preserves the game-theoretic value.

Files

File Content Size
model.safetensors + config.json weights only (recommended) 69 MB
latest.pt full training checkpoint: weights, optimizer state, args, step 207 MB

Both hold the same weights: outputs are identical.

Architecture

  • Input: 10 pits (0–50 stones) and 2 kazans (stored as kazan/2, 0–12) through categorical embeddings of width 32.
  • Trunk: linear 384 β†’ 1024, then 8 residual blocks (LayerNorm β†’ Linear β†’ GELU β†’ Linear), width 1024.
  • Heads: value 1024 β†’ 3, policy 1024 β†’ 5.
  • Parameters: 17,214,472.

Training

  • 394,852 optimizer steps at batch 32,768, i.e. 12.9 billion training samples.
  • Samples came from shards of oracle-labelled positions, regenerated throughout training (continual distillation).
  • Loss: cross-entropy on the value head, binary cross-entropy on the policy head.

Results

Against the oracle (2,000 games)

  • 1,000 symmetric start positions (equal kazans, equal stones), each played twice with colours swapped.
  • The model moves by a 3-ply minimax search over its value head; the oracle plays a value-preserving move.
  • Games are stopped at 400 plies and scored as draws. The oracle stores win/draw/loss only, so neither side is pushed to convert a win.
Metric Value
Value-preserving moves 99.69% (226,232 of 226,933; 701 value-losing moves)
Games ending below the theoretical result 556 of 2,000 (27.8%) = 556 of the 1,036 games the model started won or drawn (53.7%)
Average model moves per game 113
Random-move baseline (other starts, 100 games) 97.48% value-preserving moves

Deeper search helps:

  • 5-ply minimax: 21 of 51 degradable games below theory;
  • 5-ply with an averaged backup: 20 of 206.

A same-start comparison of all move-selection methods is being run with the updated evaluation/vs_god.py.

Value head (4,000 symmetric positions)

Metric Value
Accuracy (argmax WDL) 85.7%
Expected calibration error (15 bins) 0.0098
Mean confidence when wrong / right 0.673 / 0.891
Draw recall 0 (146 true draws; draw is never the argmax)

A telling example: on the initial position the value head gives the first player win 0.89 / draw 0.05 / loss 0.06. The tablebase says it is a loss (the second player wins).

Errors concentrate where a position's latent neighbours carry other labels: 0% error in the most homogeneous 30% of positions, 80% in the most mixed decile. Script: analysis/latent_calibration.py.

During training

A cheap greedy 1-ply check on uniformly sampled positions, run at each checkpoint, rose to 97.0% value-preserving moves and 92.9% value accuracy at the final step. Full history: the eval_results_*.json files of the training run.

Usage

pip install torch safetensors huggingface_hub
import json, torch, torch.nn as nn, torch.nn.functional as F
from huggingface_hub import hf_hub_download
from safetensors.torch import load_file

class ResBlock(nn.Module):
    def __init__(self, w):
        super().__init__()
        self.norm, self.fc1, self.fc2 = nn.LayerNorm(w), nn.Linear(w, w), nn.Linear(w, w)
    def forward(self, x):
        return x + self.fc2(F.gelu(self.fc1(self.norm(x))))

class BestemsheNet(nn.Module):
    def __init__(self, width=1024, blocks=8, emb=32):
        super().__init__()
        self.pit_emb, self.kaz_emb = nn.Embedding(51, emb), nn.Embedding(13, emb)
        self.inp = nn.Linear(12 * emb, width)
        self.body = nn.Sequential(*[ResBlock(width) for _ in range(blocks)])
        self.value_head, self.policy_head = nn.Linear(width, 3), nn.Linear(width, 5)
    def forward(self, pits, kaz):
        x = torch.cat([self.pit_emb(pits).flatten(1), self.kaz_emb(kaz).flatten(1)], dim=1)
        h = self.body(F.gelu(self.inp(x)))
        return self.value_head(h), self.policy_head(h)

repo = "ansarzeinulla/bestemshe-resmlp"
cfg = json.load(open(hf_hub_download(repo, "config.json")))
model = BestemsheNet(cfg["width"], cfg["blocks"]).eval()
model.load_state_dict(load_file(hf_hub_download(repo, "model.safetensors")))

# side to move: pits[0:5] own, pits[5:10] opponent; kazans are even, passed as kazan // 2
pits = torch.tensor([[5] * 10]); kaz = torch.tensor([[0, 0]])
with torch.no_grad():
    value, policy = model(pits, kaz)
print(dict(zip(["loss", "draw", "win"], torch.softmax(value, 1)[0].tolist())))

Citation

@misc{zeinulla2026bestemshe_model,
  title        = {Bestemshe ResMLP: Value and Policy Distillation from an Exact Oracle},
  author       = {Zeinulla, Ansar and Manassov, Murat},
  year         = {2026},
  publisher    = {Hugging Face},
  howpublished = {\url{https://huggingface.co/ansarzeinulla/bestemshe-resmlp}}
}
Downloads last month
6
Safetensors
Model size
17.2M params
Tensor type
F32
Β·
Video Preview
loading

Dataset used to train ansarzeinulla/bestemshe-resmlp

Space using ansarzeinulla/bestemshe-resmlp 1

Evaluation results

  • Value-preserving move rate on Bestemshe tablebase (1,000 symmetric start positions)
    self-reported
    99.690