ART-MLX
Verified Test Report

Tic Tac Toe GRPO Training
on Apple Silicon

End-to-end verification of the examples/mlx_tictactoe.py GRPO example: what broke against the current mlx-lm API, the fixes applied, and live training logs proving real gradient updates on an Apple M1 GPU.

✓ quicktest passed ✓ 15/15 training steps MLX 0.32.0 mlx-lm 0.31.3 Python 3.13 Apple M1 GPU

Tested 2026-07-12 · PR #1 — fix/mlx-lm-api-qlora-grpo

01Results at a glance

A fresh 90-game training run (15 steps × 6 games) with initial and final evaluation over 30 games each.

Initial win rate
53.3%
Final win rate
56.7%
Change
+3.3%
Trainable LoRA params
1.47M
Avg step time
~6.1s
Model
Qwen2.5-0.5B-4bit

02What went wrong

The example was written against an older mlx-lm release. On current versions it failed at three points — the first two are API drift, the third is a fundamental limitation of training a quantized model.

SymptomRoot causeType
generate_step() got an unexpected keyword argument 'temp' The temp= argument was removed from generate(); sampling params now come from a sampler object. API
module 'mlx.core' has no attribute 'log_softmax' log_softmax moved to mlx.nn. API
[QuantizedMatmul::vjp] no gradient wrt the quantized weights The example loads a 4-bit quantized model. MLX cannot backprop through quantized weights, so full-model gradient updates are impossible. Core

03How it was fixed

Three targeted changes in examples/mlx_tictactoe.py — the key one converts training to a QLoRA scheme so gradients flow through small trainable adapters instead of the frozen quantized base.

ChangeDetail
Sampler API temp=0.8sampler=make_sampler(temp=0.8) fixed
log-softmax mx.log_softmaxnn.log_softmax fixed
QLoRA training model.freeze() + linear_to_lora_layers(...), and mx.value_and_grad(fn)(model)nn.value_and_grad(model, fn)() so only the ~1.47M LoRA params receive gradients. fixed
Why QLoRA? Freezing the 4-bit base and attaching LoRA adapters keeps memory tiny while making the model trainable — gradients now flow only through the adapter weights, which is exactly what the GRPO optimizer updates.

04Environment check — mlx_quicktest.py

Confirms MLX, the Metal GPU, model loading, and generation before any training.

==================================================
ART-MLX Environment Check
==================================================

 MLX available (version: 0.32.0)
 Device: Device(gpu, 0)
 MLX-LM available

Loading mlx-community/Qwen2.5-0.5B-Instruct-4bit...
 Model loaded: mlx-community/Qwen2.5-0.5B-Instruct-4bit

Testing generation with prompt: 'What is 2 + 2?'
 Generation working
  Response: The answer is 4.
 ART MLX backend importable

==================================================
✓ Ready for GRPO training!
==================================================

05Training run — mlx_tictactoe.py

Full GRPO loop: load → evaluate → train (15 steps × 6 games) → re-evaluate. Loss changes and win rate fluctuates between steps, confirming real weight updates.

============================================================
ART-MLX Tic Tac Toe — GRPO Training
============================================================

Loading: mlx-community/Qwen2.5-0.5B-Instruct-4bit
 Loaded in 0.5s
 Device: Device(gpu, 0)
 LoRA adapters applied (1,466,368 trainable params)

GRPOTrainer initialized with lr=1e-05
Initial evaluation (30 games)...
  Win rate: 53.3%   W/L/D: 16/9/5

Training: 15 steps × 6 games = 90 total games
------------------------------------------------------------
Step  1/15: win_rate=83% loss= 0.3247 updates=26 (5.9s)
Step  2/15: win_rate=33% loss=-3.1994 updates=28 (6.6s)
Step  3/15: win_rate=83% loss= 3.1662 updates=27 (6.4s)
Step  4/15: win_rate=50% loss= 0.0065 updates=24 (5.8s)
Step  5/15: win_rate=50% loss= 0.6881 updates=28 (6.7s)
Step  6/15: win_rate=67% loss= 2.8066 updates=24 (5.8s)
Step  7/15: win_rate=50% loss= 0.6254 updates=28 (6.7s)
Step  8/15: win_rate=33% loss= 1.4201 updates=26 (6.4s)
Step  9/15: win_rate=33% loss= 1.0067 updates=22 (5.3s)
Step 10/15: win_rate=50% loss= 0.9868 updates=27 (6.5s)
Step 11/15: win_rate=83% loss=-0.8903 updates=26 (6.3s)
Step 12/15: win_rate=50% loss= 1.2905 updates=25 (6.0s)
Step 13/15: win_rate=67% loss= 2.2286 updates=23 (5.5s)
Step 14/15: win_rate=83% loss= 0.0288 updates=24 (5.8s)
Step 15/15: win_rate=50% loss= 0.4528 updates=26 (6.2s)
------------------------------------------------------------

Final evaluation (30 games)...
  Win rate: 56.7%   W/L/D: 17/10/3

============================================================
RESULTS
============================================================
Initial win rate: 53.3%
Final win rate:   56.7%
Change:           +3.3%

06Reproduce it

Note on the +3.3% result. The training mechanics are correct and verified; the size of the win-rate gain varies per run and with hyperparameters (learning rate, number of steps, and how many layers get LoRA adapters). This report documents that the loop trains end-to-end with real gradient updates, not a tuned final accuracy.