2026-05-06 18:07:21 +08:00
2026-04-22 10:11:42 +08:00
ops
2026-04-25 00:33:33 +08:00
2026-05-06 17:36:51 +08:00
2026-04-20 22:35:40 +08:00
ops
2026-04-25 00:33:33 +08:00
ops
2026-04-25 00:33:33 +08:00
2026-05-06 17:36:51 +08:00
2026-04-21 15:52:59 +08:00
2026-05-06 17:36:51 +08:00
2026-04-20 20:25:35 +08:00
2026-05-06 17:46:46 +08:00
ops
2026-04-25 00:33:33 +08:00
2026-05-06 18:07:21 +08:00
2026-05-06 18:07:21 +08:00
2026-04-20 20:25:35 +08:00

HUNL Poker AI — Deep CFR 训练指南

2 人 Heads-Up No-Limit Texas Hold'em, 基于 OpenSpiel + Deep CFR (External Sampling MCCFR)

对齐 Botzone 比赛规则: 初始筹码 20000, SB=50, BB=100, 筹码/100 = 得分


目录

  1. 训练总览
  2. Phase 1: Card Model 训练
  3. Phase 2: CFR 策略网络训练
  4. 硬件配置与参数调节
  5. 训练效果评估
  6. 文件结构与数据流
  7. 常见问题

1. 训练总览

训练分为两个阶段,必须按顺序执行:

Phase 1: Card Model (胜率预测器)          Phase 2: CFR 策略网络 (MCCFR 自对弈)
┌─────────────────────────┐              ┌─────────────────────────┐
│ 生成 200万~1亿 MC 样本  │              │ 加载 Card Model (冻结)  │
│ 训练 CardModel 20-100ep │ ──权重──→    │ 50000 iter 自对弈       │
│ 保存 best_card_model.pt │              │ 保存 cfr_net_checkpoint │
└─────────────────────────┘              └─────────────────────────┘
    GPU 利用率: 低 (<5%)                     GPU: 推理 (单样本) + 训练 (大 batch)
    CPU 利用率: 数据生成时高                  CPU: 单核遍历博弈树 (瓶颈)
    内存: 200万~0.5GB / 1亿~26GB             内存: Buffer 占主要
    磁盘: 200万~500MB / 1亿~20GB             磁盘: ~1MB 模型文件

执行命令:

cd /home/e2hang/kilo/codes/poker

# Phase 1
python train_card_model.py

# Phase 2 (确认 Phase 1 完成后)
python mccfr_trainer.py

2. Phase 1: Card Model 训练

2.1 Card Model 是什么

Card Model 接收手牌 + 公共牌,输出:

  • 胜率标量 (0~1): 当前牌面赢的概率
  • 胜率直方图 (50 bin): 胜率的概率分布(用于 CFR 网络的 50 维输入)

它是一个小型网络,参数量 ~45K训练很快。

2.2 训练流程

步骤 操作 资源消耗 (200万) 资源消耗 (1亿)
1. 数据生成 样本 × 1000 次 MC rollout 8 核 CPU, 内存 ~2GB 8 核 CPU 全力跑数天, 内存峰值 ~35-40GB
2. 数据缓存 保存为 .npz 文件 磁盘 ~500MB 磁盘 ~20GB
3. 加载数据 从 .npz 读入内存 <1s ~2-5 分钟
4. 模型训练 20-100 epoch, batch=16384 GPU <1GB, 每 epoch ~5-10s GPU <2GB, 每 epoch ~数小时
5. 保存模型 card_model/data/best_card_model.pt 按验证集最小 loss 保存 同左

2.3 训练参数详解

参数 当前值 含义 调节建议
NUM_TRAIN_SAMPLES 2,000,000 训练样本数 越多越准,但生成耗时线性增长
NUM_VAL_SAMPLES 50,000 验证样本数 5 万已足够评估
NUM_ROLLOUTS 1,000 每样本 MC 模拟次数 1000 是精度/速度的平衡点,不宜低于 500
NUM_BINS 50 直方图 bin 数 50 是常用值,更细(100)收益不大
BATCH_SIZE 4,096 训练 batch size 模型极小,可以开到 16384+
LEARNING_RATE 5e-4 AdamW 学习率 配合 CosineAnnealing 衰减到 1e-5
WEIGHT_DECAY 1e-4 L2 正则化 标准值
NUM_EPOCHS 100 训练轮数 100 轮足够收敛
LAMBDA_MSE 0.1 MSE loss 权重 总 loss = EMD + 0.1×MSEEMD 为主
NUM_WORKERS 8 DataLoader 工作进程数 设为 CPU 核心数的一半左右
EMBEDDING_DIM 32 牌面嵌入维度 小模型足够
MLP_HIDDEN [128, 128, 64] MLP 隐藏层 小模型足够

