首页
/ PyTorch 推理服务基准深度解读:以 batch size 32 + compile=true 结果文件为例

PyTorch 推理服务基准深度解读:以 batch size 32 + compile=true 结果文件为例

2026-09-06 19:13:27作者:舒璇辛Bertina

本文围绕 PyTorch 仓库中 benchmarks/inference 目录下的推理基准结果文件 output_32_true.md 展开。该文件记录了 PyTorch 官方"模拟推理服务"基准在 batch size 32、开启 torch.compile() 时,original(基线)与 h2d_d2h_threads(独立 H2D/D2H 拷贝线程实验)两种实现的实测对比。读完本文,你将理解每个指标列的口径含义、两种实验配置的架构差异、该结果与相邻 batch size / compile 结果的关系,以及如何在本地完整复现同一份结果。

这份结果文件从何而来:一个模拟 Python 推理服务的基准

该结果文件并非孤立数字,而是 benchmarks/inference 这套"进行中的(work in progress)Python 推理服务模拟基准"的产物。基准的核心思路在 README.md 中说明:用一个多进程/多线程框架模拟真实推理服务的请求—响应循环,据此衡量端到端推理服务在 GPU 上的温启动延迟、平均延迟、吞吐量与 GPU 利用率。

server.py 的源码结构看,整套基准由两类 worker 组成:

  • 前端进程 FrontendWorker(继承 mp.Process):内部运行三个线程——一个线程按给定 batch size 在 CPU 上生成 torch.randn(batch_size, 3, 250, 250) 的假数据并通过 multiprocessing.Queue 下发请求;一个线程从响应队列读取结果并统计指标;一个线程每 100ms 调用 nvidia-smi --query-gpu=utilization.gpu 轮询 GPU 利用率。
  • 后端 worker BackendWorker(以 asyncio 协程运行于主进程):首次收到请求时才完成模型初始化——在 meta 设备上构造 ResNet18(ResNet(BasicBlock, [2, 2, 2, 2])),加载预训练权重到 cuda:0,进入 eval(),并可选地调用 m.compile()

值得强调的是,基准刻意省略了数据预处理与结果后处理(README 明确说明),因此测到的是"纯推理服务循环"的开销,而非完整端到端 ML 流水线的性能。

结果文件的文件命名与目录由基准约定固定:results/output_{batch_size}_{compile}.md,其中 {compile} 取值 true / false。本文目标文件对应 batch size 32、compile=true 的组合;同目录下的 output_1_true.mdoutput_32_false.mdoutput_256_true.md 等则为其他 batch size / compile 组合的记录,后续将用于横向对照。

结果表中每个指标的真实计算口径

output_32_true.md 采用"均值 ± 标准差"格式,例如 originalThroughput (samples/sec)401.554 +/- 58.539。这些数值由 process_metrics.py 对多次运行的 CSV 输出做 mean()std() 统计后生成;同一 batch size + compile 组合下若多次实验,新结果行会被追加(append)到已有 Markdown 表格,这也解释了为何同目录其他结果文件(如 output_32_false.md)会累积多行不同实验名的记录。

四个指标的底层计算逻辑直接对应 server.pyFrontendWorker._run_metrics 的实现:

指标 口径(对应源码逻辑)
Warmup_latency 首个 warmup 请求的 response_time - request_time,即冷启动延迟;该阶段包含后端首次推理时的模型加载与 torch.compile() 编译
Average_latency 排除 warmup 后,其余 num_iters 次请求 time.time() - request_time 的平均值
Throughput (num_iters * batch_size) / (last_response_time - first_request_time),即以首个请求发出到末个响应收到的总时间窗计算的有效吞吐
GPU Utilization GPU 利用率线程在整个测试周期内轮询 nvidia-smi 得到的时间平均值

