Bestemshe ResMLP — Neural Value & Policy Distillation

Authors: Ansar Zeinulla & Murat Manassov
Affiliation: Nazarbayev University, Kazakhstan
Paper: Strongly Solving Bestemshe and Benchmarking Neural Value Approximations against a 10GB Endgame Oracle
10GB Endgame Oracle Dataset: ansarzeinulla/bestemshe-tablebase
Code Repository: github.com/ansarzeinulla/Bestemshe

This repository hosts the weights and evaluation benchmarks for the dual-headed Residual Multilayer Perceptron (ResMLP) trained via continuous supervised distillation on over 2 billion board configurations directly generated from the 10GB Bestemshe Endgame Oracle.

The architecture predicts:

  1. Value Head ($V$): Probability distribution over {LOSS, DRAW, WIN} for the player to move.
  2. Policy Head ($\pi$): Action logits across the 5 playable pits (0..4).

Model Architecture Specifications

  • Input Layer: 10 board pits ($0..50$ stones) + 2 Kazans ($0..24$ captured stones) mapped via categorical embedding layers ($E_{\text{pits}} \in \mathbb{R}^{51 \times 32}$, $E_{\text{kazan}} \in \mathbb{R}^{13 \times 32}$).
  • Trunk: 8 Residual MLP blocks with LayerNorm and GELU activations ($\text{Hidden Dimension} = 1024$).
  • Heads: Linear Value Head ($\mathbb{R}^{1024} \to \mathbb{R}^3$) and Policy Head ($\mathbb{R}^{1024} \to \mathbb{R}^5$).
  • Parameters: ~18.5 Million parameters.
  • Training Horizon: 394,852 steps at batch size 32,768 ($>2 \times 10^9$ total training positions seen).

Benchmarking against the 10GB Endgame Oracle

1. Final Adversarial Match Benchmark (Step 394,852)

Evaluated against the ground-truth Tablebase on 2,000 full matches from 1,000 adversarial, forced-symmetric starting configurations (equal Kazans, equal stone counts per side):

Metric Score Context
Move-Level Optimal Rate 99.69% Matches Oracle choice on 226,232 / 226,933 moves executed
Move-0 Start WDL Accuracy 83.50% Raw value prediction on hard symmetric OOD starts
Game-Level Degradation 27.80% Games lost/drawn due to compounding micro-errors over 113-ply horizons

2. Complete Training Progression Across Checkpoints

Below is the aggregated evaluation statistics across 38 milestone evaluation steps during training:

Step Positions Evaluated Games WDL Accuracy Optimal Move Rate Incidents
1 1 1 0.0000 1.0000 0
6 10 5 0.2000 0.6000 2
26 100 20 0.5300 0.5900 10
76 1,000 100 0.6760 0.7120 50
176 1,000 100 0.7550 0.7460 50
476 50,000 1,000 0.8171 0.8254 500
976 100,000 2,000 0.8455 0.8887 1,000
1,976 200,000 3,000 0.8535 0.9190 1,500
3,976 300,000 5,000 0.8787 0.9385 5,000
8,976 500,000 8,000 0.9118 0.9547 4,000
18,976 10,000 100 0.9192 0.9662 100
28,976 10,000 100 0.9224 0.9679 100
38,976 100,000 10,000 0.9263 0.9672 10,000
48,976 1,000 100 0.9310 0.9700 100
98,976 10,000 1,000 0.9223 0.9683 1,000
143,976 10,000 1,000 0.9216 0.9681 1,000
358,976 100,000 100 0.9060 0.9602 100
394,852 (Final) 100,000 100 0.9294 0.9701 100

Standalone Python Inference

You can run single-position evaluation directly by downloading latest.pt and executing this PyTorch script:

pip install torch huggingface_hub
"""
Standalone single-position inference for Bestemshe ResMLP.
Requires: pip install torch huggingface_hub
"""
import torch
import torch.nn as nn
import torch.nn.functional as F
from huggingface_hub import hf_hub_download