2.4 网络结构

Input: hole_cards [2] + board_cards [5] (整数 ID)
  │
  ├── Embedding(53, 32, padding_idx=52)    # 53 个 token, 32 维
  │     ├── hole_emb = sum(embed(hole))     # [32]
  │     └── board_emb = sum(embed(board))   # [32]
  │
  ├── concat → [64]
  │
  ├── Backbone MLP:
  │     Linear(64,128) → ReLU → LayerNorm
  │     Linear(128,128) → ReLU → LayerNorm
  │     Linear(128,64) → ReLU → LayerNorm
  │
  ├── equity_head:    Linear(64,32) → ReLU → Linear(32,1) → Sigmoid
  └── histogram_head: Linear(64,64) → ReLU → Linear(64,50) → Softmax

参数量: ~45,000

2.5 资源估算 (128GB RAM / 96GB VRAM)

当前配置 (200 万样本)

资源 估算用量 说明
CPU 8 核满载 (数据生成阶段) 多进程并行生成 MC 样本
内存 ~4-8 GB 数据集加载 + DataLoader 缓冲
GPU 显存 < 1 GB 模型极小batch=4096 也不到 1GB
磁盘 ~500 MB train_data.npz + val_data.npz
GPU 利用率 < 5% 计算量极小GPU 几乎在空转

1 亿样本配置 (NUM_TRAIN_SAMPLES=100,000,000)

每样本存储: x_hole(2×int64=16B) + x_board(5×int64=40B) + y_equity(1×float32=4B) + y_histogram(50×float32=200B) = 260 bytes/样本

资源 估算用量 说明
CPU 8 核满载数天 1亿×1000 rollout = 1000亿次 MC 模拟,这是巨大的计算量
内存 ~26-30 GB NumPy 数组加载: 1亿×260B ≈ 26GB加 DataLoader 缓冲
内存 (生成期峰值) ~35-40 GB 8 进程并行,每进程缓存 chunk 后合并
GPU 显存 < 2 GB 模型极小,训练显存与数据量无关,只与 batch size 相关
磁盘 (.npz) ~15-22 GB 1亿×260B ≈ 26GB 原始npz 压缩率约 60-85%
训练时长 数小时/epoch 1亿样本 / batch=16384 = ~6104 步/epoch
总 epoch 训练 20-30 epoch 足够 数据量极大,少量 epoch 即可充分学习

1 亿样本可行性分析 (128GB RAM / 96GB VRAM):

  • 内存: 26GB 数据 + 系统/PyTorch 开销 ≈ 35-40GB远低于 128GB
  • GPU 显存: 训练时 <2GB远低于 96GB
  • 磁盘: ~20GB npz 文件,需确保磁盘空间充足
  • CPU 生成时间: 这是真正的瓶颈。200万样本×1000 rollout 需要数小时1亿样本是 50× 数据量,预计需要 数天 连续运行 ⚠️
  • DataLoader: 1亿样本从磁盘加载到内存需 ~2-5 分钟,之后训练不受 I/O 影响

建议: 1 亿样本在硬件上完全可行,但数据生成耗时极长。建议先用 2000万-5000万 样本验证训练效果,确认收益后再决定是否扩展到 1 亿。如果 GPU 空闲,可在生成数据的同时用已有数据开始训练。


3. Phase 2: CFR 策略网络训练

3.1 MCCFR 训练流程

每个 iteration 分两个阶段:

Iteration i:
  ┌────────────────────────────────────────────────────────────────┐
  │ 阶段 A: 数据生成 (CPU 瓶颈, 单线程)                           │
  │                                                                │
  │   for game_idx in range(GAMES_PER_ITER):  # 200 局             │
  │     traversing_player = game_idx % 2   # 交替遍历 P0/P1        │
  │     state = game.new_initial_state()                            │
  │     utility = traverse(state, traversing_player, ...)           │
  │       │                                                        │
  │       ├── 终止节点 → 返回收益                                   │
  │       ├── 机会节点 → 采样一个结果,递归                          │
  │       └── 玩家节点:                                            │
  │             ├── CardModel 推理 → 50 维直方图 (frozen, eval)     │
  │             ├── CFRNet 推理 → 6 维策略 (eval)                   │
  │             ├── 对手节点: 按策略采样动作,递归                   │
  │             └── 遍历者节点: 对每个合法动作:                      │
  │                   clone state → 执行动作 → 递归 → 收益          │
  │                   计算 regret = (动作收益 - 加权收益) / 20000   │
  │                   buffer.add(info_state, legal_mask, regrets, strategy) │
  └────────────────────────────────────────────────────────────────┘
                              │
                              ▼
  ┌────────────────────────────────────────────────────────────────┐
  │ 阶段 B: 网络训练 (GPU, 极快)                                   │
  │                                                                │
  │   for step in range(TRAIN_STEPS_PER_ITER):  # 50 步            │
  │     batch = buffer.sample(TRAIN_BATCH_SIZE)  # 32768 条        │
  │     card_features = batch[:, :50]                               │
  │     env_features  = batch[:, 50:]                               │
  │     pred_regrets, pred_logits = cfr_net(card, env)              │
  │     regret_loss = MSE(pred_regrets, target_regrets) * mask      │
  │     policy_loss = MSE(softmax(masked_logits), target_strat) * mask │
  │     total_loss = regret_loss + policy_loss                      │
  │     backward + clip_grad_norm(1.0) + optimizer.step()          │
  └────────────────────────────────────────────────────────────────┘

