最新下载
热门教程
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
AI训练的"裁判员"有多快?三位独立开发者对RLHF奖励模型推理引擎做了一次彻底测速
时间:2026-08-07 11:44:51 编辑:袖梨 来源:一聚教程网
这项由三位独立研究者完成的研究以预印本形式发布于2026年7月22日,论文编号为arXiv:2607.19712,有兴趣深入了解的读者可以通过该编号查询完整论文。

故事从一个被忽视的角落开始
在人工智能领域,有一种技术叫做RLHF,全称是"基于人类反馈的强化学习"。你可以把它理解为训练AI的一种方式:先让AI生成很多候选回答,然后用一个"裁判员"(奖励模型)给每个回答打分,最后根据分数来改进AI。现在几乎所有主流的对话AI,包括你熟悉的各种聊天机器人,背后都用过或正在用这套机制。
问题是,这个"裁判员"必须在每一轮训练中给所有候选回答打完分,才能进行下一步。如果裁判打分慢,整个训练就得等着。这就像一场接力赛,跑得再快的选手,只要交棒环节出了问题,整体成绩就会被拖累。
更讽刺的是,绝大多数AI研究团队根本没有认真测过这个"裁判打分"的速度。大家默认用PyTorch(一种主流的深度学习框架)的默认模式来跑,最多开启一个叫`torch.compile`的加速选项,然后就不再深究了。
这三位研究者决定打破这个惯例。他们自己造了一套用C++语言写的推理引擎,底层依托一个叫ONNX Runtime的工业级推理框架,然后跟现有的各种方案做了一次严格的速度对比。他们测出来的结果,有些印证了直觉,更多的则让人大跌眼镜。
---
一、先搞清楚"裁判员"在整场比赛里占多大份量
在深入结果之前,有一件事必须说清楚,否则容易被数字带着走。
在RLHF的一整轮训练步骤里,奖励模型打分只是其中一个环节。真正吃掉时间的大头,是让AI生成候选回答那个阶段。来自DeepSpeed Chat体系的研究数据显示,生成候选回答这一步占据了整个训练步骤超过85%的时间,而模型参数更新大约占10%,奖励模型打分挤在剩下的零头里。
这意味着什么?意味着即便把打分速度提升两倍,整体训练时间的缩短也相当有限,因为那85%的大头你没动。研究者们把这个背景交代得很清楚,并不是要打消优化打分速度的意义,而是希望读者理解:这项研究提供的是"在这个特定环节能做到多快"的工程数据,而不是"用了这个方案整个训练就快了多少"的大包票。
打分速度的真正价值在于"释放资源"——更快的打分引擎不会直接缩短训练时长,但它占用的CPU和GPU资源更少,这部分空出来的计算能力可以让生成回答的过程用得更充分。
带着这个认知,再去看具体数字,感觉就不同了。
---
二、怎么测才算测得准:独立重复启动的方法论
这次研究在方法上有一个贯穿始终的执念:绝对不相信单次测量的结果。
计算机的运行速度受很多看不见的因素影响。操作系统随时可能把CPU让给别的程序,CPU的时钟频率会动态变化,内存的物理排布方式也会影响性能。这些因素加在一起,完全可以让一个本来更慢的方案在某次测量里看起来比更快的方案还快。卡内基梅隆大学的研究者曾经专门证明过这一点:在同样的代码和硬件上,仅仅因为某些环境变量的差异,测出来的性能结论就可能完全相反。
研究团队的解决方案是:每次测试都以独立启动新进程的方式运行,C++引擎跑5次,PyTorch系列基准方案在GPU上跑5次、在CPU上跑3次(因为CPU上`torch.compile`每次遇到新的输入长度都要重新编译,跑5次实在太耗时)。每次启动对60条测试数据分别打分,取每次启动的中位延迟(p50)和95百分位延迟(p95)作为那次启动的代表值,然后再对多次启动的代表值求均值和95%置信区间。
这种"均值的均值"做法看起来麻烦,但它把运行环境的随机波动变成了统计上可量化的东西。研究团队规定:只有当两个方案的置信区间完全不重叠时,才算是真正有意义的性能差异。
测试数据来自Anthropic公司的hh-rlhf数据集,这是一个真实的人类偏好对话数据集,正好符合RLHF的使用场景。主测试集是用固定随机种子采样的60条数据,另外用不同种子再采样了两份60条数据,以及一份150条数据,用来确认结论不是那60条特定数据造成的偶然结果。
---
三、自制C++引擎PK四大对手:CPU上赢得毫无悬念
研究团队用两个真实的奖励模型来测试,主力是OpenAssistant开源的DeBERTa v3 large奖励模型,备用是Electra large判别器奖励模型,两者用了不同的分词方式,这样即便结论在两个模型上都成立,说服力更强。
对手阵营有三位:第一是Hugging Face Transformers的默认eager模式,这是绝大多数RLHF代码库开箱即用的方式;第二是`torch.compile`,它能在不导出模型的前提下把PyTorch的操作融合成更高效的内核;第三是用FastAPI封装了PyTorch模型的HTTP服务,这模拟了很多实际工程中"通过网络接口调用奖励模型"的部署方式。
CPU上的结果可以用"摧枯拉朽"来形容。自制C++引擎的p50延迟是335.9毫秒,置信区间是[297.7, 374.0]毫秒。三个PyTorch基准方案的成绩分别是:HF eager模式602.4毫秒、FastAPI 581.6毫秒、`torch.compile` 628.8毫秒。C++引擎的置信区间上限374毫秒,远低于任何一个基准方案的置信区间下限551毫秒。用统计学的Welch t检验来验证,每一对比较的p值都小于0.001,差异显著性毋庸置疑。
C++引擎快了多少?相对于最接近的对手FastAPI,点估计上快了约1.73倍。即便用最保守的方式算——用C++引擎置信区间的上限对比FastAPI置信区间的下限——也还有约1.4倍的差距。
---
四、GPU上的意外反转:torch.compile反过来赢了
GPU上的故事就没那么简单了。
先看结果:C++引擎GPU p50延迟27.4毫秒,HF eager模式57.2毫秒,FastAPI 62.8毫秒,`torch.compile` 19.0毫秒。C++引擎清楚地击败了eager模式和FastAPI,置信区间没有重叠。但`torch.compile`反过来把C++引擎打败了——19.0毫秒对27.4毫秒,两者的置信区间同样不重叠。
这个结果让研究者有些意外,但他们选择如实呈现而不是试图解释掉它。
更令人出乎意料的是,当把视角从中位数延伸到尾部延迟(p95,也就是最差情况下的表现),差距反而扩大了。C++引擎的GPU p95是116.2毫秒,而`torch.compile`的p95仅有25.6毫秒。也就是说,在遭遇偶发性的"坏情况"时,C++引擎的表现甚至更差。
这打破了一个看起来很直觉的推断:导出成固定的ONNX图,不是应该比动态编译更稳定、更不容易出现极端值吗?实测结果表明,至少在这台硬件、这个工作负载下,并非如此。
当然,`torch.compile`有它自己的代价:每次遇到新的输入形状(也就是不同长度的文本),它都需要重新编译,这会带来一次很大的延迟峰值。如果训练过程中文本长度变化频繁,而且恰好遇到了缓存里没有的长度,代价会相当明显。这次研究的60条固定测试数据没有专门覆盖这个极端场景,所以`torch.compile`在shape cache没有命中时究竟会慢多少,还是个开放问题。
---
五、C++语言本身没有功劳:真正的英雄是执行引擎
看到CPU上的大幅优势,最自然的解释是:C++语言本身运行更快,Python太慢了。
但研究团队特意做了一个隔离实验,结果推翻了这个解释。他们把同一个ONNX Runtime会话、同一个模型、同一套库,改用普通的Python脚本来调用,其他条件完全不变,然后重新计时。
结果是349毫秒。而C++引擎是335.9毫秒。把置信区间叠在一起,这两个数字在统计上是"打平"的,完全没有可信的差异。
真正决定快慢的,是ONNX Runtime这个执行引擎本身,而不是用什么语言去调它。ONNX Runtime和`torch.compile`都做着类似的事情——把神经网络的计算图提前优化、融合,避免PyTorch eager模式下每一步操作都要解析Python指令的额外开销。这就像开车,是电动车的电机决定加速性能,而不是方向盘是木头做的还是皮革包的。
那C++在哪里真的快了?分词器。研究团队把C++原生的SentencePiece分词器换成Python的AutoTokenizer,速度从64.3微秒变成了245.8微秒,慢了约3.8倍。但分词耗时和一次完整的transformer前向传播相比,就像一滴水和一桶水,对总延迟的影响几乎可以忽略不计。
还有两个研究者本以为会有效果、结果完全没效果的优化:去掉额外的内存拷贝(zero-copy传输),以及预先分配好每次调用所需的内存缓冲区。一个约1KB的数据拷贝和一两次堆内存分配,相对于几百毫秒的transformer计算,在量级上相差了三到五个数量级,当然测不出差距。有意思的是,研究者提到他们最早做zero-copy实验时,确实看到了约10%的"改善"——但重跑相同的基准代码(没有任何修改),同样的波动又出现了。这正是单次测量不可靠的典型例证。
---
六、批处理策略才是最被低估的变量:错误填充让速度跌落悬崖
到这里,研究者开始测另一个在工程实践中极其常见的场景:批处理,也就是一次打多条数据的分。
在实际部署中,奖励模型常常需要同时处理一批候选回答,而不是一条一条来。问题是,一批里各条数据的文本长度往往不一样。神经网络的矩阵运算要求同一批数据必须等长,所以标准做法是把短的数据"填充"到和最长那条一样长。这就是"朴素填充"(naive padding)。
听起来合情合理,但研究结果显示这是一个代价巨大的默认行为。
在CPU上,从批大小1(不批处理,逐条打分)到批大小2,朴素填充方案的吞吐量从3.10条/秒直接跌到0.61条/秒,批大小8时更跌至0.40条/秒。这不是"多一些开销",这是跌落悬崖。批量处理不仅没有提速,反而让速度变成了原来的八分之一。
原因在于:一批数据里只要有一条很长的文本,整批数据都要被填充到那个长度。矩阵运算的代价基本上与序列长度成正比,所以一条长文带来的填充开销会等比例放大整批的计算量。批越大,长文混进来的概率越高,平均填充开销就越大,速度反而越慢。
对应的解决方案叫"长度感知分组"(length-bucketed batching):把长度相近的文本分到同一批,这样填充的浪费就最小化了。用这个方法,批大小4时CPU吞吐量回到2.83条/秒,接近批大小1的基准水平3.10条/秒,但仍然无法超越它。这是因为在这台机器的CPU环境中,ONNX Runtime处理一批数据的方式是把所有行合并成一个更大的矩阵串行计算,而不是真正并行处理多行,所以总计算量仍然与总token数成正比,并没有因为批处理获得并行红利。
GPU上的情况则完全相反,也完全符合GPU擅长并行的直觉。朴素填充在批大小2时吞吐量从39.8条/秒跌到11.1条/秒,跌幅同样惊人。但长度感知分组在批大小2时直接把吞吐量提升到51.5条/秒,比批大小1还快了约30%,而且随着批大小增大,吞吐量继续爬升(批大小8时达到53.7条/秒)。GPU的并行计算核心可以让同一批里长度相近的数据真正同时处理,所以这里批处理的好处才真正兑现了。
这个发现在三个不同的系统(C++引擎、HF eager模式、Python ONNX Runtime)上分别验证,在另一个模型Electra上重复验证,也在150条数据的更大测试集上重新测量,结论完全一致。
研究者的评价是:朴素填充不是中性的,它是主动有害的。而且这个害处完全隐藏在默认配置里,不主动测就不知道。
---
七、并发访问的幻想:加多少线程都没用
第三个让研究者感到意外的结果来自并发测试。
一个常见的工程直觉是:如果一个引擎服务多个并发请求,应该能更充分地利用计算资源,从而提高总吞吐量。研究者测试了两种方式:一是多个并发请求共享同一个引擎实例,二是每个并发请求有自己专属的引擎实例。
共享实例的结论是:吞吐量从并发2到并发8几乎没有增长,最多提升11%,远不是翻倍、翻四倍的幅度。延迟则大致随并发数线性增长,这是请求在排队而不是并行执行的典型信号。简单说,请求一个接一个地在引擎前面排队,并发到来并不能让处理速度加快。
通过FastAPI的HTTP接口也测了,结果一样:从并发1到并发8,吞吐量一直在0.53到0.65条/秒之间,基本上纹丝不动。
独立实例的结论则更糟糕。在CPU上,并发4时独立实例的吞吐量已经比共享实例低43%到59%,并发8时更下降到约40%。原因是每个ONNX Runtime会话都会启动自己的线程池,N个独立会话就有N个线程池在竞争同样的物理CPU核心,互相拖累。
GPU上的情况更为极端。DeBERTa模型在并发2时,独立实例比共享实例慢了约19倍。到并发8时,两个模型全部以"显存不足"(OOM)崩溃,五次启动无一成功。研究者事后单独测试了并发5、6、7的情况,发现同样全部OOM。这台GPU有6144 MB的显存,而8个独立的DeBERTa ONNX Runtime CUDA会话加在一起把显存用完了。实际可承载的独立会话上限大约在4到5个之间。
结论明确:在这台硬件上,提高吞吐量的唯一有效方法是长度感知的批处理,增加并发性(无论是共享还是独立实例)都不起作用,独立实例还会让情况变得更差。
---
八、诚实的科学:那条没能重现的数据
这篇论文有一处让人印象深刻的地方,不是某个漂亮的数字,而是研究者在论文里坦诚承认了一个结论"没能重现"。
在对比DeBERTa和Electra两个模型时,早期的数据显示Electra在HF eager模式下的p50延迟是1351毫秒,而DeBERTa只有602.4毫秒,相差约2.25倍。研究者写道,这个数字后来在一次新的单独验证中没有复现——重新跑出来的结果是DeBERTa 725.5毫秒、Electra 468.1毫秒,两个模型的排名完全颠倒了。
研究者没有悄悄删除原始数据,而是把它留在表格里,并在旁边加了注释,明确告诉读者这条数据是"3次启动的测量结果,后来没有在独立重跑中得到验证,请谨慎对待两个模型之间的比率"。他们承认这是整篇论文里唯一一个没有得到多种方式交叉验证的结论,并说这恰恰是单次测量或少量测量有多不可靠的活生生例子。
这种自我揭示的诚实,在学术论文里并不常见,某种程度上也是对全文方法论精神的最好注解。
---
九、实际使用建议:根据你的硬件和容忍度做决定
经过以上这些测试,研究者给出的工程建议很具体,但也很有条件性,并不是"用C++就完事了"这种一刀切的说法。
如果你的奖励模型运行在CPU上,最有价值的一步不是写C++引擎,而是把模型导出成ONNX格式,然后用Python调用ONNX Runtime来跑。这一步就能拿到几乎全部的速度提升,因为速度来自执行引擎,不来自语言。C++引擎在CPU上额外能带来的,只有稍快的分词速度,对整体延迟贡献微乎其微。零拷贝传输、预分配缓冲区这类底层优化完全不值得花时间。
如果你的奖励模型运行在GPU上,而你愿意接受`torch.compile`的初始编译时间,以及偶尔遇到新输入长度时的重新编译开销,那么`torch.compile`是更简单、更快的选择——它不需要导出步骤,不需要独立引擎,中位延迟和尾部延迟都更低。如果输入长度分布非常不稳定,重新编译的代价不可接受,那么ONNX Runtime方案仍然是比HF eager模式和FastAPI好得多的备选。
无论哪种硬件,批处理策略都必须认真对待。把长度相近的请求分到一组来批处理,这个操作本身计算上几乎不花什么时间,但效果是CPU上可以避免吞吐量崩塌,GPU上可以实现吞吐量真正翻倍。这个优化被埋在默认配置里从来没有人检查,却是影响最大的单一变量。
并发方面,加更多线程或者给每个请求配专属引擎,在这台硬件规格下是无效甚至有害的策略,不如把精力放在批处理上。
---
结语:每一个让你意外的发现,都是因为单次测量欺骗了你
归根结底,这篇研究要传达的不只是"用ONNX Runtime比eager模式快"或者"别做朴素填充"这两条结论。它更核心的信息是:在这个领域,凭直觉和单次测量做出的决定,有相当大的概率是错的。
torch.compile在CPU上比C++引擎慢,但在GPU上反而更快;零拷贝优化在第一次看上去有10%提升,重跑基准代码什么都没改,同样的波动又出现了;Electra和DeBERTa哪个在eager模式下更慢,三次测量和后来的单次重测给出了完全相反的结论。这些不是研究者犯的错,这是测量本身的噪声在作怪。
这也是为什么"把所有结果报告为多次独立启动的均值和置信区间"这个方法论原则,被研究团队看得和任何一条具体数字一样重要。他们认为,这种测量习惯值得被整个RLHF基础设施社区采纳。
对于那些正在搭建或维护RLHF训练流水线的工程师来说,这篇研究的价值不在于提供一个可以直接复制粘贴的"最优方案",而在于提供了一套可以信赖的方法论,以及一组在受控条件下得出的参考数据点。对于更广泛的读者,它则是一个生动的案例:在计算机性能这件事上,你以为的未必是真的,而找出真相需要的不是更好的直觉,而是更耐心的测量。
有兴趣深入了解全部实验细节、查看完整表格数据或获取代码的读者,可以通过论文编号arXiv:2607.19712找到完整论文,研究团队也将C++引擎、测试框架和分析脚本全部开源,代码仓库地址在论文末尾有提供。
---
Q&A
Q1:RLHF奖励模型用ONNX Runtime推理比PyTorch eager模式快多少?
A:在CPU上,ONNX Runtime推理(无论是C++还是Python调用)的p50延迟约为335-349毫秒,而PyTorch eager模式约为602毫秒,快了约1.7到1.8倍,置信区间完全不重叠。但在GPU上,torch.compile(19毫秒)反而比ONNX Runtime C++引擎(27.4毫秒)更快,且差异同样统计显著。
Q2:奖励模型推理中朴素批量填充到底会慢多少?
A:影响相当严重。CPU上从批大小1的3.10条/秒,用朴素填充做批大小8时吞吐量跌至0.40条/秒,相差约7到8倍。GPU上同样出现类似跌幅。改用长度感知分组批处理后,GPU吞吐量可以恢复并超过单条处理的水平,但CPU因为缺乏并行计算能力,批处理本身无法带来提速。
Q3:给奖励模型服务加更多并发线程能提高吞吐量吗?
A:在论文测试的硬件上,不能。共享单个引擎实例时,并发从2增加到8,吞吐量最多只提升11%,延迟则近乎线性增长,说明请求在排队而非并行执行。每个请求用独立引擎实例更差,CPU上会因为线程池竞争导致吞吐量下降,GPU上到并发8时两个测试模型全部因显存不足崩溃。
相关文章
- poki免费游戏入口-poki免费游戏无需登录即玩入口 08-07
- 梦幻西游化生寺加点做法 08-07
- 全新策略肉鸽塔防游戏城门血战如何试玩 08-07
- 聪明开局吧第321关如何找出20个常用字 08-07
- 喇叭之城被锁门外成就如何做 08-07
- 少女前线2追放森牙戾兽图鉴是什么 08-07