需要特别提醒:吞吐口径的 end_recv_time - start_send_time 分母在 CHANGELOG.md(PR #116188)中修正过——早期实现按 sum(response_times) 作分母,会把 CPU 上逐批 torch.randn 生成数据的间隔计入,导致不同 batch size 之间的延迟与吞吐不可比;当前版本已改为上表口径,因此在跨 batch size 对比本目录结果时,数据本身是自洽的。

两行实验配置的真实含义:original 与 h2d_d2h_threads

output_32_true.md 中两行实验名分别代表推理服务后端实现的两种形态,其差异来自 CHANGELOG.md 中记录的演进历史:

  • original(基线):最朴素的请求处理方式——后端从请求队列取数据后,同步地执行 H2D 拷贝、模型 forward 与 D2H 回传,串行完成后再轮询下一个请求。
  • h2d_d2h_threads:对应 PR #116189 引入的优化实验。它在后端额外建立两个各含 1 个线程的 ThreadPoolExecutor:一个线程负责把 CPU 请求 pin_memory() 后拷入预分配的 CUDA 输入缓冲(H2D),一个线程负责把响应从 GPU 拷回 CPU 并放入响应队列(D2H)。两个线程各自使用独立的 torch.cuda.Stream()(源码中的 self.h2d_streamself.d2h_stream),并通过 threading.Semaphore + torch.cuda.Event 与执行预测的线程同步。设计初衷是让 H2D/D2H 拷贝能与模型计算重叠,避免预测线程被拷贝阻塞。

对应的同步流程在 server.pycopy_data / model_predict / respond 三个方法中有清晰体现:copy_data 在 h2d 流上完成拷贝后 copy_event.record() 并释放 copy_semmodel_predictacquire() 该信号量,用 wait_event 等拷贝完成再在该 worker 线程自有流上执行 forward;respond 同样以信号量 + d2h_stream.wait_event(compute_event) 保证计算完成后再执行 .cpu() 回传。也就是说,每行结果背后都对应一段可读、可复现的具体实现代码,而非黑盒数字。

数值逐项解读:bs=32 + compile=true 下实验整体劣于基线

output_32_true.md 的核心数据转写如下(沿用原文的均值 ± 标准差):

Experiment Warmup_latency (s) Average_latency (s) Throughput (samples/sec) GPU Utilization (%)
original 13.559 +/- 0.183 4.756 +/- 0.960 401.554 +/- 58.539 43.026 +/- 1.221
h2d_d2h_threads 12.471 +/- 0.819 5.596 +/- 1.180 340.906 +/- 69.513 32.313 +/- 8.138

逐指标对比可得到如下事实:

  • Warmup_latency 略降:由 13.559s 降至 12.471s(约 -1.1s)。这与 CHANGELOG 记录的"h2d_d2h_threads 相对基线在所有 batch size 上均降低 warmup 延迟"相符——其原因可从代码推断:H2D/D2H 异步线程池使后端能以流水线方式消费队列,首个请求的启动等待有所缩短。
  • Average_latency 上升:由 4.756s 增至 5.596s(+0.84s)。即在稳态下每次请求往返反而更慢。
  • Throughput 下降:由 401.554 降至 340.906 samples/sec(约 -15%)。
  • GPU Utilization 下降:由 43.026% 降至 32.313%,降幅约 10.7 个百分点。

综合来看,在该配置下引入独立的 H2D/D2H 拷贝线程非但没有让 GPU 更"忙",反而降低了 GPU 利用率与整体吞吐。这与 CHANGELOG 对该实验的总结完全一致:对 batch size 1、32、64 观察到了指标变差(平均延迟上升、吞吐下降、GPU 利用率下降)。从工程原理上可以推断:batch size 32 时单批计算量有限,H2D/D2H 与计算之间的可重叠空间小,而多线程 + 信号量 + CUDA Event 的调度与同步开销反而拉长了单请求的关键路径,并在 GPU 上制造出更多空闲片段——这正与 GPU 利用率显著下滑的观测相互印证。

横向对照:compile 开关与 batch size 如何改变结论

单看一份结果容易误判为"该优化必然变差",但同目录相邻结果文件提供了更完整的证据链,说明该结论存在明确的适用边界

compile 开关的影响(对比 batch size 32 两组结果):

  • output_32_false.mdoriginal 的 warmup 仅 5.680 +/- 0.919s,而本文目标文件 compile=true 时 warmup 高达 13.559 +/- 0.183s。差异主要来自 torch.compile()首次推理迭代期间完成真实编译——正如 README 所指出的,CSV 中记录的 m_compile_time 并非模型编译时间,而是 PT2 组件(如 triton)被懒加载的时间,编译本身发生在第一次前向中并计入 warmup 延迟。

batch size 的影响(对比 compile=true 各组):

  • output_1_true.md(bs=1)中,h2d_d2h_threads 同样劣于基线:平均延迟 0.519s vs 0.297s,吞吐 138.126 vs 205.657 samples/sec;
  • 而在 output_256_true.md(bs=256)中结论反转:平均延迟 20.780s vs 26.890s、吞吐 716.763 vs 562.242 samples/sec、GPU 利用率 72.765% vs 62.266%,三项指标均优于基线。

由此可以得出一个具备仓库证据支持的规律:小 batch(1、32、64)下拷贝/调度同步开销主导关键路径,异步 H2D/D2H 线程反而有害;大 batch(128、256)下单批计算时间足够长,D2H/H2D 与计算的重叠能够真正兑现收益。CHANGELOG 对 PR #116189 的记录与此一致,可作为该结论的书面佐证。

需要额外指出的是,本结果文件只包含上述两种配置;而 output_32_false.md 中还追加了 2_predict_workers3_predict_workers4_predict_workers 等行——它们来自后续 PR #116190 对"预测线程池 worker 数"的实验(README 提到基准默认 2 个 worker,而当前 server.py--num_workers 默认值已演进为 4),且 CHANGELOG 注明由于 torch.compile() 非线程安全,多 worker 实验仅在 compile=false 下运行。阅读结果文件时需留意这些行与"批处理大小/编译开关"属于不同实验维度。

如何在本地复现同一行结果

单次运行(对应本文目标文件的其中一行数据来源)使用 server.py 的命令行入口:

python -W ignore server.py --num_iters 100 --batch_size 32 --compile

可切换的命令行参数(以 README.md 与当前 server.py 源码为准)如下:

参数 默认值 说明
--num_iters 100 除首个 warmup 请求外,向后端发送的请求数
--batch_size 32 每个请求的 batch size(假数据 shape 为 [B, 3, 250, 250]
--model_dir . 加载 ResNet18 checkpoint 的目录
--compile / --no-compile --compile 是否对模型执行 torch.compile()
--output_file output.csv 写入 results 目录的 CSV 文件名
--num_workers 4 预测线程池 ThreadPoolExecutormax_threads(README 记载的早期默认值为 2,实际以当前代码为准)

运行后若 model_dir 下不存在 resnet18-f37072fd.pth,脚本会先用 wget 从 PyTorch 官方模型站下载,结束后自动清理。结果以追加方式写入 results/output.csv,每条记录包含 warmup_latencyaverage_latencymax_latencymin_latencythroughputgpu_util 以及 torch_load_timem_compile_timebatch_sizecompile 等列。

**完整 sweep(覆盖 5 种 batch size × compile 开关)**使用 runner.sh

bash runner.sh {experiment_name}

脚本逻辑为:依次遍历 batch_size_values=(1 32 64 128 256)compile_values=(true false),对每个组合运行 num_iters=10server.py,将 10 次结果合并到一个 CSV 后调用 process_metrics.py 计算各指标的均值与标准差,并把一行结果追加到 results/output_{batch_size}_{compile}.md——这正是 output_32_true.md 等文件得以生成的完整链路。若希望将多组实验对齐,可通过 process_metrics.py--name 参数为每次 sweep 打上实验名(例如 originalh2d_d2h_threads2_predict_workers),从而在同一张表格内积累可对比的多行记录。

结论适用边界的注意事项

解读 output_32_true.md 这类结果时,务必记住基准自身声明的限制:

  • 该基准为 work in progress 的模拟实现(README.md 明确标注),不包含真实服务中常见的预处理与后处理,数字不直接等同于生产推理服务的性能;
  • GPU 利用率基于 nvidia-smi--id=0 的 100ms 轮询(见 server.py_run_gpu_utilization),反映的是 GPU 0 的瞬时利用率均值,存在采样粒度限制;
  • 结果均以均值 ± 标准差呈现,同一配置的多次运行存在明显抖动(例如上表平均延迟标准差高达 0.960~1.180s),应结合离散度而非仅凭均值下结论;
  • 结论仅对 batch size 32 + compile=true 这一组合严格成立,跨 batch size 的规律须对照 output_1_true.mdoutput_256_true.md 等文件综合判断,任何硬件/框架版本下的数值都不能无条件外推。

若要继续深入,可沿 server.pycopy_data / model_predict / respond 的同步细节阅读实现,参考 CHANGELOG.md 中 PR #115286 / #116188 / #116189 / #116190 的演进动机,并利用 runner.sh 在自己环境上复现完整的结果矩阵。

登录后查看全文
热门项目推荐
相关项目推荐