RLinf+SGLang:打造大模型微调评测高效闭环 训练一个模型到能上线真正的瓶颈往往不在训练本身而在训练完后怎么快速知道它有没有变好。我们做 Cosmos3 时就被这个问题卡了很久最后用 RLinf 加 SGLang 把微调、生成、评测揉成一条流水线才总算把这个闭环跑顺了。这篇记录一下我们自己的集成过程包括踩过的坑和现在稳定使用的流程给在后面做类似工作的朋友一个参考。先说清楚这套东西是干什么的。Cosmos3 是我们这边一个多模态推理模型的代号底座是一个开源模型我们针对特定的问答场景做微调。微调用的框架是 RLinf一个做强化学习训练编排的框架评测和 rollout 生成部分用的是 SGLang一个高性能推理引擎。整个链路的大致逻辑是用 RLinf 做策略更新用 SGLang 负责模型推理打分样本的生成再拿生成结果和 reward 模型做比对最后把评测也挂到同一个推理后端上省掉模型反复加载的时间。适合看这篇内容的人正在做 LLM 微调到评测闭环的工程师、想把 RL 训练流程跑通的研究生、以及在推理框架选型上犹豫不决的团队。下面全是实际动手的细节没有广告也没有空话。1. 项目背景与选型Cosmos3为什么需要微调评测一体化1.1 项目遇到的三个真实问题Cosmos3 这个项目一开始并不是一个复杂工程。最初就是拿一批领域问答数据对底座模型做监督微调效果还行。但做到第二轮迭代时发现三个问题。第一个问题是评估周期太长。我们的数据标注方在另一个城市反馈节奏是按周走的。我们训练一轮大概花一两天但评测要另起炉灶加载模型、跑测试集、手工看结果整个周期要三四天。等到发现这版模型在某些问题上反而变差了已经是下周了。这种节奏想跑强化学习迭代基本不可能。第二个问题是 roll out 生成的效率太低。做 RL 微调需要在每轮更新前大量采样策略模型的输出用传统的方式写一个生成脚本直接调用模型一次生成两三万条样本要跑大半天。而且这批样本还只是中间产物后面要喂给 reward model 打分再算 KL 散度整个链路里有大量重复的模型推理开销光是 wait 就让人崩溃。第三个问题是生成和评测环境不一致。之前我们训练时用一套采样设置评测时又换了一套参数结果模型在训练里表现正常到了评测集上分数莫名其妙下降。找了一圈才发现是温度系数和 top_p 的设置差异导致的。同一份权重不同解码参数输出分布完全是两回事。这三个问题的交集指向同一个解决方案把训练、生成、评测统一到同一个推理后端上并且把评测流程从人工拉长线改成自动挂到训练循环旁边。1.2 RLinf和SGLang的分工逻辑选型的时候我们比过好几个方案。最终留下 RLinf 和 SGLang 的组合核心原因是两者的分工够清楚。RLinf 本身的定位是训练编排层。它管的是数据批次怎么组织、策略模型怎么更新、KL 系数怎么调、奖励信号怎么汇聚。它不关心底层的生成怎么加速也不关心评测集怎么跑。这样我们可以在不改训练逻辑的前提下把推理后端换掉。SGLang 则是纯粹的推理执行层。它在批处理上的优势尤其是长文本生成场景下的调度优化比一般的 transformers 原生实现要快不少。我们的 rollout 样本大多是几百 token 的长回答正好踩在 SGLang 的优势区间上。两者之间的通信协议也简单。RLinf 把一组 prompt 发给 SGLangSGLang 以 OpenAI 兼容的接口返回生成结果reward model 再对结果打分最后把分数反馈给 RLinf。整个过程没有复杂的自定义协议出了问题也好排查。提示如果你们的场景只是单机小规模实验不一定需要把所有组件都上。RLinf 加 SGLang 的组合适合训练和评测要反复跑多轮的中大型迭代场景小项目反而会增加部署成本。2. RLinf微调环节从数据管线到策略更新2.1 训练数据组织与特殊情况处理RLinf 对训练数据的要求不算苛刻但数据组织方式直接决定后面能不能跑顺。我们实践下来最容易踩坑的是 prompt 的格式统一性。Cosmos3 的数据大概是这个结构一个 query 对应一个标准答案外加若干条人工标注的部分正确样本用来构造对比信号。RLinf 在 RL 训练阶段需要的是同一个 query 的多条候选回答所以我们把数据组织成 group 的形式每个 group 包含同一个 prompt 的 8 条不同生成结果。这里有个细节容易忽略reward model 的输入格式和策略模型的输入格式经常不一样。我们一开始把 reward model 的 prompt 模板和策略模型的 prompt 模板写成了同一个结果 reward model 打分一直偏低。后来调了半天才发现是系统提示词不一致导致的。建议在数据管线阶段就把两种模板分开维护并且每次训练前都做一次格式校验。训练数据里还有一类特殊情况答案中含代码块。因为 Cosmos3 面向的是技术问答场景很多标准答案里有 Markdown 格式的代码片段。这类数据在 tokenize 时如果换行符处理不对生成的回答很容易在代码块中间断掉。我们的解决办法是在数据预处理时把代码块单独标记生成阶段对这部分区域设置更高的 max_tokens 余量。2.2 奖励模型的接入与打分对齐RLinf 接入奖励模型的方式有两种一种是通过接口调用外部 reward model一种是直接加载到同一个进程里。我们选了第一种因为要反复调试 reward model 的权重独立部署更灵活。对接时要特别注意打分对齐问题。最开始我们拿一条已知优劣的样本测试发现 reward model 给出的分数区间和训练脚本里预设的 reward 范围完全对不上。训练脚本假设 reward 是 0 到 1 之间的值但实际 model 输出的是 -5 到 5 的分数。RLinf 默认不做缩放直接把原始分数当成 reward 信号用KL 惩罚的权重就失效了。这个问题的修复不难难在发现。我们把 reward model 输出的原始分值做了一次 min-max 归一化到 0 到 1 区间再做 group 内部相对排序最终喂给训练脚本的是排序后的相对回报。比起绝对值很多人都建议用相对奖励来训练策略模型这在实践中确实更稳。2.3 训练参数与稳定性控制训练参数这块我们花了不少时间调。直接放一组我们最终稳定使用的配置参数供参考参数取值说明基础模型开源底座模型LoRA 微调r64, alpha128学习率1e-5比监督微调低一个量级KL 系数0.01控制策略偏离参考模型的程度group size8每个 prompt 生成 8 条候选温度系数0.7rollout 生成阶段的采样温度top_p0.9与温度配合使用max_tokens1024单条回答的上限梯度累积4 步等效 batch size 更大这组参数跑了两周训练过程基本没有出现崩溃或 loss 爆炸。最值得说的是 KL 系数的选择。我们看到有不少教程建议 KL 系数设 0.1 甚至更高但在我们任务上这个值会导致输出多样性急剧下降生成结果各种复读机。0.01 这个值在稳定性和多样性之间平衡得比较好。另外一个经验是 rollout 阶段和训练阶段的温度要严格一致。如果 rollout 用 0.7训练阶段为了省时间把温度调成 0模型学到的分布就歪了。这在 SGLang 接入之前我们吃过亏后面会细说。3. SGLang接入评测提速的实操记录3.1 部署方式选择与请求协议SGLang 的部署方式我们有对比过几种方案。最初是直接命令行起一个 OpenAI 兼容服务后面为了和 RLinf 的 batch 逻辑对齐改成了自定义启动脚本通过同一套 HTTP 接口读写。启动 SGLang 服务的关键参数里我觉得有几个值得单独说。--chunked-prefill-size这个参数直接影响长 prompt 的并发能力。之前没设置时一旦请求的 prompt 长度超过某个阈值服务端就会把整条 prompt 一次性算完再处理下一批GPU 利用率掉得很厉害。设置了 chunked prefill 之后长 prompt 会被切成小块和 decoding 阶段混合调度吞吐提升明显。我们的 prompt 平均在 1200 token 左右设置大小为 4096 比较合适。另一个参数是--max-running-requests。这个值决定了同时有多少请求在 GPU 上执行。设得太大容易 OOM太小则吞吐上不去。我们实测 80GB 显存下max-running-requests 设为 256配合 batch size 动态调整可以稳定跑满利用率。接口协议我们用的是 OpenAI 兼容格式因为 RLinf 和评测脚本都可以直接复用同一个 client。这对调试是个巨大便利curl 一条请求就能复现问题不用去翻训练框架的内部日志。3.2 并发评测与结果对齐评测这一层是我们改造最大的地方。过去评测是一个串行脚本一条一条把测试集的 prompt 喂给模型等全部跑完再统计分数。SGLang 接入后我们改成了并发模式一次性把整个测试集的所有 prompt 发给服务端让它在内部做 batch 调度再把返回结果和原 prompt 做 id 对齐。这个改动对需要多版本模型对比的场景特别有效。我们评测时会同时启动两个服务一个是上一版权重一个是当前训练中的权重两个服务并行跑同一份测试集。最后用脚本按 prompt id 做 merge直接生成 diff 报告。这样每轮训练结束后大约 10 分钟就能看到新版模型相对旧版的分数变化。要提醒大家的是并发请求的结果顺序是不确定的。SQLite 或者 JSONL 文件里如果只按返回顺序存储最后合并的时候大概率对不上。我们一开始就犯了这个错评测分数忽高忽低查了半天才发现是结果错位。后来在每条请求里带上 prompt id响应里也要求返回同样的 id对齐问题才算彻底解决。3.3 评测流程与训练循环的衔接评测流程和训练循环怎么衔接是这个项目里改动最多的部分。最初的流程是训练完保存权重然后手动启动评测脚本评测完看一眼结果不好再调参重新训练。整个过程是断开的。后来我们把评测挂到了 RLinf 的 checkpoint 回调上。每次训练跑完一个 epoch 或者达到指定的保存步数RLinf 会把当前权重推送到评测服务评测服务自动跑一遍测试集把结果写入评测数据库。这样训练和评测完全联动了我们只需要在早上看一眼头天晚上的结果。这里有个经验评测服务和训练服务要尽量用同一个 SGLang 后端区别只在于加载的权重不同。因为就算参数完全相同不同推理引擎的采样结果也可能有小幅差异这种差异在分数上会被放大。统一后端之后评测分数基本只反映模型权重的变化排除了环境因素。4. 集成链路中最容易翻车的几个点4.1 版本兼容与tokenizer一致性集成过程中我们遇到的最隐蔽的问题是 tokenizer 不一致。具体场景是这样的RLinf 加载模型时用的是 transformers 的 AutoTokenizerSGLang 服务端加载同一个模型时也会初始化一份 tokenizer 配置。正常情况下两者应该是同一套。但因为我们训练时用了 LoRA保存的 checkpoint 里 tokenizer 配置被额外写入了一些 special token导致 SGLang 用旧配置加载时生成时对特殊 token 的处理方式不一样出现了回答里凭空多出换行符、或者代码块结束符丢失的现象。排查过程比较痛苦因为单看哪一边都像是正常的。最后是直接对比两边的 tokenizer 配置发现一个是加了 20 个 special token 的新版一个是旧版。修法是统一从训练输出目录加载 tokenizer而不是从 base model 目录加载。这一点看着小实际上非常影响生成质量。建议在写集成脚本时直接把 tokenizer 路径强绑定为训练产物的路径不要留两个入口。4.2 中间状态与缓存管理第二个高频事故集中在缓存管理。RLinf 在训练过程中会缓存生成的 rollout 样本SGLang 这边也有 prefix cache。两个缓存如果清理时机不对会带来两个方向的坑。先说 RLinf 这边的缓存。它会把每轮的 rollout 结果落盘然后下一轮继续追加当训练跑了几十轮之后磁盘占用会非常夸张。我们的训练任务跑了一周缓存目录占了快 300GB。更麻烦的是如果缓存没清理干净下一轮训练重新启动时会读到旧的 rollout 数据采样分布被污染训练曲线出现奇怪的周期性波动。这种问题表面上看是训练不稳定实际是缓存串了。SGLang 的 prefix cache 是另一个情况。它会缓存 prompt 的前缀公共部分本意是加快重复前缀的请求。但 RL 训练的 prompt 每次都会注入不同的系统提示或者少量 prompt 差异导致前缀缓存 hit 率极低反而增加了内存开销。我们最后在训练场景下直接禁用了 prefix cache生成的吞吐反而升了一些。4.3 采样参数导致的分数波动这是所有坑里最影响对模型判断的。有段时间我们发现同一个权重连续评测两次分数能差 3 到 5 个百分点。一开始怀疑是服务端状态问题重启了好几次都没用。后来仔细对比请求参数才发现评测脚本里用的 temperature 是 0.3而训练阶段 rollout 用的是 0.7。严格来说评测阶段用更低的温度并没有错很多团队甚至会设成 0 来追求确定性。问题在于我们拿评测分数去和训练阶段的 reward 分数直接对比两者的生成分布完全不同导致分数下降到底是真的模型变差还是只是采样变保守了这个问题根本没法回答。我们的解决方法是把评测分成两个环节第一个环节用与训练一致的采样参数temperature 0.7, top_p 0.9跑生成式评测用来和训练信号对齐第二个环节再用 temperature 0 跑确定性基准测试用来衡量模型的真实上限。两份分数分开看不再混为一谈。注意任何涉及这版模型比上版好还是差的判断都必须保证两次评测的采样参数完全一致否则结论就是噪声。5. 效率收益与可复用的流程建议5.1 从训练到评测的耗时对比这一套串起来之后效率变化相当明显。直接放一组我们改造前后的数据对比环节改造前改造后训练一轮数据准备手动脚本约 2 小时RLinf 自动管线约 20 分钟rollout 生成 2 万条样本串行生成约 6 小时SGLang 并发生成约 1.5 小时评测一轮测试集手动启动约 3 小时自动挂载约 10 分钟多版本对比手工导出约 1 天自动 diff约 30 分钟从一轮迭代 3 到 4 天压缩到半天以内。这意味着我们可以在一天之内跑完训练、评测、分析、调参的完整循环。对于强化学习这种需要反复实验的训练方式来说这个周转速度基本是决定项目能不能做下去的硬指标。要说最明显的收益不是某一个环节提速了而是整个链路变成了一条直线没有人工中转没有等待间隙人和机器都在干最有价值的那部分活。5.2 后续扩展思路跑通这套流程之后我们已经在规划几个方向的扩展。第一个是评测集动态扩展。现在评测集是固定的一份后续想把每次训练中 reward model 打分差异最大的样本自动追加到评测集里。这样评测集能跟着模型的问题区域走而不是永远停留在最初的几百条人工用例上。第二个是多个 reward model 的并行打分。目前的管线只接了一个 reward model后续想同时接两到三个从不同维度比如正确性和风格偏好分别打分再合成最终 reward。SGLang 侧不需要改动RLinf 侧需要把打分逻辑改成可配置的多路聚合工作量主要集中在数据层。第三个是多机并发 rollout。SGLang 本身就支持多节点部署RLinf 目前还没有完全吃透这个能力。如果能把 rollout 请求负载均衡到多台机器上生成瓶颈会被进一步打破。这一步的困难主要在 RLinf 的调度策略上SGLang 本身就是分布式友好的相对好搞定。就我们目前的使用体验来说RLinf 加 SGLang 这个组合的集成难度属于中等真正的成本不在工具本身而在把两套系统的状态对齐、参数对齐、数据对齐。只要这三点做到位了后面的训练迭代会顺很多。