← 返回 2026-07-29

RLHF奖励模型打分能多快:C++与PyTorch推理运行时的系统研究 How Fast Can Reward Models Score? A Systems Study of C++ and PyTorch Inference Runtimes for RLHF

Venkata Naga Sai Vishnu Rohit Pulipaka, Anish Katta, Deva Rohit Reddy Peddireddy 📅 2026-07-22 👍 3 2026-08-03 18:30
ONNX Runtime RLHF torch.compile 奖励模型推理 延迟优化 批处理 系统基准测试

用C++/ONNX引擎对比PyTorch,发现关键在运行时而非性能语言,批处理策略更重要

前置知识

RLHF训练循环

强化学习从人类反馈(RLHF)的训练流程:策略模型先生成候选回复(rollout),再由奖励模型对每条回复打分,最后用这些分数通过PPO等算法更新策略。论文图1展示了「生成rollout→奖励打分→策略更新」的循环,奖励打分是每个训练步都要执行的前向传播。

理解奖励模型打分在训练循环中的位置,才能明白为何它的延迟会乘到总训练墙钟时间上,以及为何本文聚焦这一步的推理速度。

ONNX Runtime

一个跨平台图执行推理引擎,将模型导出为ONNX计算图后,由其C++后端(CPUExecutionProvider/CUDAExecutionProvider)执行,跳过Python eager模式的开销。本文用它隔离「语言」与「运行时」两个变量。

论文的核心发现就是性能差距来自运行时(图执行 vs eager模式)而非性能语言本身,因此必须理解ONNX Runtime扮演的角色。

torch.compile

PyTorch 2.x引入的即时编译机制,将eager模式代码编译为融合算子内核(默认走inductor后端),无需单独导出步骤,但输入形状变化时会触发重新编译。本文把它作为GPU上的强力基线。

GPU上torch.compile反而击败C++引擎是论文的关键反直觉结论,不理解它的编译与recompilation行为就无法读懂这一结果。

延迟置信区间与Welch t检验

由于进程间噪声(OS调度、CPU睿频)足以让慢系统伪装成快系统,论文用多次独立进程启动计算均值与95%置信区间,并用Welch双样本t检验(方差不等)佐证,只有区间不重叠才算显著。

本文的每个结论都建立在这种严格统计方法上,包括「单次测量不可信」这一贯穿全文的方法论主张,是读懂结果的前提。

批处理与长度感知分组

naive批处理把一个批次内所有请求padding到最长序列长度;length-bucketed分组则先按长度排序/分桶,让相近长度的请求进同一批,减少无效padding计算。

论文发现naive批处理在CPU上损失5-8倍吞吐、GPU上损失3.5-4倍,是部署层最关键的优化杠杆,且CPU因无批并行而完全无法受益。

研究动机

在RLHF流水线中,奖励模型必须在每个训练步对每条rollout打分,策略更新要等所有rollout拿到分数才能进行,因此打分延迟直接成为训练墙钟时间的乘数。然而现实中绝大多数团队默认使用PyTorch eager模式或torch.compile,几乎没有系统评估过ONNX Runtime/C++这类生产推理引擎到底快多少、为什么快。更微妙的是,打分本身只占RLHF步耗时的很小一部分(生成阶段占85%以上),但两者争抢同一份CPU/GPU资源,所以更快的打分引擎并不能单独压缩步长,只是腾出算力给生成用。作者指出,即便DeepSpeed-Chat、Orca、vLLM等大量系统文献都聚焦于生成都不是打分,存在明显的研究空白。

本文的目标是本文要回答一个纯系统问题:针对奖励模型打分这种计算简单的标准前向传播,专门构建的C++/ONNX Runtime引擎究竟能否击败RLHF工程师默认使用的PyTorch eager、torch.compile和FastAPI服务,快多少、为什么快。进而追问两个部署层因素——批处理策略(naive padding vs 长度感知分组)和并发策略(共享实例 vs 多实例)——对吞吐的真实影响。所有结论必须建立在统计可靠的基础上,避免单次跑分产生误导。