3.2 CFR 网络结构

Input: card_features [50] + env_features [5] = info_state [55]
  │
  ├── concat → [55]
  │
  ├── Shared Backbone MLP:
  │     Linear(55, 256) → ReLU
  │     Linear(256, 256) → ReLU
  │     Linear(256, 128) → ReLU
  │
  ├── regret_head:  Linear(128, 6)    # 无激活regret 可为负
  └── policy_head:  Linear(128, 6)    # 后续 Softmax + legal_mask

参数量: ~114,600

Regret Matching (get_strategy):
  positive_regret = ReLU(pred_regrets) * legal_mask
  if sum > 0: strategy = positive_regret / sum
  else:       strategy = uniform(legal_actions)

3.3 训练参数详解

参数 当前值 含义 详细说明
NUM_ITERATIONS 50,000 总迭代次数 每次迭代 = 数据生成 + 网络训练。5 万次是长期训练目标
GAMES_PER_ITER 200 每迭代自对弈局数 单核跑的设太大会让网络更新不频繁。200 是平衡点
BUFFER_MAX_SIZE 10,000,000 Buffer 声明容量 注意: 实际实例化用了 1,000,000需改为使用此常量
MIN_BUFFER_SIZE_FOR_TRAIN 100,000 最小训练数据量 Buffer 积累到 10 万条才开始网络更新,避免早期噪声
TRAIN_BATCH_SIZE 32,768 训练 batch size 大 batch 让策略收敛平滑,减少震荡
TRAIN_STEPS_PER_ITER 50 每迭代训练步数 50 步在大 batch 下已经过大量样本
LEARNING_RATE 5e-4 AdamW 学习率 大 batch 配稍低学习率,求稳
WEIGHT_DECAY 1e-4 L2 正则化 标准值
STACK_NORMALIZE 20,000.0 筹码归一化因子 对齐 Botzone 初始筹码regret 值域 ~[-1, +1]
STREET_NORMALIZE 3.0 街道归一化因子 Preflop=0/3, Flop=1/3, Turn=2/3, River=3/3
CARD_MODEL_CHECKPOINT None Card Model 权重路径 务必设置,否则 card_features 是随机噪声

3.4 每步迭代的资源消耗

阶段 A: 数据生成 (瓶颈)

资源 消耗 说明
CPU 1 核 100% traverse() 单线程递归遍历博弈树
GPU 推理 间歇性,极低 每个玩家节点: CardModel 1次 + CFRNet 1次 (batch=1)
GPU 显存 < 1 GB 两个模型常驻,推理 batch=1
内存增长 ~10,000-40,000 条/迭代 每条 ~292 字节 (55+6+6+6 float32)
每迭代耗时 主要取决于 CPU 单局遍历 ~几十ms200 局 ~几秒

每迭代生成数据量估算:

  • 每局 HUNL 平均 ~50-200 个信息集 (取决于对局长度和动作空间)
  • 200 局 × 100 信息集 = ~20,000 条新样本/迭代
  • 100 次迭代后 Buffer 约 200 万条 → 开始 FIFO 淘汰 (当 max_size=1M)

阶段 B: 网络训练

资源 消耗 说明
GPU 显存 < 2 GB batch=32768, 3 层 MLP, ~115K 参数
GPU 计算 极快 50 步 × 115K 参数网络,<1 秒完成
CPU 空闲 等待 GPU

3.5 累计资源消耗

指标 1,000 次迭代 10,000 次迭代 50,000 次迭代
Buffer 最大占用 (1M) ~300 MB ~300 MB (FIFO 满) ~300 MB (FIFO 满)
Buffer 最大占用 (10M) ~3 GB ~3 GB (FIFO 满) ~3 GB (FIFO 满)
总生成样本 ~2 千万 ~2 亿 ~10 亿
模型文件 ~1 MB ~1 MB ~1 MB
GPU 显存峰值 < 2 GB < 2 GB < 2 GB
训练样本 (累计) ~16 亿 ~163 亿 ~819 亿

