Why this matters
Table with columns: Old mixed (v1), This (v2 sized) | Old mixed (v1) | This (v2 sized) |
|---|
| Raise output | "raise" (1 token, no sizing) | "raise 14.0" (proper sizing) |
| Strict raise sizing on PE 60-sample eval | 0/60 (0.0%) | 41/60 (68.3%) |
| Paper 11k EM | 89.86% | (within ±2 expected) |
| PE 200 AA | 84.0% | 84.0% |
The action-accuracy headline is unchanged because the eval scoring folds raise/bet/all-in into a single "aggressive" kind. The sizing is what production deployment cares about.
What changed in the data
gen-pe-format-dataset.ts used row.correctDecision (action kind only) as the label. The fix is post-hoc — scripts/fix_pe_raise_sizing.py reads each prompt, extracts BB amount from the raise pct=N(Mbb, ~M) / raise pct=N(Bet M, ~M) line, and rewrites the label.
After fix:
- 3804 raise/bet records → 0 bare
"raise" remaining
- 109 distinct raise sizes (top:
raise 14.0, raise 13.0, raise 25.0, raise 2.5, raise 10.0)
Sample outputs
prompt: "...preflop SB facing UTG raise 2.5..."
v1 (old): "raise" ← parser sees no number, default_pct: None
v2 (this): "raise 14.5" ← parser gets 14.5 BB, action complete
Usage
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
base = AutoModelForCausalLM.from_pretrained(
"Qwen/Qwen3-14B",
torch_dtype=torch.bfloat16,
device_map="auto",
)
model = PeftModel.from_pretrained(base, "ianlee1996/pokerbench-qwen3-14b-lora-mixed-v2")
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-14B")
system_prompt = (
"You are a specialist in playing 6-handed No Limit Texas Holdem. "
"Output ONLY the optimal action with no explanation. "
"Valid formats: 'fold', 'check', 'call', 'bet N', 'raise N', 'all-in'."
)
Training
- Base:
Qwen/Qwen3-14B
- Stage 1 LoRA:
04_qwen3_14b_lora_full_data_1epoch/checkpoint-4375 (60k+500k paper-format SFT, 1 epoch, EM 90.07%)
- Stage 2 LoRA (this): 250 steps on 50/50 mix of paper + production-PE-fixed jsonl
- LoRA: r=32, alpha=64, dropout=0.05, target=all-linear
- Optimizer: paged_adamw_8bit, LR 5e-5, cosine schedule, warmup 0.03
- Batch: effective 128 (4 per device × 32 grad accumulation)
- Hardware: 1× NVIDIA RTX PRO 6000 Blackwell (96 GB)
- Time: ~1h 38min
Reproducibility
Full pipeline at https://github.com/IanLiYi1996/PokerBench:
# 1. Fix the existing PE jsonl in-place
.venv/bin/python -m scripts.fix_pe_raise_sizing \
data/pe_format/mixed_paper_pe_50_50.jsonl \
data/pe_format/mixed_paper_pe_50_50_fixed.jsonl
# 2. Re-train the SFT mixed checkpoint on the fixed data
.venv/bin/python -m scripts.train --config configs/experiments/09_qwen3_14b_lora_mixed_v2_sized.yaml
Citation
@inproceedings{zhuang2025pokerbench,
title={PokerBench: Training Large Language Models to become Professional Poker Players},
author={Zhuang, Richard and Gupta, Akshat and Yang, Richard and Rahane, Aniket and Li, Zhengyu and Anumanchipalli, Gopala},
booktitle={Proceedings of the AAAI Conference on Artificial Intelligence},
year={2025},
url={https://arxiv.org/abs/2501.08328}
}
License
Apache-2.0, matching Qwen/Qwen3-14B and the PokerBench dataset.