与已有工作不同的是,既有工作的空白很清晰:推理优化文献要么服务于生成都不是打分,要么只比较语言/框架却混淆了「语言」与「运行时」两个变量。本文的独特切入在于三点。其一,用隔离实验(同一ONNX session分别用C++和Python调用)把性能增益干净归因到运行时(图执行vs eager)而非性能语言,纠正了「C++天然更快」的直觉。其二,把奖励打分作为独立瓶颈单独研究,补上既有RLHF系统文献(DeepSpeed-Chat、Orca、vLLM等)只关注生成都不看打分的盲区。其三,从方法论上强调多次独立进程启动+置信区间而非单次跑分,并主动坦承一个未能复现的结果作为反面教材。

核心方法

整体思路是先证明正确再比速度。作者用ONNX Runtime构建C++引擎,把分词、批处理、后处理都原生实现(DeBERTa用SentencePiece、Electra用WordPiece),导出ONNX图后推理完全不走PyTorch。先用相同输入比对C++引擎与PyTorch参考模型的奖励分数,CPU最大绝对差为 $5.7\times10^{-6}$、GPU为 $4.2\times10^{-3}$(CUDA累加顺序不同所致,属正常)。确认无误后,在四个系统(C++引擎、HF eager、torch.compile、FastAPI)上做延迟对比,再设计消融实验定位加速来源,最后做批处理和并发两类部署策略的吞吐实验。所有数据均来自多次独立进程启动的均值与95%置信区间,延迟指标用 $p_{50}$(中位)和 $p_{95}$(尾部)。

核心创新不在「又造一个C++引擎」,而在用隔离变量法回答「为什么快」。最关键的一步:把同一个ONNX Runtime session分别用C++和普通Python脚本调用,结果Python约 $349$ms、C++约 $335.9$ms,两者在置信区间上几乎并列,都远快于eager的 $602.4$ms。这直接证明CPU的加速来自运行时(图执行 vs eager),而非性能语言。进一步消融显示C++只带来分词加速( $64.3\mu s$ vs $245.8\mu s$,约 $3.8\times$ ),而零拷贝缓冲和预分配完全无效。这与直觉相悖的「负结果」是本文最独特的贡献。

方法步骤详情

方法分八步。(1)引擎:基于ONNX Runtime原生实现分词/批处理/后处理,测DeBERTa-v3-large与Electra-large两模型。(2)正确性:相同输入比对,CPU最大差 $5.7\times10^{-6}$、GPU为 $4.2\times10^{-3}$。(3)基线:HF eager、torch.compile、FastAPI。(4)统计:独立进程启动5次(CPU基线3次),算95%置信区间并配Welch t检验,每启动内对60行取 $p_{50}/p_{95}$ 再跨启动取均值。(5)数据集:hh-rlhf,主集合60行,另加两60行和150行集合做鲁棒性。(6)批处理:batch 1/2/4/8,naive vs length-bucketed,CPU四系统、GPU仅C++。(7)并发:2/4/8,共享实例vs每线程一实例,双模型双设备。(8)硬件:AMD Ryzen 7 5800H+RTX 3060 6GB,全fp32,ONNX Runtime 1.26.0、PyTorch 2.10.0、CUDA 12.6。

技术新颖性

技术新颖性体现在三点。第一是变量隔离方法学:绝大多数框架对比把「语言」和「运行时」混为一谈,本文用同一session的C++/Python双调用干净分离,得出「运行时是关键」的结论。第二是把严格的重复启动+置信区间方法论系统应用到RLHF推理基准这一此前缺乏的领域,并坦率承认有一个结果(Electra HF eager)未通过复现,把Mytkowicz等的测量陷阱当成正面教材。第三是同时覆盖批处理与并发两个部署维度,发现naive批处理「不仅无益反而有害」、多实例在GPU上直接OOM这类反直觉结论。值得强调的是作者主动剔除了Triton baseline(因为它用硬编码公式而非真实服务器),体现了严谨。