4. 硬件配置与参数调节

4.1 你的硬件: 128GB RAM + 96GB VRAM

核心瓶颈: CPU 单线程博弈树遍历

GPU 和内存资源严重过剩。CFR 网络只有 ~115K 参数,即使 batch=65536 也用不到 4GB 显存。Buffer 即使开到 10M 也只占 ~3GB 内存。真正限制训练速度的是 traverse() 的单线程 Python + OpenSpiel 递归。

4.2 针对你硬件的参数调节建议

Buffer 大小: 开到 10M

问题: 当前代码 buffer = CFRBuffer(max_size=1_000_000) 硬编码了 1MBUFFER_MAX_SIZE = 10_000_000 常量已定义。1M Buffer 太小,在 ~50 次迭代后就满了,导致 FIFO 大量淘汰早期数据。

建议修改:mccfr_trainer.py 第 475 行,将 max_size=1_000_000 改为 max_size=BUFFER_MAX_SIZE

资源影响:

  • 10M × 292 字节 ≈ 2.9 GB 内存 (你的 128GB 完全没压力)
  • 好处: 保留更多早期经验,避免灾难性遗忘,策略收敛更平滑

TRAIN_BATCH_SIZE: 可以开到 65536

当前 32768 已经过大样本量了。但你的 96GB 显存完全可以承受:

Batch Size 显存占用 效果
32,768 (当前) < 2 GB 收敛平滑
65,536 < 4 GB 更平滑,更稳定
131,072 < 8 GB 极度平滑,但可能过度平均化

建议: TRAIN_BATCH_SIZE = 65536。显存占用不到 4GB梯度估计更稳定。

注意: batch size 增大时学习率通常需要线性缩放。65536 = 2×32768 所以 LR 可以从 5e-4 提到 1e-3。但不建议冒进保持 5e-4 更稳。

GAMES_PER_ITER: 保持 200 或适当提高

每迭代数据量 网络更新频率 建议
50 ~5,000 条 很频繁 早期可考虑
200 (当前) ~20,000 条 平衡 推荐保持
500 ~50,000 条 较低 数据更丰富但更新慢

建议: 保持 200。单核遍历是瓶颈加大只是让每次迭代更慢而不会更有效率。

Card Model 数据量: 可以大幅提升

你的 128GB 内存和 96GB 显存在 Phase 1 几乎闲置。可以把训练数据量大幅提高:

参数 当前值 建议值 (中等) 建议值 (激进) 理由
NUM_TRAIN_SAMPLES 2,000,000 20,000,000 100,000,000 更多极端牌型覆盖
NUM_VAL_SAMPLES 50,000 200,000 500,000 更可靠的验证指标
BATCH_SIZE 4,096 16,384 16,384 显存完全够用
NUM_EPOCHS 100 40 20-30 数据量更大,不需要太多 epoch

各数据量对应的资源需求:

样本数 内存 (加载) 磁盘 (.npz) 生成 MC 模拟量 训练步数/epoch (bs=16384)
200 万 ~0.5 GB ~500 MB 20 亿次 ~122 步
2000 万 ~5 GB ~4 GB 200 亿次 ~1221 步
5000 万 ~13 GB ~10 GB 500 亿次 ~3052 步
1 亿 ~26 GB ~20 GB 1000 亿次 ~6104 步

4.3 资源利用分析

资源使用对比 (当前参数 vs 你的硬件上限)

            当前使用    硬件上限    利用率
CPU:        1 核       多核*       ~12.5% (8核) / ~3% (32核)
内存:       ~3 GB      128 GB      ~2.3%
GPU 显存:   ~2 GB      96 GB       ~2.1%
磁盘 I/O:   极低       —           —

* traverse() 是单线程的无法并行化External Sampling MCCFR 的特性)

结论: 你的硬件资源远超当前训练需求。瓶颈在 CPU 单线程遍历,而非 GPU 或内存。

4.4 推荐的生产环境参数配置

# === mccfr_trainer.py 推荐配置 (128GB RAM / 96GB VRAM) ===

NUM_ITERATIONS = 50000             # 长期训练目标
GAMES_PER_ITER = 200               # 保持不变
BUFFER_MAX_SIZE = 10_000_000       # 充分利用内存
MIN_BUFFER_SIZE_FOR_TRAIN = 100_000
TRAIN_BATCH_SIZE = 65536           # 翻倍,利用显存
TRAIN_STEPS_PER_ITER = 50          # 保持不变
LEARNING_RATE = 5e-4               # 保持不变 (batch 翻倍但求稳)
WEIGHT_DECAY = 1e-4
CARD_MODEL_CHECKPOINT = "card_model/data/best_card_model.pt"  # 必须设置!
# === card_model/config.py 推荐配置 (128GB RAM / 96GB VRAM) ===

