Instructions to use lnkz/laya-go-tic-tac-toe with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Laya
How to use lnkz/laya-go-tic-tac-toe with Laya:
# No code snippets available yet for this library. # To use this model, check the repository files and the library's documentation. # Want to help? PRs adding snippets are welcome at: # https://github.com/huggingface/huggingface.js
- MLX
How to use lnkz/laya-go-tic-tac-toe with MLX:
# Download the model from the Hub pip install huggingface_hub[hf_xet] hf download lnkz/laya-go-tic-tac-toe --local-dir laya-go-tic-tac-toe
- Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- LM Studio
- Atomic Chat
laya-go-tic-tac-toe
A 1.2 MB Laya-format decision model that plays noughts and crosses (tic-tac-toe). It was created and trained entirely in Go and is served unchanged by mlx-serve.
Published by lnkz.
Why
Laya is a decision model: you give it a state as text and a set of questions, and it returns a probability for each option. The published model has about 400M parameters and is trained with Python.
This project wanted the smallest Laya-compatible model that still plays a game well, built with no Python at all:
- Created in Go: the model folder, safetensors, configs and tokenizer, in the
laya-mlxformat that mlx-serve reads. - Trained in Go: hand-written backpropagation and Adam on CPU. The Go loss matches Laya's Python training code to 6 decimal places, and every gradient is checked against finite differences.
- Served unchanged by mlx-serve: mlx-serve's answers match the Go forward pass to within 0.0014 on this model.
Noughts and crosses was the test case because it can be checked exactly: a perfect-play oracle knows the best moves and the result of every one of the 4,520 positions where a move is still to be made.
Results
Measured through mlx-serve over all 4,520 positions. "Decisive" positions (3,191) are those where some move changes the result.
| All positions | Decisive positions | |
|---|---|---|
| Best move | 96.7% | 95.4% |
| Playable but not best | 1.8% | 2.5% |
| Blunder (turns a draw or win into a worse result) | 1.5% | 2.1% |
| Winner called right | 98.9% | 98.9% |
Against the perfect-play oracle, it drew 19 of 20 games (all 10 as X, 9 of 10 as O). Perfect play draws every game, so this model is not yet perfect: it still makes 148 non-best moves across all positions.
For scale:
| Model | Parameters | Size | Best moves |
|---|---|---|---|
| laya-go-tic-tac-toe | 620,038 | 1.2 MB | 96.7% |
| Laya cut down to 1 layer and fine-tuned in Python | 39.1M | 78 MB | 67.9% |
What it is
| Format | laya-mlx (safetensors, float16) |
| Encoder | ModernBERT-style, 2 layers, width 128, 2 heads, MLP 256, full attention |
| Decision head | 1 layer, Laya's scorer and act head |
| Tokenizer | Laya's tokenizer trimmed to the 320 tokens this game's text uses |
| Parameters | 620,038 |
rl_agent_config.json keeps Laya's field "encoder": "answerdotai/ModernBERT-large" because the format expects it. The real encoder shape is in encoder/config.json.
Use
Copy the folder into mlx-serve's models folder, for example ~/.mlx-serve/models/lnkz/laya-go-tic-tac-toe/, then rescan models in mlx-serve.
Quick check
X has two in the top row, so X wins by playing A3:
A: X X _
B: O O _
C: _ _ _
Ask the model for X's move and the winner:
curl -s http://localhost:11234/v1/decisions -H 'content-type: application/json' -d '{
"model": "lnkz/laya-go-tic-tac-toe",
"state": "Noughts and crosses. X to move.\nA: X X _\nB: O O _\nC: _ _ _",
"questions": {
"Best move": {
"type": "choice",
"instructions": "Which square should X play next?",
"criteria": ["A3", "B3", "C1", "C2", "C3"]
},
"Who will win": {
"type": "choice",
"instructions": "Who will win this game?",
"criteria": ["X", "O", "Draw"]
}
}
}'
Each answer has a choice and a probability for every option. Expect:
Best move: A3 (1.00)
Who will win: X (1.00)
example.go sends the same request and prints those two lines. It uses only the Go standard library:
go run example.go
Asking about other positions
The model was trained on exactly this prompt layout; other wording will work less well. The board is written as rows A to C and columns 1 to 3, with X, O and _ for empty. List only the empty squares as options. "Who will win" assumes perfect play from both sides.
Training
The training data is every position where a move is still to be made, with targets from the perfect-play oracle: equal probability over the best moves for Best move, and probability 1 on the true result for Who will win. It was trained in three runs, each continuing from the last, on a Mac mini CPU:
| Run | From | Epochs | Learning rate | Loss | Best moves | Time |
|---|---|---|---|---|---|---|
| 1 | random | 30 | 3e-4 | 1.648 β 0.877 | 73.3% | 41 min |
| 2 | run 1 | 60 | 1e-4 | 0.877 β 0.577 | 90.8% | 84 min |
| 3 | run 2 | 60 | 1e-4 | 0.577 β 0.409 | 96.7% | 85 min |
Every run used batch 16, Adam, a 5% warm-up, cosine decay and gradient clipping at norm 1. training.json holds the last run's settings and loss for every epoch. Restarting the learning-rate schedule from the previous run's weights helped a lot; at the end of run 3 the model was still improving.
Limits
- Not yet perfect play: 148 non-best moves and 48 wrong winner calls remain.
- Near-ties: mlx-serve computes in float16, so when two options score almost the same, it can pick a different one from the Go float32 forward pass.
- Exact prompts only: it only knows the prompt layout above.
License and credit
Apache-2.0. The model format, architecture and tokenizer come from Laya by ConvAI Innovations and its MLX conversion aac6fef/laya-mlx, both Apache-2.0. The weights were trained from random initialisation and contain nothing from Laya's trained weights.
Quantized