实验结果

核心发现分四块。延迟对比(Table 1):CPU上C++引擎 $p_{50}=335.9$ms 击败全部基线(eager $602.4$、FastAPI $581.6$、torch.compile $628.8$ms),置信区间不重叠;GPU上C++引擎 $27.4$ms 击败eager和FastAPI,但torch.compile以 $19.0$ms 反超。GPU $p_{95}$ 上torch.compile $25.6$ms vs C++ $116.2$ms,尾部差距更大,对比 $p<0.001$。第二,加速归因:同一ONNX session用Python调用约 $349$ms 与C++ $335.9$ms 并列,证明加速来自运行时而非语言,C++仅分词快 $3.8\times$。第三,批处理:naive padding在CPU损失 $5\sim8\times$、GPU损失 $3.5\sim4\times$;length-bucketed只在GPU回血,CPU因无批并行永不超batch=1基线。第四,并发:共享实例吞吐仅增约11%,多实例GPU并发8五次全OOM。

Main latency comparison, p50 (ms), mean [95% CI]
Table 1: Main latency comparison, p50 (ms), mean [95% CI]
GPU p95 latency (ms), mean [95% CI]
Table 2: GPU p95 latency (ms), mean [95% CI]
Welch's t-test for headline comparisons
Table 3: Welch's t-test for headline comparisons
CPU batching throughput (rows/sec), naive vs. length-bucketed
Table 4: CPU batching throughput (rows/sec), naive vs. length-bucketed
GPU batching throughput (rows/sec), C++ engine
Table 5: GPU batching throughput (rows/sec), C++ engine
CPU concurrency, p50 latency and throughput
Table 6: CPU concurrency, p50 latency and throughput
GPU concurrency, p50 latency and throughput
Table 7: GPU concurrency, p50 latency and throughput
DeBERTa vs. Electra, CPU p50 latency, Python backends
Table 8: DeBERTa vs. Electra, CPU p50 latency, Python backends
p50 latency by system, CPU and GPU
Figure 2: p50 latency by system, CPU and GPU
Tokenizer-only latency, native C++ SentencePiece vs Python AutoTokenizer
Figure 3: Tokenizer-only latency, native C++ SentencePiece vs Python AutoTokenizer
Throughput vs. batch size, naive vs. length-bucketed scheduling
Figure 4: Throughput vs. batch size, naive vs. length-bucketed scheduling
Throughput vs. concurrency level, shared vs. multi-instance, DeBERTa
Figure 5: Throughput vs. concurrency level, shared vs. multi-instance, DeBERTa
查看结构化数据
任务指标本文基线提升
CPU p50奖励打分延迟 p50延迟(ms) 335.9 HF eager 602.4 / FastAPI 581.6 / torch.compile 628.8 约1.7-1.9倍,置信区间完全不重叠
GPU p50奖励打分延迟 p50延迟(ms) 27.4 HF eager 57.2 / FastAPI 62.8 / torch.compile 19.0 击败eager和FastAPI,但被torch.compile反超
GPU p95尾部延迟 p95延迟(ms) 116.2 torch.compile 25.6 torch.compile尾部更优,差距比中位更大
GPU length-bucketed批处理吞吐 行/秒(batch=8) 53.7 naive 10.0 约5.4倍,证明长度感知批处理是唯一有效杠杆
正确性验证 最大绝对差 CPU 5.7e-6 / GPU 4.2e-3 PyTorch参考模型 可信任的数值一致性

局限与改进