# --- 中等配置 (2000 万样本) ---
NUM_TRAIN_SAMPLES = 20_000_000    # 2000 万 (原 200 万)
NUM_VAL_SAMPLES = 200_000          # 20 万 (原 5 万)
BATCH_SIZE = 16384                 # 利用显存
LEARNING_RATE = 5e-4               # 保持不变
NUM_EPOCHS = 40                    # 数据多了epoch 可以少
NUM_WORKERS = 16                   # 根据 CPU 核心数调整

# --- 激进配置 (1 亿样本) ---
# NUM_TRAIN_SAMPLES = 100_000_000  # 1 亿
# NUM_VAL_SAMPLES = 500_000        # 50 万
# BATCH_SIZE = 16384
# LEARNING_RATE = 5e-4
# NUM_EPOCHS = 25                  # 数据极多25 epoch 足够
# NUM_WORKERS = 16

5. 训练效果评估

5.1 Phase 1: Card Model 评估

内置指标 (每 epoch 输出):

指标 含义 期望趋势 收敛参考值
Train EMD 直方图预测误差 持续下降 < 0.01
Val EMD 验证集直方图误差 先降后平稳 < 0.015
Train MSE 胜率标量误差 持续下降 < 0.001
Val MSE 验证集胜率误差 先降后平稳 < 0.002
Val Total Loss 验证集总损失 (EMD + 0.1×MSE) 最佳模型选择依据 < 0.02

判断标准:

  • Val EMD < 0.015: 直方图质量可以接受
  • Val EMD < 0.01: 直方图质量良好
  • Val MSE < 0.001: 胜率预测精度良好
  • 过拟合信号: Train EMD 继续下降但 Val EMD 开始上升 → 停止训练,使用 best model

手动测试: python card_model/test_inference.py

  • 输入手牌和公共牌,查看预测的胜率和直方图
  • 抽查已知牌型: AA vs random → 胜率应 ~85%, 72o vs random → ~40%

5.2 Phase 2: CFR 训练评估

内置指标 (每 iteration 输出):

指标 含义 期望趋势 说明
Buffer 当前 Buffer 大小 增长到满后稳定 满 1M/10M 后 FIFO 淘汰
P0_avg_util P0 平均收益 → 0 零和博弈,双方接近 Nash 时趋零
P1_avg_util P1 平均收益 → 0 同上
Regret Loss 遗憾预测 MSE 下降 反映网络拟合 regret 的能力
Policy Loss 策略预测 MSE 下降 反映网络拟合策略的能力
Total Loss Regret + Policy 下降 整体训练指标

关键判断标准:

  1. 平均收益趋零 (最重要)

    • 初期: |avg_util| 可能很大 (几百到几千)
    • 训练中: |avg_util| 应逐渐缩小
    • 收敛: |avg_util| < 100 (相对于归一化前 20000 的筹码量,<0.5%)
    • 含义: 双方策略接近 Nash 均衡,无人能单方面偏离获利
  2. Loss 下降

    • Regret Loss: 下降 → 网络越来越能预测每个动作的遗憾值
    • Policy Loss: 下降 → 网络越来越能预测最优策略分布
    • 停滞信号: Loss 在 1000 次迭代后不再下降 → 可能需要调 LR 或增加数据多样性
  3. 策略稳定性

    • 观察后期 iteration 的 avg_util 波动幅度
    • 如果波动剧烈 → 策略不稳定,可能 batch 不够大或 LR 过高
    • 如果平稳 → 策略收敛良好

5.3 进阶评估方法 (需额外实现)

当前代码未内置以下评估手段,但对正式比赛很重要:

方法 说明 实现难度
对战固定基线 与 Always-Call / Random 策略对战,看胜率
对战旧版本 新 checkpoint vs 旧 checkpoint看胜率提升
Exploitability 计算最佳应对 (Best Response) 的可利用度
NashConv 两位玩家最佳应对价值之和
实战模拟 在 Botzone 模拟器中跑完整对局

最简评估方案 (推荐先实现):

  1. 训练完成后,写一个脚本让 CFR 网络与 "Always Call" 策略对战 1000 局
  2. CFR 网络应稳定赢 "Always Call" (胜率 > 60%)
  3. 随着训练推进,这个胜率应该持续提高

5.4 训练曲线解读

正常训练曲线示意:

