资讯动态

从数独到GRPO:昇腾NPU上构建LLM强化学习训练链路

发布时间:2026/10/17 4:45:53 来源:尧图企业网站定制
先说我自己的结论如果你手头正好有一张昇腾NPU卡又一直想搞明白GRPO强化学习到底是怎么跑的那么“教LLM玩数独”是一个性价比极高的入门项目。它不像训练通用对话模型那么虚数独规则明确、奖励可验证、任务闭环完整该踩的坑一个不少但整体数据量和算力需求又非常克制非常适合用来验证一套从“数据构造→rollout采样→奖励计算→策略更新”的完整RL训练链路。这篇文章是我自己把整个项目从零跑通之后的记录内容以实操为主涉及GRPO的核心机制、数独环境的搭建、奖励函数设计、昇腾NPU环境下的工程适配以及我在训练过程中遇到的一系列问题和对应的排查思路。没有太多花哨的东西就是一条一条踩出来的经验希望对正在考虑在NPU上进LLM强化学习训练的团队和个人有点参考价值。1. 项目整体思路为什么选数独做GRPO训练场景1.1 数独为什么是天然的强化学习题目数独是一个典型的“结果可验证、过程难监督”任务。模型面对的输入是一个9x9的盘面其中一部分格子已经填好剩下的是空格要求模型输出一个完整盘面满足每行、每列、每个3x3宫格内数字1到9不重复且必须与给定的初始数字一致。这类任务有两个对强化学习极友好的特点。第一最终答案对不对可以写一个确定性的校验函数去判断不需要人工标注也不需要模型评判。这让奖励信号完全透明、可复现。第二中间推理过程很难给每一步评分因为填数独本来就是一个不断试错回溯的过程你没法精确判断某一步填“3”是不是最终解的一部分。这意味着我们不适合用密集的逐token监督而是应该把整条生成序列当成一次完整动作用稀疏奖励驱动模型学会全局一致性地填满盘面。所以数独本质上是一个用强化学习倒逼模型学会“自洽推理”的任务正好是GRPO这类面向可验证奖励场景的算法的看家本领。1.2 不选PPO选GRPO的核心考量在LLM强化学习训练中PPO长期是主流方案但它有一个不太让人省心的组件——价值网络critic。价值网络需要和策略模型一起训练给每个token预测一个优势估计值这直接带来两个问题显存占用几乎翻倍训练稳定性也容易出幺蛾子。尤其是在国产加速卡上有些算子生态对critic这类额外网络的适配不见得完善调起来非常费劲。GRPO的思路则完全不同。它不要价值网络而是对同一个prompt采样出多个输出结果组成一个组然后在这个组内部对奖励做标准化用相对优劣来计算优势值。公式说人话就是这样[ A_i \frac{r_i - \text{mean}(r_1, r_2, ..., r_G)}{\text{std}(r_1, r_2, ..., r_G)} ]其中G表示针对同一个问题生成的不同回答数。这个组内标准化的做法让优势值有了相对意义某个回答如果比同组的其他回答更好就得到正向优势训练更新时鼓励这条路径如果更差就压低这条路径的概率。GRPO在数独这个场景下尤其合适因为数独是一个多解的推理空间同一个谜面采样出的8个回答之间天然存在质量差异有的部分正确有的完全乱填组内相对比较可以提供非常清晰的学习信号而且省掉了critic模型在NPU上调优的时间成本。1.3 为什么非要放在昇腾NPU上跑说实话现在主流的LLM训练和强化学习工具链几乎都默认跑在GPU生态上很多团队在做RL训练时根本没意识到换个硬件底座会牵扯出多少适配层面的问题。我选择昇腾NPU动机其实很朴素想验证一套不依赖GPU生态的LLM强化学习训练链路能不能跑通以及GRPO这种算法在国产加速卡上的实际表现到底怎么样。GRPO的算力诉求很亲民。以数独任务为例一个基座模型的推理规模控制得当的话单卡就能完成组内采样和策略更新对显存压力远小于传统PPO加价值网络的组合。这意味着用NPU跑强化学习训练并不需要顶级的算力卡中低端规格就已经具备可行性这对很多预算有限的研究团队和中小公司来说是个重要信号。2. 环境准备与昇腾NPU适配2.1 NPU上跑LLM训练前要搞清楚的几个差异在动手之前先要理解NPU和GPU在使用习惯上的差异。对我这种习惯了GPU为主力训练设备的人来说切换到NPU第一个要适应的就是“算子支持边界”这件事。GPU生态里很多深度学习算子开箱即用但在NPU侧第三方算子库对模型训练框架的支持是通过适配层完成的覆盖面会落后一些。所以在选模型和设置算子时最好提前确认自己用到的算子是不是都有相应实现。第二个差异来自于执行模式。NPU侧通常有两种执行思路一种是动态图模式和PyTorch默认行为接近写起来灵活但性能相对低另一种是图模式会把整个计算图编译优化后再执行性能高但对动态shape比较敏感。强化学习训练恰好是一个shape极其动态的场景因为每轮采样出的文本长度不固定送到训练环节的token数也不同。我自己的经验是rollout阶段用比较宽松的动态配置训练阶段尽量把序列padding到固定长度否则图模式编译会反复触发速度直接掉一个数量级。2.2 依赖与环境搭建要点这次训练用到的底座是一个7B规模的开源Causal LM这种规模在单张NPU上做推理采样还能接受训练时用LoRA进一步压缩了显存压力。软件层面主要包括模型训练框架、NPU算子适配层、以及推理采样用的执行引擎。环境搭建虽然过程比较琐碎但关键就两条版本匹配极其重要适配层版本和模型训练框架主版本必须严格对齐否则会出现“算子找不到”的诡异报错。NPU显存和GPU显存的使用习惯非常接近可以用环境变量控制空闲显存比例但要注意采样阶段和训练阶段不宜完全共享显存建议预留一部分给rollout推理。我在第一次启动训练时把所有显存都分配给了模型权重结果一进入采样阶段就碰到显存申请失败。后来预留了大约20%的显存给推理采样模块整个循环才算顺畅跑起来。2.3 数据与模型输入格式设计数独任务的输入输出格式直接决定后续奖励计算的复杂度这里花点心思是值得的。我使用的盘面表示方式是数字0代表空格1到9代表已填数字。序列格式大概是这样的数独谜面 530070000...81个字符 请补充盘面缺失的数字只输出结果盘面。刚开始我犯了个小错误用数字“0”既代表空格又参与盘面文本序列导致模型在生成时经常把空格和真实数字“0”混淆。后来把空格位改成英文句点“.”之后情况好了很多因为“.”在数独场景里语义极其清晰模型输出解析也省事。模型的输出长度限制在128到256个token以内因为一个完整盘面只需要81个数字加上少量结构化标记。把输出长度锁死不仅加快采样速度也避免了NPU图模式下动态shape带来的额外开销。3. 数独环境与奖励设计3.1 谜面生成的几种方法与难度控制训练数据的来源不能只用现成题库数量不够且难度分布不受控最好还是自己生成。生成数独谜面的第一步是随机生成一个完整终盘。我的逻辑是先准备一个标准终盘然后通过随机交换行、列、宫的方式生成足够多样化的终盘。第二步是挖洞也就是把终盘里的部分数字置为空格。挖洞数量直接决定题目难度挖掉30个左右属于中等难度挖掉45个以上就接近专家级了。但这里有个关键约束挖完之后必须确保题目有唯一解。理论上数独题应当保证唯一解否则强化学习环境里的奖励函数会出现不稳定——同一个盘面有两种正确答案模型输出其中一种却被判错这会让训练信号非常混乱。所以我用回溯法求解器对每道题做唯一解校验不满足唯一性的题就丢弃。跑起来的时候我会特意让生成器把中等难度和较难题目的比例控制在7比3左右避免模型在单一难度分布里过拟合。3.2 答案校验函数的实现细节答案校验是整个训练管线的裁判它出错的话后边的所有学习信号都白搭。校验函数需要做三件事。第一解析输出。模型输出可能包含“思考过程”、“解释文本”等额外内容我会先用正则把所有非数字字符统一处理只提取候选盘面中的81个有效数字。第二对比初始谜面。任何空格位置填上了数字是允许的但任何原始提示数字被改动都算非法直接给0分。第三规则检查。逐行、逐列、逐宫检查是否有重复数字同时检查是否填满了81个格子。这里我强烈建议用矩阵化方式批量处理不要在Python里写三层循环逐格检查否则训练速度会被校验函数拖垮。把81个数字reshape成9x9矩阵后行、列、宫的重复性检测可以各自用集合运算一次完成批量场景下渲染成向量化运算就更好了。3.3 奖励函数怎么设计才不至于误导模型奖励设计是我在这个项目中反复调整最多次的部分直接决定训练能不能收敛。我最初只设置了一个二值奖励最终盘面完全正确得1分否则得0分。这种极稀疏奖励在理论上能工作但实际训练时模型前期几乎没有正反馈强化学习算法很容易在随机探索里原地打转。后来我改成混合奖励合法性奖励如果模型输出的候选盘面没有违反行、列、宫约束就奖励0.4分。这个奖励鼓励模型先学会“不犯规则错误”。填满率奖励按模型实际填出的格子数量占81格的比例给分满分0.3。这个奖励鼓励模型尽量完成整个盘面而不是填到一半就放弃。最终正确奖励如果盘面完整且与标准答案一致再追加0.3分。这样三个分数加在一起总奖励分布在0到1之间。模型前期不需要碰运气撞到完整答案只需要先学着构造一个不违反规则的盘面就能获得部分奖励这相当于给探索过程放了几个路标。实际跑起来这种软化的奖励梯度确实比纯二值奖励收敛快了很多模型不会因为几千步内拿不到1分就彻底躺平。这里有个值得注意的教训不要奖励“中间推理步数”。我一开始尝试过给模型每一步落子的次数加分结果是模型学会疯狂分行输出token数量暴涨但正确率没有任何提升。强化学习优化的是你定义的奖励如果你在奖励里放了可钻的空子模型一定会把它钻爆。4. GRPO训练流程实现细节4.1 Rollout采样阶段怎么让模型生产一组可比较的回答训练循环的第一步是对同一谜面采样多个回答。这一步直接决定了GRPO组内优势计算的质量。我在rollout阶段采样数量G取8温度设为1.2关闭了贪心解码。温度调高是为了让同一谜面下输出多样性足够强不然组内8条回答长得一模一样相对优势计算就失去了意义。采样阶段要把算力效率提上来。数为题的一个谜面加上输出序列大概只有不到300个token所以可以用较大的并发采样数把一张NPU卡的推理吞吐跑满。实际操作中我会先把一批prompt拼接成动态batch送入推理等全部返回后再统一做奖励计算。这里要注意的是内存管理rollout阶段的临时张量非常多如果每个采样结果都保留完整logits显存会爆。通常只需要保存生成的token序列以及对应的对数概率。4.2 优势计算与策略更新的伪代码GRPO训练的核心循环以伪代码的形式呈现大概是这样for step in range(total_steps): prompts, puzzles next_batch() # 阶段1: 采样多个回答 outputs rollout(prompts, group_size8, temperature1.2) # 阶段2: 计算组内奖励 rewards [compute_reward(out, puzzle) for out, puzzle in zip(outputs, puzzles)] rewards np.array(rewards).reshape(-1, 8) # 阶段3: 组内标准化得到优势 advantages (rewards - rewards.mean(axis-1, keepdimsTrue)) / ( rewards.std(axis-1, keepdimsTrue) 1e-6) # 阶段4: 策略优化 policy_loss -advantages * output_log_probs # 阶段5: 加入KL散度约束 loss (policy_loss kl_coef * kl_divergence).mean() loss.backward() optimizer.step()这里有几个容易被忽略的细节。第一个是用np.float32对rewards做组内标准化而不是使用默认的float16不然几个小数值的差在低精度下可能直接变0。第二个是KL散度约束GRPO虽然不像PPO那样强制裁剪但依然需要在旧策略和新策略之间控制距离防止模型在某个偶然拿到高奖励的样本上突然走偏把之前学到的合法格式全部忘光。4.3 训练超参数与收敛曲线观察我训练时使用的超参数如下参数推荐值说明组内采样数G8太小组内优势不稳定太大会拖慢每一步采样温度1.2鼓励探索但又不会生成太多乱码学习率1e-5基座模型继续训练时不宜过大训练轮数300-500步数独任务不需要长训练最大输出长度256 tokens限制动态shape节省显存batch大小8个谜面每个谜面再采样8次整体为64条序列收敛曲线是观察训练是否正常的最直接窗口。我跑下来的曲线特征非常鲜明前一两百步奖励几乎趴在0附近偶有波动然后突然在某个step附近出现一个明显的跃升之后慢慢爬向0.8以上。这种“乍看没反应、忽然开窍”的模式在数独强化学习里很常见因为模型需要足够多样性的探索样本才能偶然撞到完整合法盘面再从中尝到甜头。如果你发现三四百步之后奖励依然完全没有向上趋势不要盲目加大学习率先检查rollout采样的多样性是不是太低了。4.4 判断模型真的学会数独了没有训练结束后评估模型是否“学会了”数独不能只看训练集上的平均奖励否则模型有可能记住了部分谜面的解法。我会用完全独立的测试题集去评估计算三个指标完整求解率、合法率、平均填充比例。完整求解率是最硬的标准代表模型在没有任何外部提示的情况下独立给出正确答案的比例。合法率代表模型生成的盘面是否符合数独规则但不要求完全正确。平均填充比例则反映模型是否愿意尝试完成整个盘面。如果完整求解率不到20%但合法率很高说明模型学会的是“构造一个合法的伪解答”而不是“推理出真正的唯一解”。这个区别很重要它意味着模型可能只是学了个表面规律并没有真正掌握数独的全局约束。我训练出的模型在这三个指标上的表现大致是完整求解率30%到40%合法率70%以上平均填充比例90%以上。作为一个纯RL跑出来的结果已经足够说明算法在学但离“可靠解数独”还有明显差距毕竟模型的推理能力还是会受到参与数量和训练规模的限制。5. 常见问题与排查实录5.1 训练停滞奖励一直为零怎么办这是几乎所有RL项目新手都会撞上的问题我也不例外。一上来用纯RL训练模型在rollout阶段生成的全是乱码盘面非法率接近百分之百奖励永远为0算法拿不到任何梯度方向。排查思路分两步。第一步看采样多样性如果温度设置太低模型每次都输出几乎一样的内容探索空间太小自然碰不到正确样本。把温度提高到1.2到1.5之间往往能缓解。第二步如果多样性没问题就得考虑是不是任务本身在纯随机策略下的成功率太低了。数独这种任务随机填满一个盘面还要恰好合法的概率微乎其微纯随机探索可能永远撞不到奖励。这时候最有效的办法是做短时间的SFT预热。用一小批现成的数独解让模型先学会“按格式输出一个合法盘面”哪怕求解率不高至少探索时更容易落到奖励非零的区域。我在实践中发现加了几百条SFT样本做预热之后GRPO训练奖励上升的速度快了一倍不止。5.2 组内奖励方差太高导致训练震荡GRPO的优势计算依赖组内相对比较。如果某一条回答的奖励远高于组内其他几条它的优势值会被拉得特别大这次更新就会用力过猛体现在loss曲线上就是突然的尖峰。解决这个问题的办法有两个。一是把组内样本数G从4增加到8甚至12让极端值的影响被稀释。二是在rollout时做一下温度控制防止某一组样本里出现大量随机乱码而另一条恰巧碰对了完整答案。更稳妥的做法是给奖励函数加上上限比如把最终正确奖励从1分降为0.5配合合法性奖励一起让奖励分布更平滑。GRPO不是一定要用原始分数经过适当尺度调整的奖励往往更容易训练。5.3 NPU适配层下动态shape带来的性能隐患这是昇腾NPU场景下独有的坑。模型训练框架在NPU上跑时如果输入数据的shape频繁变化图模式会反复触发重新编译每一步都要等很久。我的数据loader一度使用动态padding每个batch长度都不一致训练速度慢到无法接受。解决办法是显式固定训练阶段的输入长度。把所有训练样本统一padding到512个token超出部分截断不足部分用padding标记补齐。这样每一步的model forward输入shape都是固定的图模式只需编译一次。rollout阶段则保持动态长度因为推理采样的开销大头在自回归生成对编译重触发没那么敏感。5.4 多卡并行时的负载不均衡如果手头有多张NPU卡想把组内采样切到不同卡上并行会遇到负载不均的问题。现象是某张卡的显存占用远高于其他卡训练一步要等最慢的那张卡跑完才算完。根本原因是采样阶段每条序列长度不同而动态batch的padding长度由最长样本决定分配到不同卡上的样本长度分布不一致导致计算时间差异很大。我这里建议做负载均衡时不要只按样本数量切而是按预估长度总量去切或者直接把所有样本统一padding到固定长度后再分卡。虽然固定长度会浪费少量算力但在多卡并行场景下算力闲置总比同步阻塞要好。6. 这个项目带来的影响与下一步扩展6.1 验证一条可行的NPU强化学习链路这个项目的意义首先在于证明了一套完整的LLM强化学习训练链路在昇腾NPU上是可以跑通的不依赖特定硬件生态。GRPO因为不需要价值网络将强化学习训练的资源需求降低了很多恰好让国产加速卡在自身算子覆盖还有一定局限的情况下也能胜任LLM的后训练任务。这给我一种感觉是未来国产加速卡的大规模应用未必非要等所有预训练场景都全部适配好先从强化学习、推理优化这类后训练环节切入可能更务实可行。6.2 从数独扩展到更通用的可验证任务数独的训练流程写完后全部工作其实由三部分构成环境构造、奖励计算、GRPO训练循环。这三部分都具备很强的迁移性。把数独环境替换成数学应用题、SQL查询生成、代码生成等同样具备外部验证条件的任务整套训练代码改动量其实很小。我在项目完成后试过把奖励函数换成简单的“数学结果是否正确”的判断其他部分原封不动也能跑出正常的奖励上升曲线。这说明数独项目训练出来的不只是一个会解数独的模型而是一套通用的“规则感知→策略搜索→结果验证”强化学习训练模板。6.3 开源协作与后续演进方向最后说说后续。我在整理这套训练流程时已经把数独数据生成、奖励计算、GRPO训练脚本做成了模块化结构方便替换进其他任务。下一步我打算补充更多带约束推理场景的实验比如跨越多步骤的规划类任务同时也会尝试用更长时间的训练观察模型能不能在数独完整求解率上突破50%。如果哪天这套流程磨得更顺了再考虑把细节整理成一份可直接试跑的工程配置放出来。对有兴趣尝试的朋友我建议从单卡起步先把环境跑通再去折腾多卡优化这条路我替你探过走起来不会太绕。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