🀄 Mortal Online 训练参数辩论庭

麻将 AI Mortal 的在线自对弈训练一共二十来个关键参数。这一页不直接给"标准答案",而是让 🔺 加大派🔻 收紧派 就每个参数当庭对质,最后由 ⚖️ 裁决 给出单卡 RTX 4090 上的取值、设置原因、关联因素,以及调低 / 调高分别会发生什么。所有结论均对照 train.py / server.py / client.py 源码行为。

🖥️ 单卡 RTX 4090 ♻️ 三进程闭环 ⚔️ 自我辩论式定参 📄 config_online.toml
第 0 章

Online 训练闭环 —— 参数都作用在哪个环节?

辩论之前先把"法庭现场"看清楚。Online 阶段是三个进程通过 TCP(默认 127.0.0.1:5000)协作的闭环,每个参数都挂在闭环的某一段上——知道它挂在哪里,才知道它调错了会卡住谁。

🗄️ server.py (参数服务器 + 样本缓冲,纯 CPU)

  • 保管最新网络参数(带 param_version
  • buffer_dir 攒 client 回传的牌谱
  • 缓冲达 capacity → 拒发参数(反压)
  • trainer 来 drain → 整仓搬到 drain_dir

🎮 client.py (自对弈采样,可多开)

  • 轮询拿最新参数(拿不到 sleep 3s)
  • trainee(Boltzmann 探索) vs 3×baseline(贪心)
  • 每轮打 games 场,牌谱打包回传
  • 打印最近 history_window 轮滑动战绩

🏋️ train.py (online=true,占大头显存)

  • drain 到新牌谱 → 只学 trainee 视角样本
  • DQN MSE + 辅助任务(无 CQL,BN 冻结)
  • submit_every 步推新参数给 server
  • save_every 存档 / test_every 评估
四种 TCP 消息: get_param client → server 取参数 · submit_replay client → server 交牌谱 · submit_param trainer → server 推参数 · drain trainer → server 取整仓样本
⚠️ 一个必须提前知道的"特性":online 模式下 train.py 的主进程只负责拉起训练子进程。每次 test_play 评估结束后子进程会 sys.exit(0)(规避一个官方注释里写明的卡死 bug),父进程 3 秒后自动重启并从 checkpoint 续训。周期性重启是正常现象,不是崩溃。
💡 本页参数取值的三大前提:① offline 阶段已完成,拿 best.pth 同时作为 online 起点和 baseline 对手;② 三个进程同挤一张 24GB 的 4090;③ 麻将是超高方差环境——很多参数的裁决最终都会回到这一点。
第 1 章

训练节奏五参数 —— batch、累积、存档、推送、评估

这五个参数决定 trainer 的"心跳"。它们之间有两条硬性断言锁死:save_every % opt_step_every == 0test_every % save_every == 0,改任何一个之前先想想另外两个。

batch_size 每个训练 step 喂给网络的样本数(决策点数) 裁决:256
🔺
加大派

offline 我们用 512 甚至 1024 都没问题,4090 有 24GB!大 batch 梯度更稳,40 层 ResNet 的算力利用率也更高。online 数据本来就是自对弈产生的噪声数据,更大的 batch 能平掉一部分方差,凭什么要砍半?

🔻
收紧派

因为这张卡不是你一个人的。同一张 4090 上还跑着 client 的推理(trainee + baseline 两套网络)和每 test_every 步一次的 test_play。峰值撞到一起就是 OOM,而且 online 一轮 drain 只有约 capacity=1600 场牌谱、几十万个样本,大 batch 意味着一轮没几个 step,参数推送频率跟着变稀。

⚖️
裁决:256

收紧派赢在"同卡三进程"这个硬约束上。取 256,同时用 opt_step_every=2 做梯度累积,等效 batch 512——梯度平滑度不输 offline,显存峰值却砍半,给 client 留出 3–4GB。这是"想要大 batch 的效果、付小 batch 的显存"的标准解法。若把 client 挪去第二台机器,这里可以直接回 512。

设得过低(如 64)梯度噪声大、BN 已冻结无法自我修正;GPU 利用率低,训练墙钟时间反而拉长;单步统计量抖动让 TensorBoard 曲线没法看。
设得过高(如 1024)与 client / test_play 叠加时 OOM;一轮 drain 的样本只够十几个 step,submit_every 到不了就 epoch 结束,参数更新变得又稀又猛。
opt_step_every 梯度累积步数:攒 N 个 batch 的梯度再做一次 optimizer.step() 裁决:2
🔺
加大派

设成 4 甚至 8,等效 batch 上千,梯度质量拉满,还能进一步把单步 batch_size 压到 128 省显存。

🔻
收紧派

累积不是免费的:optimizer.step() 变稀疏后,同样 400 步 save_every 里实际参数更新只有 400/N 次。online 的意义就是"边采边学",参数更新太稀,client 拿到的永远是几乎没动过的网络,闭环退化成昂贵的空转。而且 scheduler.step() 是每个 batch 都走的,累积过深还会让 lr 调度与真实更新错位感变强。

⚖️
裁决:2

只为配合 batch_size=256 凑出等效 512 而设,够用就停。注意断言 save_every % opt_step_every == 0:save_every=400 能被 2 整除,合法。若显存宽裕(client 上了别的机器),首选做法是直接加 batch_size 而不是加深累积。

设为 1完全合法(offline 默认就是 1),只是 batch_size=256 时等效 batch 偏小,梯度略毛糙。
设得过高参数更新频率骤降,policy 迭代变慢;等效 batch 过大还会钝化探索带来的新信息;忘了整除断言直接 AssertionError 起不来。
save_every 每 N 步保存 checkpoint + 刷 TensorBoard 统计 裁决:400
🔺
加大派

存 checkpoint 要序列化整套模型 + AdamW 动量,几百 MB 往磁盘写,太频繁纯属浪费 IO。设 2000 步存一次足够。

🔻
收紧派

你忘了 online trainer 每次评估后会 sys.exit(0) 重启?重启是从最后一个 checkpoint 恢复的,save_every 就是崩溃 / 重启时最多丢多少进度。而且 TensorBoard 的 loss、Q 值直方图也是这个粒度才写一次,2000 步一个点根本没法盯训练健康度。

⚖️
裁决:400

沿用官方示例值。400 步 × 256 batch ≈ 10 万样本存一次档,IO 占比可忽略,而重启丢失窗口和监控粒度都在舒适区。它还是 submit_every 的锚:源码在 steps % save_every == 0 时若与 submit 不同步会补推一次参数,两者取相同值最省心。

设得过低(如 50)频繁写盘 + 每次都同步刷 TensorBoard 直方图,训练吞吐可见下降;checkpoint 文件反复覆写增加损坏风险窗口。
设得过高(如 5000)评估重启 / 意外崩溃一次回退半天进度;监控曲线稀疏到发现 loss 爆炸时已晚;test_every 被迫跟着取更大的倍数。
submit_every trainer 每 N 步向 server 推送一次最新参数(决定 client 用多新的策略采样) 裁决:400
🔺
加大派(推得勤)

这可是 online 训练的灵魂参数!推送越频繁,client 采样用的策略越接近 trainer 正在优化的策略,off-policy 偏差越小。理论上每次 optimizer.step() 后都该推,设 100 步推一次不过分吧。

🔻
收紧派(推得省)

每次推送要把 40 层 ResNet + DQN 的 state_dict 全量序列化走一遍 TCP,server 端还要在锁里做 torch.save。更关键的是:client 一轮自对弈打 800 场要好几分钟,你 100 步推一次,client 一轮牌还没打完参数已经换了好几版——推了也没人用,纯烧 CPU。推送频率超过 client 的消费频率就是无效功。

⚖️
裁决:400,与 save_every 对齐

推送节奏应当匹配 client 打完一轮的耗时,而不是越快越好。单卡上 trainer 跑 400 步与 client 打完一轮 800 场的耗时量级接近,一轮一版参数刚好衔接。另外源码保证 save 时若错过 submit 会补推一次,二者同值可以让"存档的模型"和"client 在用的模型"始终一致,排查问题时少一个变量。

设得过低序列化 + 网络传输开销吃掉训练吞吐;参数版本切换比 client 消费还快,get_param 返回的版本跳跃大,战绩曲线解释性变差。
设得过高(如 4000)client 长期用旧策略采样,MC 回报评的是旧策略、更新的是新策略,off-policy 漂移加剧,训练效率与稳定性同时受损——这是最不能省的方向。
test_every 每 N 步暂停训练,与 baseline 打一场大规模评估赛(并触发子进程重启) 裁决:8000
🔺
评得勤派

online 训练最怕悄悄变弱(自对弈会放大策略的自我偏食)。评估间隔太长,模型走偏了几万步才发现,白烧几天电。2000 步一评,早发现早止损。

🔻
评得省派

一次评估 = 3000 场对局的纯推理 + 完事后进程重启重建 DataLoader,在 4090 上是十几分钟量级的纯停训时间。2000 步一评,评估耗时能占到总墙钟的两三成。而且 400 步内策略根本没变多少,评了也是噪声——麻将 3000 场的置信区间本来就不小。

⚖️
裁决:8000(= 20 × save_every)

评估的价值在于"策略变化量足够大,胜率差异能盖过 3000 场的统计噪声"。按 submit_every=400 算,8000 步意味着 client 已经用了约 20 版参数、贡献了几万场新对局——这时候的评估才测得出真实进步。折中于官方示例 offline 用的 20000 和评得勤派的 2000 之间,因为 online 阶段确实更需要盯退化。

设得过低频繁停训 + 频繁重启,有效训练时间占比骤降;相邻评估差异淹没在方差里,best.pth 反而被噪声"新高"污染。
设得过高策略退化(如探索塌缩、过拟合 baseline 漏洞)长时间无人发现;best.pth 长期不更新,轮换 baseline 的节奏也被拖慢。
第 2 章

探索策略 —— trainee 该多"浪"?

client 里 trainee 用 Boltzmann 采样探索,三个 baseline 永远贪心。探索参数是 online 阶段数据质量的总开关:探索不足学不到新东西,探索过头牌谱全是昏招。这些参数放在 [train_play.<profile>] 段,可用环境变量 TRAIN_PLAY_PROFILE 让不同 client 用不同探索档位。

boltzmann_epsilon 每个决策点以此概率改用 Boltzmann 采样(其余时刻贪心选 Q 最大动作) 裁决:0.005
🔺
多探索派

RL 没有探索就是自嗨。ε 才 0.5%?DQN 传统 ε-greedy 都从 1.0 退火到 0.05 呢。不探索,trainee 永远在自己 Q 值的舒适区里打转,那些"offline 数据里人类不打、但其实更优"的招永远发现不了。

🔻
少探索派

麻将不是 Atari。一局 East-South 战每个玩家约有上百个决策点,ε=0.05 意味着每局平均好几次故意打偏——切错一张牌就可能直接放铳 12000 点,整局的 MC 回报全被这次探索污染,而这个被污染的回报会回传给这局所有正常决策的动作。稀疏大惩罚环境里,探索的代价是全局性的。另外我们不是从零训练:offline 已经给了一个人类水平的先验,需要的是精修不是撒网。

⚖️
裁决:0.005(官方示例值,别乱动)

取 0.005 ≈ 每局在1 次以下的探索动作,加上 Boltzmann 本身按 Q 值加权(不是均匀乱选),偏差动作大概率也是"次优而非昏招"。这是微创探索:既保留发现新招的通道,又把回报污染控制在单局可承受范围。想要更多探索,正确姿势是多开一个 TRAIN_PLAY_PROFILE=explore 的 client 专门产高探索数据,而不是把主力 client 调浪。

设为 0完全贪心:数据分布迅速塌缩到当前策略的窄走廊,Q 值对未走过的动作越来越失真(online 没有 CQL 兜底了!),几轮后训练停滞甚至自我强化错误。
设得过高(如 0.05+)牌谱大量掺入自毁操作,MC 回报方差爆炸;trainee 战绩虚低导致你误判模型退化;学出来的 Q 值把"队友会犯蠢"当成环境常态。
boltzmann_temp Boltzmann 采样温度:softmax(Q/T) 的 T,控制"探索时偏离多远" 裁决:0.05
🔺
高温派

T 太低的话 softmax(Q/T) 几乎还是 argmax,所谓探索就是走个形式。T=1 才能让次优动作真正有出场机会。

🔻
低温派

看看 Q 值的量纲:回报是 pts=[6,4,2,0] 尺度下的期望 pt 增量,动作间的 Q 差常在 0.1 以下。T=1 时这点差异被 softmax 抹平,等于在合法动作里近似均匀乱抽——包括那些 Q 值垫底的自杀切牌。探索应该在"好动作的邻域"里,不是掀桌子。

⚖️
裁决:0.05

低温派对量纲的观察是决定性的:T=0.05 把 0.1 的 Q 差放大成 softmax 里 e² ≈ 7 倍的概率比,效果是"在前几名动作之间摇摆"——恰好是我们要的"探人类先验附近的未开发区"。T 与 ε 要一起看:ε 定频率、T 定幅度,两个都激进 = 数据报废;一收一放才是可控探索。

设得过低(如 0.001)softmax 退化回 argmax,ε 名存实亡,等效于零探索(见上一场辩论"设为 0"的后果)。
设得过高(如 1.0)探索时近似均匀随机,昏招频出;配合高 ε 时牌谱质量崩盘,回报信号信噪比归零。
top_p 核采样截断:Boltzmann 采样时只在累积概率前 p 的动作集合里抽 裁决:1.0(不截断)
🔺
截断派

加一道保险:top_p=0.9 把概率长尾里的极端昏招直接排除,探索永远不会打出 Q 值垫底那几张牌,牌谱质量有下限保证。

🔻
不截断派

T=0.05 的低温 softmax 已经让尾部动作概率指数级衰减到近乎为零,再加 top_p 是双重保险叠罗汉,实际行为几乎不变,还多一个要解释的超参。真出问题时你分不清是 T 的锅还是 p 的锅。

⚖️
裁决:1.0

低温已提供实质截断,保持 1.0 少一个自由度。它的真正用武之地是高温 profile:如果你专门开一个 T=0.2 的高探索 client,配 top_p=0.8 能在"探得野"和"不自杀"之间画出安全区——届时再动它。

设得过低(如 0.5)探索空间被压缩回前一两个动作,探索名存实亡;与低温叠加时完全退化为贪心。
1.0 即上限无额外风险——风险控制全权交给 T 与 ε。
games client 每轮自对弈的对局数(内部 = games/4 个种子 × 4 个座位轮换) 裁决:800
🔺
大批派

批量推理才有 GPU 效率,一轮多打点,牌谱管够,trainer 永远不会饿着等数据。而且 800 场的战绩统计比 200 场稳得多。

🔻
小批派

一轮打得越久,这批数据的"策略年龄"越老——第 800 场还在用轮初拿的参数,而 trainer 那边可能已经推了两版新的。games 本质上是数据新鲜度和采样效率的兑换率

⚖️
裁决:800

800 场 = 200 个种子 × 4 座轮换(duplicate 制,同一副牌四个座位各坐一遍,砍掉配牌运气的方差)。在 4090 上一轮耗时与 trainer 跑 400 步(= submit_every)大致同步,闭环节奏最顺。多开 client 或多机采样时,可以把单 client 的 games 减半,让参数消费更细粒度。

设得过低(如 100)每轮 get_param/submit_replay 的固定开销占比上升;单轮战绩几乎纯噪声(好在有 history_window 滑动平均兜底)。
设得过高(如 4000)数据策略年龄严重滞后于 trainer;一轮数据灌爆 buffer 触发 capacity 反压,client 干等;发现探索参数配错时已浪费一大轮算力。
repeats 同一批对局种子重复打几轮后才换新种子 裁决:1
🔺
重复派

同一副牌用不同版本的参数各打一遍,天然的对照实验!还能让模型把一副牌"打透",类似课程学习。

🔻
换牌派

训练数据的多样性是 RL 的生命线。反复喂同一批配牌,模型会记住"这副牌该这么打"而不是学会普适牌理;何况天凤种子空间近乎无限,没有理由省着用。

⚖️
裁决:1

默认不重复。repeats 的合理场景是调试(固定种子对比两版参数的行为差异)或算力极度受限时摊薄种子生成成本——正常训练保持 1,让每轮都是新牌局。

1 即下限无风险,多样性拉满。
设得过高对特定配牌过拟合,评估(新种子)表现与训练战绩脱节;数据相关性升高,等效样本量缩水。
第 3 章

数据管道与反压 —— server 怎么当好中间商

server 是闭环的流量阀门。这一章的参数决定"采样→训练"的耦合松紧:太松 = trainer 学陈年旧账,太紧 = 两边互相罚站。

capacity buffer 里最多攒多少份牌谱;到顶后 server 拒绝给 client 发参数(反压) 裁决:1600
🔺
大仓派

缓冲当然越大越好:client 永远不被反压卡住,GPU 采样端满负荷运转,trainer 每次 drain 都有充足的大批量数据,吞吐拉满。

🔻
小仓派

capacity 的真实身份是数据陈旧度的上限。trainer 一次 drain 拿走整仓,仓越大,这批数据里最老的牌谱离当前参数越远。Mortal 用的是 MC 回报 + 当前策略拟合,吃太陈的数据就是在拟合一个已经不存在的策略的价值。反压不是 bug,是让采样端"等一等学习端"的设计。

⚖️
裁决:1600 = 2 × games

取一轮自对弈产量(800)的两倍:允许 client 在 trainer 消化时先打完下一轮不被卡住(管道并行),但攒不满第三轮——数据年龄被压在"两版参数以内"。扩多 client / 多机采样时按 2 × 所有 client 的单轮总产量 等比放大。

设得过低(如 400)client 一轮没打完就被反压挂起,采样 GPU 空转;trainer 每次 drain 只有几百场,DataLoader 重建开销占比飙升。
设得过高(如 20000)trainer 一次 drain 面对几小时前的旧策略数据,off-policy 漂移放大;单机上牌谱文件堆积占内存/磁盘。
force_sequential true 时强制"采一批 → 训一批"严格串行(client 必须等 trainer 空闲版参数) 裁决:false
🔺
串行派

true 才是教科书式的同步 RL:每批数据都严格由"上一版训练完成的参数"产生,零陈旧、零版本混杂,实验完全可复现,调参归因清晰。

🔻
并行派

代价是什么?trainer 训练时 client 罚站,client 采样时 trainer 罚站——同一张 4090 上两个角色轮流干瞪眼,等效算力直接砍半。我们已经用 capacity=2×games 把陈旧度压到两版参数以内了,这点软陈旧换一倍吞吐,稳赚。

⚖️
裁决:false

单卡训练时间是最贵的资源,选流水线并行。什么时候切 true?做严格的消融实验(比如对比两组探索参数的收敛曲线)时,串行模式消除了"数据年龄"这个混杂变量,结论更干净——跑正式训练再切回 false。

false吞吐最大化,代价是最多两版参数的软陈旧(已由 capacity 控制)。
true硬件利用率约砍半;但换来零陈旧与可复现性——只该在做对照实验时用。

两个免辩论的速裁参数

参数裁决理由与后果
[online].history_window 50 只影响 client 日志里滑动平均战绩的窗口,完全不参与训练。50 轮 × 800 场 = 4 万场的滑动窗口,平均顺位的噪声约 ±0.02,足以判断趋势。设太小(5)曲线抖成心电图看不出涨跌;设太大(500)对"最近变强了吗"反应迟钝。纯监控参数,按口味调。
[dataset].num_workers 1(online 阶段) offline 我们开 4–8 个 worker 喂饱 GPU,但 online 每轮 drain 只有约 1600 场牌谱,几个 worker 分不到几个文件,反而多付进程启动 + GRP 模型每 worker 各加载一份的开销(DataLoader 每个 epoch 都重建!)。设高了不会错,只是白费内存和启动时间。
[online.server].sample_reuse_rate / threshold 0 / 0 诚实说明:这两个键在 config.example.toml 里存在,但当前版本 server.py 并未读取使用(源码只读 buffer_dir / drain_dir / capacity / force_sequential)。保留默认 0 即可,别指望调它们生效。
第 4 章

优化器与稳定性 —— 别把 offline 学的东西训没了

最容易翻车的一章。核心矛盾:offline 起点是一个已经很强的策略,online 的自对弈数据却又少又噪——学猛了毁掉先验,学怂了原地踏步。还有一个源码细节垫底:从 offline checkpoint 切到 online 时,train.py 检测到模式变化,不会加载 optimizer/scheduler 状态,学习率完全按本章配置从零生效。

scheduler.peak / final 学习率调度:peak=final 且 max_steps=0 ⇒ 恒定学习率 裁决:1e-5 恒定
🔺
大步派

1e-5 也太怂了。offline 用 1e-4 都稳稳的,online 数据是模型自己产的、分布更贴合当前策略,学习率不该更大胆点吗?小 lr = 龟速进步 = 电费白烧。

🔻
小步派

三点反驳。①offline 有百万级人类对局和 CQL 正则托底,online 每轮只有 1600 场自产数据且CQL 已关闭——正则少了,步长必须跟着收;②MC 回报在麻将里方差巨大,大 lr 会让单批噪声直接写进权重,几轮就把 offline 学的先验冲垮(灾难性遗忘);③自对弈有正反馈回路:策略走偏 → 产出偏数据 → 更偏,大 lr 是这个回路的放大器。

⚖️
裁决:peak = final = 1e-5,warm_up_steps=0,max_steps=0

online 本质是精调(fine-tune)而不是从头学,取 offline 峰值的 1/10。恒定而非退火:online 没有"训练集跑完"的概念,数据无限供给,不存在一个自然的退火终点;要降 lr 应该由你看着评估曲线手动改配置重启(反正 trainer 本来就周期性重启)。warm_up 也不需要——权重不是随机初始化的,没有冷启动震荡问题。

设得过低(如 1e-7)参数更新幅度低于数据噪声地板,评估曲线纹丝不动,闭环空转,纯烧电。
设得过高(如 1e-4)数轮内 test_play 胜率断崖下跌的经典配方:噪声梯度冲垮先验 + 自对弈正反馈放大偏移,而且 BN 已冻结、CQL 已关,没有任何机制拉你回来。
freeze_bn.mortal 冻结编码器所有 BatchNorm 的 running mean/var(仿射参数仍可学习) 裁决:true(online 必须)
🔺
解冻派

BN 统计量跟着数据走才是 BN 的本意。自对弈数据分布确实和人类牌谱不同,冻着不更新,网络永远用"人类牌谱时代"的均值方差做归一化,这不是刻舟求剑吗?

🔻
冻结派

问题恰恰在于 online 的数据分布是非平稳的——它随着策略更新每轮都在漂。BN 统计量追着一个移动目标跑,会造成三重伤害:①训练端归一化基准持续抖动,等效于给每层输入注入噪声;②client 推理用的是 running stats,统计量一变,同一套权重的行为就变,战绩曲线失去可比性;③策略偏移 → 分布偏移 → BN 偏移 → 输出偏移,又一条正反馈回路。config.example.toml 的注释直接写着 "true in online training"——作者用真金白银的算力踩过这个坑。

⚖️
裁决:true,没有商量余地

这是 online 与 offline 在训练语义上的两大区别之一(另一个是 CQL 自动关闭)。冻结的只是 running statistics,BN 的 γ/β 仿射参数仍在正常学习——表达能力还在,只是归一化基准锚定在 offline 时代的大数据统计上,稳定压倒一切。

false(忘了改)online 训练的头号隐形杀手:loss 看着正常,test_play 却一路阴跌,因为推理时的 BN 统计量已经漂离权重的最优工作点。
true唯一副作用:若未来自对弈分布与人类分布差异极大,归一化基准会略失配——实践中远小于漂移的危害。
max_grad_norm 全局梯度范数裁剪阈值(0 = 不裁剪;官方示例为 0) 裁决:1.0
🔺
不裁派

官方 example 就是 0!裁剪会偏置梯度方向,AdamW 本身自带二阶矩自适应,天生对梯度尺度不敏感,再裁一刀是画蛇添足。

🔻
要裁派

AdamW 的自适应对"持续的尺度差异"有效,对突发的单批异常没那么快——二阶矩是指数滑动平均,需要几步才追上。而麻将回报是重尾的:一批样本里出现一局役满(±32000 点级的回报),MSE 损失的梯度瞬间放大几十倍。offline 有百万样本把这类事件摊薄,online 一轮才 1600 场,重尾事件占批次的比例高得多。clip 是专防这种单点雷击的保险丝。

⚖️
裁决:1.0(有意识地偏离官方示例)

这是全页唯一主动改离官方 example 的参数,理由是 online 小批量 + 重尾回报 + 无 CQL 的组合放大了单批雷击风险。1.0 是"平时摸不到、雷击时兜得住"的宽松阈值——正常训练梯度范数远小于 1,裁剪几乎不触发,不会引入不裁派担心的方向偏置。如果 TensorBoard 显示 loss 从无尖刺,想回 0 也无妨。

设得过低(如 0.1)大量正常批次被裁,梯度方向持续失真,收敛变慢且诡异。
0(不裁)多数时候没事,直到某一批役满扎堆,一步大更新把策略踹出稳定区——然后自对弈正反馈接管,曲线再也回不来。

三个速裁参数

参数裁决理由与后果
[optim].weight_decay 0.1(与 offline 一致) 只作用于 Linear/Conv1d 的 weight(源码手工分组)。保持与 offline 相同——正则强度突变本身就是一种分布冲击。设 0 权重会缓慢膨胀,长训后过拟合自对弈风格;设 1.0 会持续把 offline 学到的权重往原点拉,变相遗忘。
[cql].min_q_weight 5(保留但不生效 源码 if not online: cql_loss = ... —— online 下 CQL 项根本不计算。为什么关?CQL 惩罚"数据分布外的动作高 Q",offline 用它防止脱离人类先验;online 的数据就是当前策略自己产的,继续压分布外动作等于禁止探索成果被利用。留着这个键只为切回 offline 时不用改配置。
[aux].next_rank_weight 0.2(与 offline 一致) 辅助任务:让编码器顺带预测本局结束后的顺位,给表征提供密集监督信号,缓解 MC 回报稀疏的问题。online 数据虽噪,顺位标签仍然是真实的,继续留着。设 0 丢失正则效果;设过高(1.0+)编码器被辅助任务绑架,Q 学习沦为副业。
第 5 章

奖励与评估 —— 模型到底在优化什么

奖励参数是所有参数里"看起来最无害、实际上最定调"的:它们直接定义了模型的价值观。评估参数则决定你敢不敢相信自己看到的曲线。

env.pts 顺位价值向量:GRP 奖励 = 期望顺位 pt 的逐局增量,这就是 pt 的标尺 裁决:[6.0, 4.0, 2.0, 0.0](训练期)
🔺
实战派

要上天凤打段位战,就该用真实 pt 表,比如特上桌的 [90, 45, 0, -135]!四位的惩罚是一位收益的 1.5 倍,模型必须从奖励里学会"避四大于争一"。用等差的 [6,4,2,0],模型对掉到四位的痛感不够,打出来的是"敢打敢拼但偶尔暴毙"的风格。

🔻
均衡派

非对称 pt 表在训练早期是毒药:避四的梯度信号压倒一切,模型会学出畏缩的弃和风格,进攻能力萎缩。而且 [90,45,0,-135] 的数值尺度大了一个数量级,Q 值量纲跟着变,boltzmann_temp、mse loss 的绝对值全得重调。等差 pts 让四个顺位的信号均衡,先把"普适麻将力"练扎实。注意 test_play 显示用的 [90,45,0,-135] 是写死的展示口径,和训练 pts 无关。

⚖️
裁决:训练期 [6,4,2,0],冲段前微调再切非对称

课程式安排:online 主训练期用等差 pts 打基本功;确定目标场况后,末期把 pts 换成目标 pt 表的等比缩小版(如 [0.9, 0.45, 0, -1.35])做短期微调,让策略向"避四"偏置。切换 pts = 切换奖励函数,等于换了优化目标,切换后前几次评估波动属正常,别急着回滚。

各档差距过小(如 [4,3.5,3,2.5])顺位之间的价值差被压平,奖励信号微弱,模型对顺位争夺不敏感,风格趋向摸鱼。
四位惩罚过重(如 [6,4,2,-20])极端避四:立直全弃、副露龟缩,平均顺位可能反而变差(把二三位也守没了)。
env.gamma 局内折扣因子:Q 目标 = γ^(距本局终局步数) × 本局 GRP 奖励 裁决:1
🔺
折扣派

RL 教科书说 γ<1 能降低远期回报的方差,还能表达"眼前的牌效优先于虚无缥缈的终局"。设个 0.99 稳一点。

🔻
无折扣派

信用分配的脏活已经被 GRP 干完了——奖励不是"整场比赛终局才结算",而是每一局(kyoku)结算一次期望 pt 增量,回报延迟最多几十步。这几十步内,γ=0.99 会让局开头的配牌选择只拿到 0.99^70 ≈ 0.5 倍的信号——可麻将恰恰是"开局几张牌的取舍决定整局走向"的游戏,前期决策才最该拿满信用!

⚖️
裁决:1,不折扣

无折扣派的结构性论证成立:GRP 逐局塑形 + 局内不折扣,是"方差控制"和"信用完整"兼得的设计。γ 唯一该动的场景是诊断性实验(比如怀疑模型过度铺后期大牌),正常训练锁死 1。

设低(如 0.9)0.9^70 ≈ 0.0006,局前半段的决策几乎拿不到任何奖励信号,模型只学会终盘几巡的打法,序盘手牌配置全靠 offline 老本。
1 即上限MC 方差略高,但已被 GRP 逐局结算消化,无实际风险。
test_play.games 每次评估打多少场(与 baseline 对战,决定 best.pth 是否更新) 裁决:3000
🔺
多打派

麻将的单场方差臭名昭著,强 9% 的模型也能连输几百场。3000 场的平均顺位标准误差还有 ±0.02 左右,两版模型的真实差距经常就在这个量级里!要我说打 10000 场才配叫评估。

🔻
少打派

评估是纯开销:不产训练数据、占满 GPU、结束还触发重启。10000 场一次评估要一小时起步,配合 test_every=8000 意味着评估时间逼近训练时间的一半。评估的目的不是精确测出 Elo,是发现明显退化 + 筛选明显进步,3000 场(其中 duplicate 座位轮换已砍掉配牌方差)足够识别 0.05 级别的顺位变化。

⚖️
裁决:3000,精评靠离线 1v3

训练内评估当"烟雾报警器",3000 场够灵敏;真要给两个 checkpoint 排座次,用独立的 one_vs_three.py 跑几万场慢慢打,不占训练时间。另外记住 best.pth 的更新判据是平均 pt 平均顺位双双不劣于历史最佳——双指标 + 3000 场,已经能挡住大部分统计噪声的假新高。

设得过低(如 400)评估结果基本是抛硬币,best.pth 被噪声污染,你会看着曲线上蹿下跳做出错误的调参决定。
设得过高(如 20000)每次评估停训一小时以上,有效训练时间占比崩塌;评估期间 client 还在傻等参数更新。
第 6 章

最终配置与启动 —— 辩论结果汇总落地

全部裁决落进一份 config_online.toml。带 ★ 的行是辩论庭的核心裁决,其余为路径与常规项。

TOML · config_online.toml
[control]
version = 4
online = true                # ★ 在线模式总开关
state_file = '/data/mortal/online/mortal.pth'   # 起点 = offline 的 best.pth 副本
best_state_file = '/data/mortal/online/best.pth'
tensorboard_dir = '/data/mortal/tb/online'
device = 'cuda:0'
enable_cudnn_benchmark = true
enable_amp = true            # 4090 tensor core,省显存提吞吐
enable_compile = false
batch_size = 256             # ★ 第 1 章:给同卡 client 留显存
opt_step_every = 2           # ★ 梯度累积,等效 batch 512
save_every = 400             # ★ 存档/监控粒度;须被 opt_step_every 整除
test_every = 8000            # ★ 20×save_every;评后子进程重启属正常
submit_every = 400           # ★ 与 client 单轮耗时对齐

[train_play.default]
games = 800                  # ★ 200 种子 × 4 座 duplicate
log_dir = '/data/mortal/online/train_play'
boltzmann_epsilon = 0.005    # ★ 微创探索:每局约1次以下
boltzmann_temp = 0.05        # ★ 低温:只在好动作邻域内摇摆
top_p = 1.0                  # 低温已自截断,不再叠保险
repeats = 1

[test_play]
games = 3000                 # ★ 烟雾报警器精度;精评走 one_vs_three
log_dir = '/data/mortal/online/test_play'

[dataset]                    # online 不读 globs,但该段仍被加载
globs = ['/data/dataset/**/*.json.gz']
file_index = '/data/mortal/file_index.pth'
file_batch_size = 15
reserve_ratio = 0.0
num_workers = 1              # ★ 每轮数据量小,多 worker 是负收益
player_names_files = []
num_epochs = 1
enable_augmentation = false
augmented_first = false

[env]
gamma = 1                    # ★ GRP 已逐局塑形,局内不再折扣
pts = [6.0, 4.0, 2.0, 0.0]   # ★ 训练期等差;冲段前再切非对称微调

[resnet]                     # 必须与 offline 完全一致,否则权重加载失败
conv_channels = 192
num_blocks = 40

[cql]
min_q_weight = 5             # online 下不参与计算,留作回切 offline

[aux]
next_rank_weight = 0.2       # 顺位辅助任务,密集监督信号

[freeze_bn]
mortal = true                # ★★ online 必须冻结 BN,头号隐形杀手

[optim]
eps = 1e-8
betas = [0.9, 0.999]
weight_decay = 0.1
max_grad_norm = 1.0          # ★ 防役满级回报的单批雷击(偏离官方示例)

[optim.scheduler]
peak = 1e-5                  # ★ offline 峰值的 1/10;模式切换时优化器重置
final = 1e-5
warm_up_steps = 0            # 非随机初始化,无需预热
max_steps = 0                # peak==final ⇒ 恒定学习率

[baseline.train]             # client 的自对弈对手
device = 'cuda:0'
enable_compile = false
state_file = '/data/mortal/online/baseline.pth'

[baseline.test]              # trainer 的评估对手
device = 'cuda:0'
enable_compile = false
state_file = '/data/mortal/online/baseline.pth'

[online]
history_window = 50          # 纯监控:滑动战绩窗口
enable_compile = false

[online.remote]
host = '127.0.0.1'           # 多机采样:server 改 0.0.0.0,client 填其 IP
port = 5000

[online.server]
buffer_dir = '/data/mortal/online/buffer'   # server 启动会清空重建!
drain_dir = '/data/mortal/online/drain'
sample_reuse_rate = 0        # 当前版本 server 未使用
sample_reuse_threshold = 0
capacity = 1600              # ★ 2×games:陈旧度 ≤ 两版参数
force_sequential = false     # ★ 流水线并行;做消融实验时才切 true

[grp]
state_file = '/data/mortal/grp.pth'          # 与 offline 同一个 GRP

[grp.network]
hidden_size = 64
num_layers = 2

启动三部曲

SHELL · 三个终端
# 准备:用 offline 成果初始化(起点与对手是同一个模型)
mkdir -p /data/mortal/online
cp /data/mortal/best.pth /data/mortal/online/mortal.pth
cp /data/mortal/best.pth /data/mortal/online/baseline.pth
cd $MORTAL_ROOT/mortal

# 终端 1:参数/样本服务器(纯 CPU,最先启动)
MORTAL_CFG=config_online.toml python server.py

# 终端 2:训练器(启动即提交初始参数,然后阻塞等样本)
MORTAL_CFG=config_online.toml python train.py

# 终端 3:自对弈采样器(可多开;高探索副本用 profile 区分)
MORTAL_CFG=config_online.toml python client.py
# TRAIN_PLAY_PROFILE=explore MORTAL_CFG=config_online.toml python client.py

4090 显存分账

进程占用内容估算
trainer训练态 Brain+DQN+Aux、AdamW 一二阶矩、batch 256 AMP 激活~6–10 GB
trainer 内 test_play评估期额外的推理态 challenger + baseline~2–3 GB(峰值期)
client推理态 trainee + baseline(AMP 推理)~3–4 GB
🚨 OOM 最常发生在 test_play 与 client 自对弈叠加的瞬间。先按本页参数跑起来看 nvidia-smi 稳态,确认峰值有余量后再逐步上调 batch_size。

收敛后做什么:轮换 baseline

💡 当 client 日志里最近 50 轮的滑动平均 pt 稳定为正、平均顺位 < 2.5 并持续改善,说明 trainee 已明显强于 baseline。此时把 best.pth 复制为新的 baseline.pth,重启三进程,让模型和"更强的自己"继续对弈——这就是 online 阶段的版本迭代循环。别太频繁(评估噪声),也别太久不换(打赢弱对手学不到东西),以评估曲线连续两三次创新高为节奏。
第 7 章

自测四题 —— 检验你有没有听懂辩论

🧠

参数辩论庭 · 结业小测

每题点选一个答案,立即判分并给出解析
Q1.online 训练中,以下哪个设置错误会导致"loss 曲线看着正常,但 test_play 战绩一路阴跌"?
Acapacity 设得太大
Bfreeze_bn.mortal 忘了改成 true
Chistory_window 设得太小
解析:BN 不冻结时,running statistics 追着自对弈的漂移分布跑,训练端 loss 依旧下降,但推理端(client/test_play 用 eval 模式的 running stats)的归一化基准已经漂离权重的最优工作点——典型的"训练正常、推理变弱"。capacity 过大影响的是数据陈旧度,history_window 只是日志窗口。
Q2.为什么 online 阶段学习率要取 offline 峰值的约 1/10(1e-5)?最核心的原因是:
A4090 显存不够,只能用小学习率
BAMP 混合精度要求学习率必须小
C数据少且噪声大、CQL 正则已关闭,大步长会冲垮 offline 学到的先验并被自对弈正反馈放大
解析:online 是精调:每轮仅约 1600 场自产数据,MC 回报方差大,且失去了 CQL 的保守约束;策略走偏会产出更偏的数据,形成正反馈。学习率与显存、AMP 无关。另外记住源码行为:offline→online 切换时 optimizer/scheduler 状态不继承,scheduler 配置从零生效。
Q3.boltzmann_epsilon=0.005 与 boltzmann_temp=0.05 的分工是:
Aε 决定多久探索一次,T 决定探索时偏离最优动作多远
Bε 控制训练步长,T 控制评估频率
C两者等价,调一个就够了
解析:ε 是频率闸门(每个决策点以 0.5% 概率进入采样分支),T 是幅度旋钮(softmax(Q/T) 的温度,低温=只在 Q 值接近的头部动作间摇摆)。麻将回报稀疏且惩罚巨大,一次昏招污染整局回报,所以两者都收得很紧,形成"微创探索"。
Q4.capacity 取 1600(= 2 × games)的设计意图是:
A凑一个 2 的幂次方便显存对齐
B允许 client 在 trainer 消化时先打完下一轮(管道并行),同时把数据陈旧度压在约两版参数以内
Cserver 的 TCP 连接数上限是 1600
解析:capacity 是反压阀门,也是数据年龄上限:buffer 攒满即拒发参数,client 被迫等待。2×games 恰好允许一轮的流水线重叠,又不至于让 trainer drain 到太陈旧的牌谱。多 client 部署时按所有 client 单轮总产量的 2 倍等比放大。