avg_util |                    ──────────────
         |               ────/
         |          ────/
         |     ────/
    0 ───|────/───────────────────────────────── iter
         |
         |────/────
         |    翻转 (P0/P1 交替优势后趋零)

Loss     |\
         | \
         |  \___________________________  ← 收敛
         |    \
         |     \_______________
         |_______________________________ iter
异常训练曲线:

Loss     |\        /\
         | \      /  \      /\
         |  \____/    \____/  \___  ← 震荡不收敛
         |_______________________________

→ 可能原因: LR 过高 / batch 过小 / Buffer 太小导致遗忘

6. 文件结构与数据流

6.1 项目文件

poker/
├── train_card_model.py          # Phase 1 入口脚本
├── mccfr_trainer.py             # Phase 2 主训练脚本
├── cfr_net.py                   # CFR 策略网络 (MLP + Regret Matching)
├── cfr_buffer.py                # FIFO 经验回放池 (deque)
├── env_adapter.py               # OpenSpiel 适配器 (动作空间 + 状态提取)
├── README.md                    # 本文件
├── cfr_net_checkpoint.pt        # Phase 2 训练产出
├── card_model/
│   ├── config.py                # Card Model 所有超参数
│   ├── model.py                 # CardModel 网络
│   ├── data_generator.py        # MC rollout 数据生成
│   ├── dataset.py               # PyTorch Dataset + .npz 缓存
│   ├── train_card_model.py      # Card Model 训练循环
│   ├── test_inference.py        # 交互式推理测试
│   └── data/
│       ├── best_card_model.pt   # 最佳 Card Model 权重
│       ├── final_card_model.pt  # 最终 Card Model 权重
│       ├── train_data.npz       # 训练数据缓存
│       └── val_data.npz         # 验证数据缓存
└── open_spiel/                  # OpenSpiel 源码 (已编译)

6.2 数据流

                    ┌─── Phase 1 ───┐

OpenSpiel Game ──→ 随机采样状态 ──→ (hole_cards, board_cards)
                                          │
                                    MC rollout × 1000
                                          │
                                          ▼
                    (x_hole[2], x_board[5], y_equity, y_histogram[50])
                                          │
                                    .npz 缓存到磁盘
                                          │
                                    DataLoader → CardModel
                                          │
                                    Loss = EMD + 0.1×MSE
                                          │
                                    best_card_model.pt


                    ┌─── Phase 2 ───┐

OpenSpiel Game (fullgame, stack=20000, SB=50, BB=100)
        │
        ▼
traverse(state, traversing_player)
        │
        ├── extract_cards_from_state(state) ──→ CardModel (frozen) ──→ card_features [50]
        │
        ├── extract_env_state(state) ──→ 归一化 ──→ env_features [5]
        │     [pot/20000, p0/20000, p1/20000, street/3.0, position]
        │
        ├── info_state = cat(card[50], env[5]) ──→ [55]
        │
        ├── CFRNet.get_strategy(card, env, legal_mask) ──→ current_strategy [6]
        │
        ├── regret = (action_utility - node_utility) / 20000
        │
        └── buffer.add(info_state[55], legal_mask[6], regrets[6], strategy[6])
                    │
                    ▼
            CFRBuffer (deque, FIFO)
                    │
                    ▼
            buffer.sample(32768) ──→ CFRNet forward ──→ Loss ──→ Backprop
                                                    │
                                          Regret Loss: MSE(pred, target) × mask
                                          Policy Loss: MSE(softmax, target) × mask

6.3 动作空间映射

CFR 网络输出 6 个离散动作,由 BetTranslator 映射到 OpenSpiel 引擎动作:

CFR ID 名称 映射逻辑 Botzone 边界处理
0 FOLD 直接映射到引擎 FOLD(0) 始终合法
1 CALL 直接映射到引擎 CALL(1) 始终合法
2 MIN_RAISE 引擎最小加注额 筹码不足 → Fallback 到最大合法加注 (All-in)
3 HALF_POT 底池一半的加注 筹码不足 → Fallback 到最近合法加注
4 FULL_POT 底池大小的加注 筹码不足 → Fallback 到最近合法加注
5 ALL_IN 全部筹码 始终映射到引擎最大合法加注

Fallback 安全链: 目标加注 → _find_nearest_legal_action → Call → Fold → legal[0],绝不抛异常。


7. 常见问题

Q1: 必须先训练 Card Model 吗?

是的。 Card Model 提供 50 维胜率直方图作为 CFR 网络的输入。如果不训练CFR 网络的 card_features 是随机噪声,训练效果极差。

设置方法: CARD_MODEL_CHECKPOINT = "card_model/data/best_card_model.pt"

Q2: 训练中途中断了怎么办?

