RL训练瓶颈在推理?独立扩展推理系统的工程实践

RL训练瓶颈在推理?独立扩展推理系统的工程实践 一个很常见的局面RL 训练任务已经跑起来了但训练卡的利用率却一直上不去。打开监控一看训练器在等采样队列推理服务的响应时间越来越长几千条 prompt 排队等着被生成。团队的第一反应通常是加训练卡、调并行策略但真正的问题不在训练而在 inference——RL 的训练循环已经被推理环节卡住了脖子。如果把“让模型学会一个策略”比作一场需要持续试错的实验那么 RL 里每一步策略更新都依赖模型先生成大量尝试结果。这些尝试结果不是从文件里读出来的而是当前模型依靠前向计算“推理”出来的。模型权重每更新一次旧尝试结果作废新一批尝试又要重新推理。于是训练算力和推理算力不再是一前一后的关系而是紧紧耦合在了一起。推理一旦跟不上训练就只能空转。这篇文章想讲清楚一个判断RL 的瓶颈已经转移到推理侧解决方式不是继续盲目扩大训练集群而是要把推理当成一个可以独立扩展的系统来对待。下面会拆开来说为什么推理会成为瓶颈、独立扩展到底指什么、工程上应该怎么落地以及哪些情况下不要急着做这件事。1. 为什么 RL 的瓶颈正在从训练转向推理1.1 推理不是“评估动作”而是“生产数据”很多人刚接触 RL 时容易把它想成一种类似监督学习的过程准备好一批数据模型读进去算出 loss更新梯度。但 RL 的核心不同在于模型需要不断和环境/奖励模型互动用当前策略去生成新数据。这个生成过程本质上是模型推理。在 ChatGPT 类模型的 RLHF、RLAIF 流程里典型循环是给模型一批 prompt模型为每个 prompt 生成若干条回复或动作轨迹奖励模型或规则函数为这些轨迹打分用这些分数更新模型参数模型参数变了下一轮的全部轨迹重新生成。第 2 步就是批量推理。它不只是评估一下“这个动作好不好”而是真正在制造训练数据。没有足够多、足够多样的推理输出RL 更新就成了“无米之炊”。这也是和 SFT 最大的区别SFT 用的数据集是固定的读一遍可以反复用RL 用的是当前模型现场生成的数据模型一变旧数据的价值就大幅衰减甚至完全无效。所以 RL 越往规模走推理就越像一个“隐性数据中心”承担着源源不断生成样本的任务。1.2 长序列生成把推理成本进一步放大如果只是像普通分类任务那样输入一堆状态输出一个动作或一个概率分布推理成本也许没那么吓人。但今天很多 RL 场景是语言模型任务尤其是带长思维链的推理型模型。这类模型一次生成可能不是几十个 token而是几百、上千甚至更多。生成每一个 token 都需要一次前向计算而且下一个 token 的生成依赖前面所有 token没法像训练那样整批并行塞进矩阵乘。再加上 RL 探索需要多样性同一个 prompt 可能要生成 4 条、8 条乃至更多候选推理总 token 数会成倍上升。这里也解释了“RL 冷启动”为什么会难。冷启动阶段模型还没有学会有效的探索策略推理出来的结果大多奖励很低。为了找到少量有信号的数据只能增加采样数量。可采样数量一上去推理更慢整个 RL 训练更卡。很多项目就卡在“越冷启动越需要探索越探索推理越吃紧”的死循环里。1.3 训练集群不能直接替代推理集群有人会想既然推理不够我就在训练集群上多跑几个生成进程不就行了实际落地时这条路通常走不远。训练阶段优化的是大规模矩阵乘法、梯度通信和多卡同步追求的是高吞吐、大批量、稳定流水。推理阶段优化的是低延迟、高并发、连续批处理要动态管理不同长度的序列、缓存和采样。两者的显存分配模式也不同训练往往需要存优化器状态和梯度推理需要存 KV cache。如果非要用训练节点扛 rollout短期跑通可以但一旦训练和采样同时争抢同一批卡就会出现训练等采样、采样又导致训练显存不足的相互拖累。资源利用率看起来不低实际产出很低。这也是为什么我更倾向于把推理从 RL 主循环里拆出去让它成为一块弹性可扩展的独立资源而不是训练进程里的一个子任务。2. “独立扩展推理”不是一句口号而是三层解耦标题里的“Scale It Independently”如果简化成“多给推理加几张卡”会失去原意。从工程视角看它至少包含三层含义。2.1 资源层把推理服务从训练进程中拆出来最直接的变化是架构层面的训练集群和推理集群不再混合部署而是各自独立。训练端需要不断计算梯度推理端需要持续生成 rollout。两者通过 API 通信。这样做的第一个好处是训练和推理可以各自按瓶颈扩展。如果模型策略更新很快、但采样速度不够就扩展推理节点如果经验池已经堆满、训练 update 跟不上就扩展训练节点。两者不会互相拖累。第二个好处是推理服务可以被多个实验复用。同一个 70B 模型服务今天给这个 RL 实验做 rollout明天给另一个评估任务用资源规划更灵活。它更像一个“模型能力服务”而不是某一个训练任务的附属模块。2.2 数据层从同步等待到异步采样很多 RL 训练慢不是单次推理慢而是同步等待太严重。训练器发出采样请求就阻塞在那里等推理服务返回推理服务为了凑满并发也会攒请求最终整个系统像堵车一样越走越慢。更合理的做法是异步采样推理服务持续从任务队列里取 prompt生成样本后写入经验池或消息队列训练器按自己的节奏从经验池里取样本更新策略策略更新完成后再把新的 prompt 需求放回任务队列。异步化之后推理和训练不需要严格同步。训练器不再空等推理服务也更容易跑满。但这里必须提醒一句异步不是一个无成本的选择。RL 讲究 on-policy也就是样本要来自当前策略。如果训练已经更新了好几版推理服务还在用旧版本的模型生成数据这些样本就会“过期”。因此工程上通常会给样本标记“策略版本号”训练器消费样本时检查陈旧度超过阈值就丢弃或降低权重。2.3 策略层推理自身的策略也可以独立优化除了资源和流程推理环节还能在“策略”上独立优化。这里的策略指的是推理时参数和生成策略。比如调整 temperature、top_p 来放大或收敛探索范围调整 max_tokens 控制生成长度调整 n_samples 控制同一条 prompt 的采样数量使用更长的思维链让模型在推理阶段多“想”几步提升输出质量。训练算法不需要改动只改推理侧的采样参数和推理次数就能改变数据分布。这也是 inference-time scaling 的思路在某些任务上不增加训练算力而是增加推理时计算量模型效果就能明显提升。更进一步还可以用不同规格的模型做“协作式 rollout”。比如用更大的模型为小模型生成高质量负样本或偏好数据再由小模型做 RL。这同样属于独立扩展推理能力而不是单纯增加训练规模。3. 把“推理独立扩展”落到工程四个关键动作讨论完概念接下来是落地时最常见的四个动作。3.1 先量化怎么确认 RL 真的被推理卡住不要一上来就搭服务、拆架构。先把现状量化。一个最简单的判断方法给训练循环加日志统计每轮 step 的总耗时以及其中等待 rollout 的时间占比。# 伪代码统计一个训练 step 中 rollout 等待时间占比 step_start time.time() trajectories sample_from_policy(prompt_batch, n_samplesN) sampling_wait time.time() - step_start # ... 计算奖励、训练更新 update_time time.time() - step_start - sampling_wait print(fsampling_wait{sampling_wait:.2f}s, update_time{update_time:.2f}s)如果采样等待时间在总耗时里的占比超过一半而且训练卡本身没有被打满那基本可以判断瓶颈在推理侧不在训练侧。还可以看更多指标推理服务吞吐tokens/s推理服务队列深度平均值和 P99 值每条样本平均生成 token 数采样成功率和重试率训练器实际等待时间。这些指标可以帮助判断到底是“生成太慢”“队列太长”还是“服务不稳定”而不是笼统地归结为“训练太慢”。3.2 把 rollout 打包成服务确认瓶颈后最常见的改造是把采样函数从训练进程里抽出来变成一个独立服务。在常见实践里可以用 vLLM、SGLang、TensorRT-LLM 等推理引擎部署一个兼容生成接口的服务训练端通过 HTTP/gRPC 发送请求。也可以自研一个轻量服务内部包装推理引擎。伪代码结构类似# 伪代码把 rollout 发给独立推理服务 def rollout(prompt, server_url, n_samples, max_tokens, temperature): payload { prompt: prompt, n: n_samples, max_tokens: max_tokens, temperature: temperature, top_p: 0.9, } return request_inference(server_url, payload, timeout120)这里有几个工程细节值得注意请求要有超时和重试否则单条请求卡住会拖垮整个训练循环要对请求做限流避免一次性塞太多请求导致推理服务 OOMprompt 的格式必须和训练时完全一致包括 system prompt、角色标签、分隔符等否则采样分布会漂移返回的样本要带上模型版本号、采样参数、时间戳等元信息方便后续训练端做数据过滤和实验复现。3.3 推理参数和吞吐调优推理服务部署完后重点就变成“如何在相同资源下产出更多有效样本”。以下表格是一份通用参考具体参数要根据环境和引擎能力调整参数/配置影响常见建议n_samples同一条 prompt 生成多少条直接决定数据量和推理成本先用 1~2 条验证流程再逐步提升max_tokens生成长度上限决定单样本耗时按任务需要设置不要无限放长temperature / top_p探索程度过高会导致输出发散过低会导致多样性不足RL 探索阶段可适度调高稳定阶段再收窄连续批处理同时处理多个请求提升 GPU 利用率确保推理引擎开启连续批处理或类似能力前缀缓存固定 prompt 前缀可以复用 KV 缓存大幅减少 prefill 时间任务模板固定、大量相同前缀时收益最大量化FP8/INT8/AWQ 等可以降低显存占用但可能影响输出分布先做一致性测试再大规模启用队列/超时控制请求拥堵避免雪崩设置合理超时和指数退避重试需要特别提醒的是提升推理吞吐不等于提升 RL 效果。如果为了跑得快把 temperature 调得过低生成样本同质化严重训练曲线可能非常平稳但真实任务指标提升有限。反过来如果为了多样性把 temperature 拉得过高奖励分布可能一片噪声训练更难收敛。最好的做法是先用小批量验证不同采样参数下的奖励分布和训练曲线再决定最终配置。3.4 监控和可复现独立扩展推理后系统节点变多排查问题的难度也会上升。所以监控和元数据记录必须从一开始就做好。至少需要记录推理请求量、队列长度、P99 延迟、吞吐每条 prompt 生成的样本数量、成功率平均生成长度、平均奖励分数样本对应的策略版本号训练端消费样本的速度和丢弃比例。训练曲线、奖励曲线、推理吞吐曲线和资源配置最好能放在同一个看板上。这样每次推理资源或参数变化都能快速判断是“效果变好了但成本变高”还是“效果根本没变只是在空转”。4. 千万别把“独立扩展”做成无脑堆卡边界与坑“独立扩展”是一件听起来很酷的事也很容易被过度执行。只要判断失误就会把一个本可以用简单方式解决的问题搞成一套复杂的分布式流水线。4.1 什么时候不用独立扩展如果只是在单卡上做 toy 任务或者跑一个最多几分钟的小实验不要急着搭推理服务。进程内直接生成、同步等待反而更简单也更容易定位问题。还有一类场景也不需要在线推理服务如果任务允许使用固定数据集的离线 RL或者可以接受旧策略生成的离线样本那么推理节点不在训练的关键路径上。这种情况下更多的推理算力无非是让离线数据多一点但收益可能有限。判断标准很简单训练一次 step 只有几秒且 rollout 占比不高实验规模小模型只有几 B只是验证某个算法改进不需要大规模探索团队没有充足的人力维护独立服务。这些情况下过早做架构解耦弊大于利。先跑通最小流程再按数据增长和卡顿情况决定要不要拆。4.2 异步扩展会引入数据陈旧问题异步采样最大的好处是快最大的风险是策略不一致。如果训练器更新了 10 次策略而推理服务还在用 5 个版本之前的模型生成样本这些样本的价值就大打折扣。对于 PPO 这类 on-policy 方法严重滞后会造成梯度估计偏差甚至发散。一个可行的处理方案是给样本打版本标签并设置陈旧度阈值# 伪代码检查样本是否过期 def is_stale(sample, current_policy_step, max_stale_steps3): return current_policy_step - sample[policy_step] max_stale_steps超过阈值就丢弃或降权。这样能在吞吐和样本有效性之间做平衡。不过具体阈值多少合适取决于任务对采样分布变化的敏感度需要实验验证。4.3 推理引擎的输出分布可能和训练进程不一致分布式推理引擎为了保证吞吐常常会改变批处理方式、内存管理、算子实现甚至采样器。同一个模型在 PyTorch 原生推理和 vLLM/SGLang 等引擎里输出的概率并不一定完全一致。如果 RL 训练前没有做一致性验证训练曲线突然变化不一定是算法出了问题很可能是推理阶段生成分布变了。我建议在切到独立推理服务时先做一次对比实验同一个 prompt 集合、同一组采样参数分别用原来的采样方式和新的推理服务生成结果比较输出文本/分布的差异奖励分数的均值与分布生成 token 数量和失败率。如果有明显差异优先统一采样器实现、随机种子或 dtype再继续扩展。4.4 成本与利用率问题独立扩展意味着推理服务可能要单独占用一批 GPU 资源。如果任务不是持续产生高负载这批资源很容易闲置成本反而更高。更合理的做法是弹性伸缩根据队列深度或请求延迟自动调整推理实例数量。任务高峰期多开几个副本低谷期缩容回最小实例。也可以把推理服务给多个任务共用提高资源利用率而不是每个 RL 实验都独占一整套推理集群。成本控制还要落在请求级别不是所有任务都需要 n_samples 拉到很大也不是所有 prompt 都需要 max_tokens 给满。要给每个任务设置合理的资源预算避免一次实验烧掉大量推理 token只得到一批重复样本。5. 从“卡在推理”到“独立扩展”一条可复用的四步路线如果不想每次遇到 RL 卡顿都靠感觉调参可以按下面的路线走。这个路线并不复杂但它把“把推理独立扩展”这个抽象判断拆成了渐进可执行的步骤。5.1 第一步基准定位先不改架构只改日志。记录每个训练 step 中采样耗时奖励计算耗时训练更新耗时队列等待耗时。三个 step 就能看出瓶颈分布。如果采样耗时占比显著最高进入第二步如果训练更新本身才是瓶颈那该处理的是训练并行和显存问题而不是推理。5.2 第二步最小解耦不要一开始就建一个完美的推理平台。先做最小改动把原来的generate()调用替换成请求一个独立推理服务。这个服务可以只跑在单机多卡上不需要容器编排也不需要自动扩缩容。这一阶段的目标是验证两件事独立推理服务的输出和原采样方式基本一致训练循环通过 API 请求 rollout 后流程仍然稳定。如果这两点都过了再考虑异步和弹性伸缩。5.3 第三步独立扩展与动态伸缩当模型变大、任务变多后单机推理服务开始成为瓶颈。这时再把推理服务做成多实例加上任务队列让请求可以平衡到不同推理节点。扩展方向有几个水平扩展增加推理服务实例数垂直扩展给单实例加更多显存或更强 GPU拆分场景探索任务用高吞吐池高质量评估用低并发池异步化把 rollout 从同步阻塞改成异步队列。每一步扩展后都要回到第一步重新量化采样耗时和训练曲线变化。不要只看吞吐上去了就以为一切 OK。吞吐上去了但训练效果没变说明扩展的是“垃圾数据的吞吐”没有价值。5.4 第四步效果回归与沉淀独立扩展不是一次性的架构改造而是一套持续优化的流程。每当改变推理服务参数、资源或引擎配置都要回归一次 RL 效果包括训练 loss 曲线是否平稳奖励分数是否改善最终任务指标是否符合预期总推理成本是否上升。最好把每次实验的配置、数据和结果沉淀成一份简单的实验记录。后续团队遇到类似问题可以直接查记录而不是从头开始试错。经历了完整路线之后你会发现“推理独立扩展”不再是一个听起来很高级的理念而是一个日常可用的手段先定位瓶颈再最小解耦然后按需扩容最后做效果回归。RL 被推理卡住脖子不是算法的失败而是任务对探索和生成的要求变高之后一种必然的工程现象。真正解决问题的关键不是让训练去迁就推理也不是简单地无脑堆卡而是把推理看作 RL 的数据基础设施给它独立扩展、独立优化、独立验证的机会。下一次 RL 训练再卡住时可以先别急着调损失函数去看看推理侧是不是已经偷偷成了整个系统里最长的短板。