PyTorch 推理服务基准深度解读:以 batch size 32 + compile=true 结果文件为例
本文围绕 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.md、output_32_false.md、output_256_true.md 等则为其他 batch size / compile 组合的记录,后续将用于横向对照。
结果表中每个指标的真实计算口径
output_32_true.md 采用"均值 ± 标准差"格式,例如 original 行 Throughput (samples/sec) 为 401.554 +/- 58.539。这些数值由 process_metrics.py 对多次运行的 CSV 输出做 mean() 与 std() 统计后生成;同一 batch size + compile 组合下若多次实验,新结果行会被追加(append)到已有 Markdown 表格,这也解释了为何同目录其他结果文件(如 output_32_false.md)会累积多行不同实验名的记录。
四个指标的底层计算逻辑直接对应 server.py 中 FrontendWorker._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_stream与self.d2h_stream),并通过threading.Semaphore+torch.cuda.Event与执行预测的线程同步。设计初衷是让 H2D/D2H 拷贝能与模型计算重叠,避免预测线程被拷贝阻塞。
对应的同步流程在 server.py 的 copy_data / model_predict / respond 三个方法中有清晰体现:copy_data 在 h2d 流上完成拷贝后 copy_event.record() 并释放 copy_sem;model_predict 先 acquire() 该信号量,用 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.md 中
original的 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_workers、3_predict_workers、4_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 | 预测线程池 ThreadPoolExecutor 的 max_threads(README 记载的早期默认值为 2,实际以当前代码为准) |
运行后若 model_dir 下不存在 resnet18-f37072fd.pth,脚本会先用 wget 从 PyTorch 官方模型站下载,结束后自动清理。结果以追加方式写入 results/output.csv,每条记录包含 warmup_latency、average_latency、max_latency、min_latency、throughput、gpu_util 以及 torch_load_time、m_compile_time、batch_size、compile 等列。
**完整 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=10 次 server.py,将 10 次结果合并到一个 CSV 后调用 process_metrics.py 计算各指标的均值与标准差,并把一行结果追加到 results/output_{batch_size}_{compile}.md——这正是 output_32_true.md 等文件得以生成的完整链路。若希望将多组实验对齐,可通过 process_metrics.py 的 --name 参数为每次 sweep 打上实验名(例如 original、h2d_d2h_threads、2_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.md、output_256_true.md 等文件综合判断,任何硬件/框架版本下的数值都不能无条件外推。
若要继续深入,可沿 server.py 中 copy_data / model_predict / respond 的同步细节阅读实现,参考 CHANGELOG.md 中 PR #115286 / #116188 / #116189 / #116190 的演进动机,并利用 runner.sh 在自己环境上复现完整的结果矩阵。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00