当前代码不支持断点续训。每次运行 mccfr_trainer.py 都从头开始。如果中断了,之前的训练进度全部丢失。

建议: 定期手动保存 checkpoint (可自行在训练循环中加入 torch.save 逻辑)。

Q3: 为什么 GPU 利用率这么低?

正常现象。 MCCFR 的 traverse() 是 CPU 单线程递归遍历博弈树,只在每个节点做一次 GPU 推理 (batch=1)。GPU 在阶段 A 基本空闲,只在阶段 B 的大 batch 训练时短暂工作。

这是 External Sampling MCCFR 的固有特性,不是代码问题。

Q4: Buffer 中 BUFFER_MAX_SIZE=10_000_000 和实际 max_size=1_000_000 不一致?

这是代码中的一个不一致: 第 72 行定义了 BUFFER_MAX_SIZE = 10_000_000,但第 475 行实例化时硬编码了 max_size=1_000_000。应该改为 max_size=BUFFER_MAX_SIZE 以使用 10M Buffer。

Q5: 训练多久能收敛?

无法给出确切时间,取决于:

  • CPU 单核性能 (traverse 速度)
  • Card Model 质量
  • 超参数设置

粗略参考:

  • 1000 次迭代: 策略初步成型,可能仍有明显漏洞
  • 10000 次迭代: 策略较为合理,能击败简单基线
  • 50000 次迭代: 接近收敛,策略接近 Nash 均衡

Q6: 如何在 Botzone 上使用训练好的模型?

需要编写一个 Botzone 适配器:

  1. 加载 Card Model 和 CFR 网络的权重
  2. 接收 Botzone 的 JSON 输入 (手牌、公共牌、筹码、动作历史)
  3. 转换为 info_state → CFRNet.get_strategy() → 选择动作
  4. 将动作映射回 Botzone 格式输出

这部分需要额外开发,不在当前训练代码范围内。

Q7: 128GB 内存和 96GB 显存能同时跑多个训练进程吗?

理论上可以,但意义不大:

  • Phase 1: 可以开 2-3 个实验 (不同超参数),每个 <8GB 内存
  • Phase 2: CPU 瓶颈使得并行化困难。traverse() 单线程,多进程不会加速单个训练

如果要跑多个实验,建议串行运行 Phase 2并行运行 Phase 1 的不同配置。


附录: 关键数值速查

模型参数量:
  CardModel:    ~45,000
  CFRNetwork:   ~114,600

信息集维度:
  card_features:  50 (胜率直方图)
  env_features:    5 (pot, p0_stack, p1_stack, street, position)
  info_state:     55 (拼接)

CFR 动作空间:
  FOLD(0), CALL(1), MIN_RAISE(2), HALF_POT(3), FULL_POT(4), ALL_IN(5)

Botzone 规则:
  初始筹码: 20000, SB=50, BB=100
  得分: 筹码变化 / 100
  边界: 筹码不足时只能 Fold 或 All-in

归一化:
  STACK_NORMALIZE = 20000.0 (筹码/底池)
  STREET_NORMALIZE = 3.0 (街道)

Buffer 样本大小: ~292 字节/条
  info_state: 55 × float32 = 220 bytes
  legal_mask: 6 × float32 = 24 bytes
  regrets:    6 × float32 = 24 bytes
  strategy:   6 × float32 = 24 bytes

Card Model 每样本大小: ~260 字节
  x_hole:     2 × int64   = 16 bytes
  x_board:    5 × int64   = 40 bytes
  y_equity:   1 × float32 = 4 bytes
  y_histogram: 50 × float32 = 200 bytes