class ResBlock(nn.Module):
    def __init__(self, w):
        super().__init__()
        self.norm = nn.LayerNorm(w)
        self.fc1 = nn.Linear(w, w)
        self.fc2 = nn.Linear(w, w)

    def forward(self, x):
        return x + self.fc2(F.gelu(self.fc1(self.norm(x))))


class BestemsheNet(nn.Module):
    """Embeddings -> Residual MLP -> Value head (loss/draw/win) + Policy head (5 moves)."""

    def __init__(self, width=1024, blocks=8, emb=32):
        super().__init__()
        self.pit_emb = nn.Embedding(51, emb)   # stones per pit: 0..50
        self.kaz_emb = nn.Embedding(13, emb)   # kazan // 2: 0..12
        self.inp = nn.Linear(12 * emb, width)
        self.body = nn.Sequential(*[ResBlock(width) for _ in range(blocks)])
        self.value_head = nn.Linear(width, 3)
        self.policy_head = 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)


def load_model(repo_id="ansarzeinulla/bestemshe-resmlp", filename="latest.pt", device="cpu"):
    path = hf_hub_download(repo_id=repo_id, filename=filename)
    ckpt = torch.load(path, map_location="cpu")
    args = ckpt["args"]
    model = BestemsheNet(args["width"], args["blocks"]).to(device).eval()
    
    # Strip torch.compile() prefix if present
    state_dict = {k.replace("_orig_mod.", ""): v for k, v in ckpt["model"].items()}
    model.load_state_dict(state_dict)
    return model, ckpt.get("step", 0)


@torch.no_grad()
def evaluate_position(model, pits, kazan_self, kazan_opp, device="cpu"):
    """
    pits: list of 10 ints — stones in each pit (0..4 = own side, 5..9 = opponent's)
    kazan_self, kazan_opp: int — each player's kazan (captured stones), 0..24 (even)

    Returns WDL probabilities for the side to move + raw 5-way move-policy logits
    (index 0..4, own pits only).
    """
    pits_t = torch.tensor([pits], dtype=torch.long, device=device)
    kaz_t = torch.tensor([[kazan_self // 2, kazan_opp // 2]], dtype=torch.long, device=device)

    value_logits, policy_logits = model(pits_t, kaz_t)
    probs = torch.softmax(value_logits[0], dim=0).cpu().numpy()

    return {
        "p_loss": float(probs[0]),
        "p_draw": float(probs[1]),
        "p_win": float(probs[2]),
        "predicted_class": int(value_logits[0].argmax()),  # 0=loss, 1=draw, 2=win
        "policy_logits": policy_logits[0].cpu().numpy().tolist(),
    }


if __name__ == "__main__":
    model, step = load_model()
    print(f"Loaded checkpoint at training step: {step}")

    # Standard starting board: 5 stones in every pit, kazans empty
    result = evaluate_position(model, pits=[5] * 10, kazan_self=0, kazan_opp=0)
    print("Standard Starting Position Evaluation:")
    print(f"  P(Loss): {result['p_loss']:.4f}")
    print(f"  P(Draw): {result['p_draw']:.4f}")
    print(f"  P(Win) : {result['p_win']:.4f}")
    print(f"  Policy Logits (Pits 0..4): {result['policy_logits']}")

Citation

@misc{zeinulla2026bestemshe_model,
  title={Bestemshe ResMLP: Neural Value and Policy Distillation},
  author={Zeinulla, Ansar and Manassov, Murat},
  year={2026},
  publisher={Hugging Face Models},
  howpublished={\url{https://huggingface.co/ansarzeinulla/bestemshe-resmlp}}
}
Downloads last month

-

Downloads are not tracked for this model. How to track
Video Preview
loading

Dataset used to train ansarzeinulla/bestemshe-resmlp

Space using ansarzeinulla/bestemshe-resmlp 1

Evaluation results