1. 从推理到强化学习:一个AI系统工程师的视角转换
这两年跟同行聊天,话题从“你家用什么卡跑推理”慢慢变成了“你们RL那套怎么搭的”。这个转变挺有意思。早几年大家关心的是模型能不能跑起来、延迟能不能压到200毫秒以内、单卡吞吐能拉多高,典型的推理infra思维——把训练好的模型部署好,让请求高效地进出。但现在越来越多团队开始碰强化学习相关的工程,问题就完全不一样了:训练和推理搅在一起,环境交互成了瓶颈,GPU利用率忽高忽低,整个系统的复杂度上了一个台阶。
我自己是从推理服务这条线一路做过来的,踩过KV cache调优的坑,也经历过扩缩容策略反复推翻重来的阶段。后来因为项目需要,开始接触RL训练相关的infra搭建,才发现这两者虽然都叫“AI基础设施”,但底层逻辑差别很大。推理infra的核心是“稳定高效地执行固定计算”,而RL infra的核心是“协调一个动态的、多组件耦合的闭环系统”。前者像开一条流水线,后者更像指挥一支乐队。
这篇文章想聊的就是这个转换过程中,我实际遇到的技术差异、架构思路的调整,以及那些只有真正上手才会明白的工程细节。不管你是做推理服务想往RL方向延伸,还是刚接触RL训练需要搭基础设施,希望这些经验能帮你少走一些弯路。文章会从推理infra的核心机制讲起,再展开到RL infra的架构挑战,中间穿插具体的参数配置、排查思路和踩坑记录。
2. 推理infra的核心机制与工程要点
2.1 推理服务的本质:把固定计算做到极致
推理infra说到底就一件事:给定一个训练好的模型,让它在单位时间内处理尽可能多的请求,同时保证延迟在可接受范围内。这个“固定计算”的特性很关键——模型权重不变,计算图不变,每次请求走的路径基本一致。所有的优化手段都围绕这个前提展开。
最核心的优化点是KV cache管理。自回归生成模型每生成一个token,都需要前面所有token的Key和Value参与注意力计算。如果每次都重新算,显存和算力都扛不住。所以标准做法是把历史KV缓存下来,新token只需要计算自己的Q,然后跟缓存的KV做注意力。这个缓存的大小直接决定了你能同时服务多少个并发请求。
以常见的7B模型为例,假设32层、32个注意力头、head dimension为128,精度用FP16。单个token的单层KV大小是2×32×128×2字节=16KB,32层就是512KB。如果序列长度是2048,那一个请求的KV cache就是1GB左右。一张80GB显存的卡,扣掉模型权重占的14GB左右,剩下60多GB,理论上能同时缓存60个左右长度为2048的请求。但实际中还要留出激活值、临时缓冲的空间,真正能跑的可能就30到40个并发。
这就是为什么PagedAttention这类技术重要——它把KV cache按页管理,像操作系统管理内存一样,减少碎片,提升利用率。vLLM就是靠这个把吞吐拉上去的。但这里有个容易忽略的点:页大小(block size)的选择。默认16或32,太小了页表开销大,太大了内部碎片多。我实测下来,对于大部分对话场景,block size设16比较均衡,但如果你的请求长度分布很集中,比如都是固定长度的摘要任务,那可以适当调大。
2.2 批处理策略:连续批处理为什么成为标配
早期推理服务用的是静态批处理——攒一批请求,一起送进模型,等全部生成完再返回。问题是生成长度不一样,短的早就完了,长的还在跑,短请求占着的显存和计算槽位就浪费了。连续批处理(continuous batching)解决了这个问题:每生成一个token就检查一次,有请求完成了就立刻腾出位置给新请求。
这个机制听起来简单,实现起来有不少细节。比如调度器需要维护一个运行队列和等待队列,每次迭代时决定哪些请求进入下一轮计算。这里有个权衡:如果每步都做精细调度,CPU开销会上去;如果调度粒度太粗,又浪费GPU。实践中一般是在每个decode step之后做一次调度决策,同时设置一个最大批大小上限,防止单批过大导致延迟飙升。
还有一个容易踩的坑是prefill和decode的混合调度。prefill阶段计算量大但只做一次,decode阶段计算量小但要做很多次。如果一批里既有新请求的prefill又有老请求的decode,计算特征差异很大,GPU利用率会受影响。有些框架会把prefill和decode分开调度,或者用chunked prefill把长prompt切分成小块,跟decode混在一起跑,平滑计算负载。
2.3 显存与算力的平衡:量化与并行策略
推理infra另一个核心问题是显存不够。模型越来越大,单卡放不下就得做张量并行或流水线并行。张量并行是把每一层的矩阵运算切到多张卡上,通信量大但延迟低;流水线并行是把不同层放到不同卡上,通信量小但需要micro-batch来填流水线气泡。
实际选型时,如果卡间带宽够(比如NVLink),优先张量并行,因为RL训练里经常需要低延迟的推理反馈。如果带宽一般,流水线并行更稳。但流水线并行在推理场景有个麻烦:请求得按micro-batch切分,增加了调度复杂度。
量化是另一条路。INT8量化能把显存占用砍一半,INT4再砍一半。但量化对生成质量的影响因任务而异。我做过对比测试,在代码生成任务上,INT8基本无损,INT4在长序列后半段会出现明显的重复和逻辑断裂。所以如果做RL训练,推理部分建议至少用INT8,INT4只适合对质量要求不高的场景。
3. RL infra的架构挑战:当训练和推理纠缠在一起
3.1 RL训练的基本流程与infra需求
强化学习训练跟监督学习最大的区别是:数据不是预先准备好的,而是模型跟环境交互产生的。以RLHF为例,流程大致是:策略模型生成回复,奖励模型打分,然后根据打分更新策略。这个过程循环往复,每一轮都需要推理(生成回复)和训练(更新参数)交替进行。
这就带来一个根本性的架构问题:推理和训练用的模型是同一个,但推理需要模型处于eval模式、用推理优化过的引擎,训练需要模型处于train模式、用训练框架。两者之间的参数同步成了关键路径。
我见过几种做法。最简单的是串行:推理阶段把训练框架的模型权重导出,加载到推理引擎里,生成完数据再导回来训练。缺点是每次切换都要重新加载权重,7B模型加载一次要几十秒,大模型更久,整个训练周期里大量时间花在等待上。
好一点的做法是共享内存或显存。训练框架和推理引擎跑在同一组GPU上,通过CUDA IPC或者NCCL直接传权重,省去落盘和重新加载。但这要求两个框架能共存,显存分配得协调好。实践中经常是训练占大部分显存,推理用剩下的,或者分时复用。
3.2 推理引擎与训练框架的耦合方式
耦合方式的选择直接影响训练效率和系统稳定性。我总结下来有三种模式,各有适用场景。
第一种是分离式:推理和训练跑在不同的GPU组上,通过高速网络传数据。优点是互不干扰,推理可以用最优的引擎配置,训练也可以满负荷跑。缺点是资源利用率低,因为推理和训练是交替的,总有一方在空闲。而且数据传输有延迟,尤其是生成大量样本时,网络可能成为瓶颈。
第二种是共卡分时:同一组GPU,推理时全部用于推理,训练时全部用于训练。切换时做权重同步。这种方式资源利用率高,但切换开销大。如果推理和训练的时间比例接近1:1,那切换开销可能占到总时间的20%以上。优化方向是减少切换次数,比如一次推理生成多轮数据,或者用异步方式让推理和训练重叠。
第三种是共卡同时:推理和训练同时跑在同一组GPU上,通过MPS(Multi-Process Service)或者手动划分SM来隔离资源。这种方式最复杂,但理论上效率最高。实际中需要仔细调优资源分配比例,否则容易出现推理延迟抖动或者训练速度下降。我试过用MPS把70%的SM给训练、30%给推理,效果还可以,但推理的尾延迟会变长,因为SM被抢占时请求得排队。
3.3 环境交互的工程化:从模拟器到真实系统
RL训练里的“环境”是个宽泛的概念。在游戏AI里,环境是游戏引擎;在机器人控制里,环境是物理仿真器;在RLHF里,环境就是人类偏好或者奖励模型。不管哪种,环境交互的吞吐和延迟直接影响训练效率。
以RLHF为例,奖励模型通常也是个神经网络,打分需要前向计算。如果奖励模型跟策略模型一样大,那每生成一个样本就要多跑一次前向,计算量翻倍。优化方法包括:用更小的奖励模型(蒸馏过的),或者把奖励计算批量化,一次给多个样本打分。
更麻烦的是环境本身有状态。比如多轮对话任务,环境需要维护对话历史,每轮生成后更新状态。如果环境是单线程的,那它就成了整个训练流程的瓶颈。解决办法是环境并行化——起多个环境实例,每个实例独立维护状态,策略模型轮流跟它们交互。这里要注意状态同步问题,尤其是当策略更新后,旧环境里的状态可能跟新策略不匹配,需要定期重置。
4. 从推理到RL:实操中的关键环节与参数配置
4.1 推理引擎的选型与配置调优
做RL infra,推理引擎的选型很关键。我主要用过vLLM和TensorRT-LLM,各有优劣。
vLLM的优势是上手快、社区活跃、对HuggingFace模型支持好。PagedAttention和连续批处理都是默认开的,基本不用怎么调就能跑。缺点是自定义程度有限,比如你想改调度策略或者加自定义的采样逻辑,得改源码。而且vLLM的显存管理比较激进,有时候会OOM。
TensorRT-LLM的优势是性能极致,尤其是用了FP8或者INT4量化后,吞吐能比vLLM高30%到50%。但编译过程复杂,模型转换、引擎构建、插件配置,每一步都有坑。而且它对动态形状的支持不如vLLM灵活,变长序列处理起来麻烦。
我的建议是:如果做RL训练,优先vLLM,因为训练过程中模型权重频繁更新,vLLM加载新权重更快,而且对变长序列友好。如果做线上推理服务,追求极致吞吐,可以考虑TensorRT-LLM。
配置方面,几个关键参数:
gpu_memory_utilization:vLLM里控制显存占用比例,默认0.9。RL训练时建议调到0.7到0.8,给训练框架留空间。max_num_seqs:最大并发序列数,根据显存和请求长度分布调。7B模型、2048长度,一般设64到128。max_model_len:最大序列长度,设太大浪费显存,设太小截断请求。建议按实际任务的最长序列加10%余量。block_size:PagedAttention的页大小,16或32,前面分析过。
4.2 训练框架与推理引擎的权重同步方案
权重同步是RL infra里最容易出问题的地方。我踩过的坑包括:同步时显存不够导致OOM、同步后推理结果跟训练不一致、同步耗时过长拖慢训练。
同步方案的选择取决于耦合模式。分离式最简单,训练完把权重存成HuggingFace格式,推理引擎重新加载。但要注意保存和加载的精度要一致,训练用FP32保存,推理用FP16加载,可能会有数值差异。建议训练时就用混合精度,保存时转成推理用的精度。
共卡分时的话,可以用CUDA IPC直接传显存里的权重。具体做法是训练框架把权重张量放到共享显存,推理引擎通过IPC句柄直接映射。这样省去了序列化和反序列化,速度快很多。但要注意同步时机——必须等训练的所有梯度更新完成,否则推理用的可能是中间状态。
共卡同时最复杂,需要更细粒度的同步。一般是训练完一个batch后,把更新后的权重广播给推理进程。这里可以用NCCL的broadcast,或者用共享内存加信号量。我试过用PyTorch的distributed模块做broadcast,延迟在毫秒级,可以接受。
4.3 采样策略与训练稳定性的工程权衡
RL训练里,采样策略直接影响训练稳定性。常见的采样参数包括temperature、top_p、top_k。这些参数在推理时控制生成的多样性,在RL训练里还影响探索和利用的平衡。
temperature太高,生成的样本多样性好,但质量参差不齐,奖励信号噪声大;temperature太低,样本质量高但多样性差,策略容易陷入局部最优。实践中一般从1.0开始,根据训练曲线调整。如果奖励方差太大,就降低temperature;如果策略更新缓慢,就提高temperature。
top_p和top_k是另一种控制多样性的方式。top_p是累积概率截断,top_k是固定数量截断。我一般用top_p=0.9到0.95,配合temperature=0.8到1.0。top_k用得少,因为固定数量截断在不同分布下效果不稳定。
还有一个容易被忽略的点是重复惩罚。RL训练早期,策略模型容易生成重复内容,导致奖励模型给低分,但低分样本被用来更新策略后,模型可能更倾向于生成重复内容,形成恶性循环。加一个重复惩罚系数(比如1.1到1.2)能缓解这个问题。但惩罚太强会导致生成不连贯,需要根据实际输出调。
5. 常见问题与排查技巧实录
5.1 推理侧典型问题速查
| 问题现象 | 可能原因 | 排查方法 | 解决思路 |
|---|---|---|---|
| 吞吐突然下降 | 显存碎片化导致KV cache分配失败 | 看日志里有没有preemption或swap | 调小block_size,或重启推理服务 |
| 生成结果重复 | 采样参数不当或重复惩罚缺失 | 检查temperature和repetition_penalty | 提高temperature,加重复惩罚 |
| 首token延迟高 | prefill阶段计算量大或排队 | 看调度日志里prefill队列长度 | 用chunked prefill,或增加推理实例 |
| 尾延迟抖动 | 共卡场景下资源被训练抢占 | 监控SM利用率和推理队列等待时间 | 调整MPS资源分配比例 |
| 权重同步后输出异常 | 精度不一致或同步不完整 | 对比同步前后同一输入的输出 | 统一精度,加同步完成校验 |
5.2 RL训练中的稳定性问题与应对
RL训练比监督学习难调,很大原因是训练信号不稳定。我遇到过几种典型情况。
奖励突然崩塌:训练到一半,平均奖励从0.8掉到0.2。排查发现是策略模型生成了大量无意义但能骗过奖励模型的样本,奖励模型被“攻击”了。解决办法是加KL散度约束,限制策略模型跟参考模型的偏离程度。KL系数一般设0.01到0.1,太小约束不够,太大策略更新不动。
梯度爆炸:尤其是用PPO算法时,优势函数估计方差大,梯度容易爆。标准做法是加梯度裁剪,max_grad_norm设0.5到1.0。另外可以用GAE(广义优势估计)降低方差,lambda设0.95左右。
训练和推理不一致:训练时用FP32,推理用FP16,导致同一输入输出不同。这个在RL里影响很大,因为策略更新依赖推理生成的样本。解决办法是训练也用混合精度,保持跟推理一致。如果必须用FP32训练,那推理也用FP32,牺牲速度换一致性。
5.3 资源利用率的监控与调优
RL infra的资源利用率监控比推理服务复杂,因为训练和推理交替,GPU利用率天然有波动。关键指标包括:
- 推理阶段的GPU利用率:正常应该在70%以上,如果低于50%,说明批大小不够或者调度有问题。
- 训练阶段的GPU利用率:正常应该在85%以上,如果低,可能是数据加载瓶颈或者通信瓶颈。
- 权重同步耗时:占总时间的比例应该低于5%,如果超过10%,需要优化同步方案。
- 环境交互耗时:如果环境是瓶颈,推理GPU会空闲等待,这时候要考虑环境并行化。
我一般会搭一个简单的监控面板,把这几条曲线画在一起,一眼就能看出瓶颈在哪。比如推理利用率高但训练利用率低,说明训练是瓶颈,可能是batch size太小或者优化器太慢。反过来则是推理瓶颈,需要调推理引擎参数。
6. 一些个人体会与后续可扩展的方向
做了一段时间RL infra,最大的感受是:这活儿的复杂度不在单点技术,而在系统协调。推理infra你可以把每个组件都调到最优,拼起来就是好系统。但RL infra不行,推理调快了训练可能跟不上,训练调快了推理可能成瓶颈,环境交互又可能卡在中间。整体最优不等于局部最优之和。
另一个体会是,RL infra的迭代速度比推理infra慢很多。推理服务上线后基本稳定,偶尔调调参数。但RL训练每次改算法、改奖励函数、改环境,infra都得跟着调。所以设计的时候要留足够的灵活性,别把参数写死,别把组件耦合太紧。
后续如果继续深入,我觉得有几个方向值得探索。一是异步RL,让推理和训练真正并行起来,而不是交替等待。二是分布式环境交互,把环境也做成可扩展的服务。三是自适应资源分配,根据训练阶段动态调整推理和训练的资源比例。这些方向都有不少工程挑战,但也是提升RL训练效率的关键。
踩过的坑就不一一列举了,上面提到的那些已经够新入门的同学喝一壶。核心建议就一条:先把推理infra做扎实,再往RL方向延伸,因为RL infra的很多问题最终都落脚在推理的效率上。推理不稳,RL训练就是空中楼阁。