作者承认的局限:全部基准在单台开发机(AMD Ryzen 7 5800H + RTX 3060 Laptop 6GB)上完成,没有第二台机器或服务器级硬件交叉验证,CPU/GPU动态和torch.compile结论能否迁移到不同硅片存疑;多实例并发GPU显存上限(4-5个独立session)是用并发5/6/7的单次非重复运行框定的,边界不严格;只测了同进程多线程多实例,未测真正进程隔离;模型仅限两个OpenAssistant奖励模型,更大或不同backbone可能不同;fp32单一精度;基准在隔离前向传播上,未端到端测RLHF训练墙钟。更严重的是作者自曝:Electra的HF eager对比(Table 8的4.7x vs 1.7x)从未做鲁棒复现,重跑时DeBERTa/eager变成725.5ms、Electra 468.1ms,方向完全相反,作者选择保留原数并标注警示。我自己的观察是:奖励打分在RLHF步中占比可能极小(生成85%+),所以这些「几倍加速」对真实训练时间的收益可能远小于延迟数字暗示的程度,工程价值需结合占比重新评估。

独立分析的弱点

第一,硬件外推性不足。仅一台6GB消费级GPU,无法判断torch.compile在数据中心GPU(A100/H100显存充裕、recompilation代价不同)上是否仍胜C++引擎,也无法判断多实例并发在多卡/大显存上能否真正并行。改进方向:租用云GPU做多硬件交叉验证,特别测试recompilation在更大形状分布缓存下的命中率。第二,端到端缺失。论文自己承认只测隔离前向传播,未把C++引擎插入真实RLHF训练循环(如OpenRLHF)测墙钟收益。改进:跑一个端到端PPO训练对比,量化「打分快X倍」实际换来多少训练步加速。第三,单一精度。只fp32,但生产RLHF常混合精度/量化。改进:补fp16/int8路径对比,这会显著改变显存与并发上界。第四,Electra HF eager结果不可复现却仍保留在表中,虽有警示但仍可能误导读者引用4.7x。改进:在定稿前补做完整重复启动测量再替换。第五,recompilation风险未量化——torch.compile的形状缓存miss尖峰有多大、训练中形状分布是否稳定,是决定GPU结论实用性的关键变量,应专门研究。

未来方向

作者明确提出的方向:定量研究torch.compile的recompilation风险——rollout长度分布变化触发形状缓存miss时的延迟尖峰幅度,以及训练过程中形状分布是否足够稳定让缓存保持热状态;端到端把C++引擎接入真实RLHF训练循环测墙钟吞吐(已提供OpenRLHF适配器但未评测);多硬件、多卡、分布式服务场景的扩展验证。基于成果可延伸的方向:在更大/不同backbone的奖励模型上验证「运行时而非性能语言」结论是否仍成立;引入fp16/int8量化与Triton Inference Server等真实服务栈补全对比;把长度感知批处理做成自适应调度器(根据实时长度分布动态分桶)并测对真实RLHF训练步长的端到端收益;探索多进程隔离的多实例并发是否在服务器级CPU上能真正并行,从而推翻本文「多实例无用」的结论边界。

复现评估

复现性总体优秀。代码、基准harness、分析脚本已开源(github.com/vishnup22/reward-model-benchmarks),数据集hh-rlhf公开,硬件/软件版本披露详尽(ONNX Runtime 1.26.0、PyTorch 2.10.0+cu126、CUDA 12.6、driver 592.00、CPU型号、GPU显存6144MiB),fp32全程一致,C++与Python端ONNX Runtime版本pin一致以排除版本混淆。统计方法(独立进程启动次数、置信区间、Welch t检验)交代清楚,多次种子和150行数据集做了鲁棒性检查。主要障碍是硬件门槛:结论强依赖单台特定机器,他人需同型号才能精确复现;Triton因需Docker Desktop和大镜像被作者主动剔除(属正确取舍)。难度中等偏低——模型小、数据小、代码开源,研究生用单GPU即可复现,难点在于严格遵循多次独立启动的统计纪律。