你是一个资深强化学习与分布式系统专家。我当前的德州扑克 Deep CFR 系统(基于 OpenSpiel在训练到中期时因为博弈树指数级爆炸导致底层的 C++ 引擎出现 double free or corruption 内存堆损坏错误。

为了彻底解决算力和内存爆炸问题,请帮我重构代码,进行“动作空间降维”和“深度/内存双重熔断机制”。

请严格按照以下 4 个步骤修改对应文件,并输出完整的修改后代码片段:

任务 1将 6 个动作简化为 5 个 (移除 THIRD_POT)

我们需要将 CFR 的动作空间从 6 维缩减为 5 维。 新的动作定义及索引必须严格为:

  • 0: FOLD
  • 1: CALL
  • 2: HALF_POT (1/2 底池)
  • 3: FULL_POT (1.0 底池)
  • 4: ALL_IN

需要修改的文件:

  1. env_adapter.py:
    • 修改 CFR_ACTIONS 列表,移除 THIRD_POT
    • 更新 NUM_CFR_ACTIONS = 5
    • 更新 RAISE_MULTIPLIERS 字典,只保留 HALF_POTFULL_POT 对应的倍率,注意索引要更新为 2 和 3。
    • 检查 BetTranslator 中的 get_cfr_legal_maskcfr_to_engine_action,确保逻辑与新的索引 (0到4) 严格对齐。
  2. cfr_net.py: 将常量 NUM_ACTIONS 修改为 5。
  3. cfr_buffer.py: 将 __init__ 中的默认参数 num_actions 修改为 5。

任务 2极其严格的博弈树修剪 (修改 env_adapter.py)

env_adapter.pyBetTranslator 类中,将 RAISE_CAP 从 3 修改为 1。 这表示在同一个街区Street只允许发生 1 次比例加注(此后只能 Call 或 All-in。这是控制无限制德州博弈树深度的最核心参数。

任务 3增加硬性递归深度限制 (修改 mccfr_trainer.py)

mccfr_trainer.py 中,为 traverse_traverse_worker 函数增加一个参数 depth: int = 0

  • 在函数开头添加熔断保护:
    if depth >= 40:
        return 0.0  # 超过极限深度,强制返回 0 收益,截断博弈树
    

在所有递归调用这两个函数的地方,将参数传入为 depth + 1。

任务 4修复 C++ 内存泄漏 (修改 mccfr_trainer.py)

这是引发 double free 的直接原因。在 _traverse_worker 和 traverse 的 “3b. 遍历者回合” 循环中,由于频繁调用 state.clone() 产生了大量 C++ 对象堆积。 请在 for a in legal_indices: 的循环末尾,显式地添加一行:

del child_state

强制 Python 立即回收该节点,防止底层 Pybind11 内存分配器雪崩。

你是一个资深强化学习系统的架构优化专家。当前我的德州扑克 Deep CFR 系统已经通过限制动作空间和树深解决了内存爆炸问题。但在长时间多进程运行(如 Iteration 115系统出现了偶发的 BrokenProcessPool 崩溃。

经过排查这是由于以下三个底层原因导致的并发段错误Segfault或死锁

  1. 主进程与 22 个 Worker 对临时权重文件 IPC_WEIGHTS_PATH 发生了并发读写冲突。
  2. _traverse_worker 的对方回合Chance/Opponent Node直接对传入的 state 进行了原地修改 apply_action,破坏了递归栈中的 C++ 状态。
  3. PyTorch 底层的 OpenMP 线程池在 spawn 模式下发生了死锁。

请帮我修改 mccfr_trainer.py 文件,严格执行以下 3 个任务:

任务 1强制压制底层多线程死锁

mccfr_trainer.py 文件的最顶部(必须在 import torch 之前),写入以下环境变量,锁死所有的 C++ 线性代数库多线程:

import os
os.environ["OMP_NUM_THREADS"] = "1"
os.environ["MKL_NUM_THREADS"] = "1"
os.environ["OPENBLAS_NUM_THREADS"] = "1"

任务 2彻底废除临时权重文件改用参数传递
因为我们的模型参数极小< 500KB无需通过文件同步直接通过函数参数传递即可做到 100% 内存安全
修改 main() 函数的阶段 A
删除所有向 IPC_WEIGHTS_PATH 写入和 os.replace 的代码
直接获取当前权重cfr_state_dict_cpu = {k: v.cpu() for k, v in cfr_net.state_dict().items()}
在提交任务时将字典作为参数传入future = executor.submit(worker_traverse_batch, chunk, cfr_state_dict_cpu)
修改 worker_traverse_batch 函数
更新函数签名增加参数def worker_traverse_batch(game_indices: List[int], cfr_state_dict: dict) -> Tuple[...]:
删除原有通过 os.path.getmtime 读取 IPC_WEIGHTS_PATH 临时文件的所有逻辑包括 try...except )。
直接使用传入的参数加载权重_WORKER_STATE["cfr_net"].load_state_dict(cfr_state_dict)

任务 3修复对方回合的 C++ 状态污染
 _traverse_worker  traverse 函数中找到 3a. 对方回合 的逻辑
原代码是直接 state.apply_action(engine_action) 并继续递归这会破坏上一层的 state
请修改为
code
Python
child_state = state.clone()
        child_state.apply_action(engine_action)
        
        utility = _traverse_worker(child_state, traversing_player, card_model, cfr_net,
                                   experiences, translator, depth + 1)
        
        # 强制立即释放 C++ 对象,防止内存泄漏
        del child_state
        return utility
(请注意如果代码中 Chance Node 发牌逻辑也是直接 state.apply_action也请务必按照同样的逻辑修改为 clone() -> apply_action() -> 递归 -> del child_state)
Description
No description provided
Readme 5.5 MiB
Languages
Python 100%