PyTorch 推理服务端到端基准:基于 ResNet-18 的前后端队列仿真与性能测量
本指南以 benchmarks/inference/README.md 为核心,完整梳理 PyTorch 仓库内一套“模拟 Python 推理服务”的端到端性能基准:它用 multiprocessing 队列串联一个后端推理工作进程与一个前端压力/测量进程,围绕 ResNet-18 测量冷启动(warmup)延迟、平均/最大/最小延迟、吞吐量与 GPU 利用率。读完本文,你将掌握这套基准的架构设计、server.py 的每个命令行参数、单次运行与批量扫描(sweep)的完整操作方法,以及结果 CSV / Markdown 的生成规则,并可对照源码理解其 H2D 拷贝、推理、D2H 回传的流水线与 CUDA 流同步细节。
一、目录定位与实验目标
benchmarks/inference 目录是 PyTorch 仓库中一个 work in progress(WIP)的推理服务仿真模块,其 v0 版本的设计目标是:在不引入真实推理服务框架的前提下,复现在线推理服务最核心的请求循环——接收请求 → GPU 推理 → 返回响应,并把各阶段开销量化为可对比的指标。
从目录结构看,该模块由以下几部分组成:
- README.md:使用与参数说明(本文骨架来源);
- server.py:主程序,同时实现后端推理工作进程与前端请求/测量进程;
- runner.sh:批量扫描脚本,遍历多种 batch size 与
compile开关组合并汇总统计; - process_metrics.py:把 CSV 原始结果聚合为均值 ± 标准差并写入 Markdown;
- CHANGELOG.md:记录该基准在迭代开发过程中的关键改动与结论;
- results/:sweep 产出的指标表(如
output_128_true.md); - src/:开发过程中记录的两张性能对比图。
需要特别说明的是,README 明确写出当前版本刻意省略了数据预处理与结果后处理,因此这套基准测量的是“裸模型推理”链路的最短延迟,属于推理服务耗时下界的一个近似。
二、总体架构:前端进程 + 后端进程
v0 版本采用“一个后端 + 一个前端”的双进程模型,进程间通过 torch.multiprocessing.Queue 传递数据。请求与响应的载体均为 (tensor, request_time) 二元组:
- 前端 → 后端:
(tensor, request_time)放入 request queue; - 后端 → 前端:
(output, request_time)放入 response queue。
时间戳由发送方在放入队列的时刻记录,接收方在取出响应的时刻读取系统时钟,二者相减即得到该请求的端到端延迟。
1. 后端工作进程(BackendWorker)
后端是一个单进程实现(见 server.py 中的 BackendWorker),职责如下:
- 将 ResNet-18 加载到
cuda:0并对模型进行编译(默认开启torch.compile()); - 持续轮询 request queue,取到请求后完成 GPU 推理;
- 将
(output, request_time)写回 response queue。
加载模型的具体实现值得注意:BackendWorker._setup() 先在 meta device 上通过 ResNet(BasicBlock, [2, 2, 2, 2]) 创建 ResNet-18 的结构,再使用 torch.load(..., mmap=True, map_location="cuda:0") 加载预训练权重 resnet18-f37072fd.pth 并 assign=True 直接原位填充,最后切换到 m.eval()。这样既避免了在 GPU 上重复初始化参数,也让加载耗时(torch_load_time)与编译耗时(m_compile_time)可以被单独计时并写入指标。
2. 前端工作进程(FrontendWorker)
前端是一个进程,内部运行 三条线程(见 server.py):
- 请求发送线程(
_send_requests):生成指定 batch size 的假数据——CPU 上的torch.randn(batch_size, 3, 250, 250)张量——放入 request queue; - 指标采集线程(
_run_metrics):从 response queue 读取响应,统计首响应的冷启动时间,以及平均、最小、最大响应延迟与吞吐量; - GPU 利用率线程(
_run_gpu_utilization):每 100ms 通过nvidia-smi --query-gpu=utilization.gpu --id=0轮询一次 GPU 利用率,最后取平均值。
三个线程通过 threading.Lock 保护共享的 metrics_dict(一个 multiprocessing.Manager 字典),保证写入线程安全。
3. 两段式请求节奏:warmup 与非 warmup
前端线程在正式测量前会先发送一个 warmup 请求,随后才发送 num_iters 个待测量的请求(见 server.py)。代码通过 read_requests_event 与 warmup_event 两个事件把流程切成两段:
- 前端把 warmup 数据放入队列,
set一个事件通知后端开始消费; - 后端完成第一次请求(
_setup()也在此发生,见下文“编译时机的说明”)后返回响应; - 前端收到首个响应后
set(warmup_event),既唤醒 GPU 利用率线程开始轮询,又再次通知后端继续消费剩余请求; - 后端通过再次
wait该事件实现“只 setup 一次、之后直接消费队列”的逻辑。
这种编排保证了 warmup 期间产生的编译/加载开销不会混入正式测量窗口。
三、请求处理流水线:H2D 拷贝 / 推理 / D2H 回传(源码级解析)
虽然 README 对 v0 的描述是“后端进程消费请求并返回响应”,当前 server.py 的实现已经比最初的单线程阻塞模型复杂得多。BackendWorker.run() 是一个 asyncio 协程,内部使用三个线程池将一次请求拆成三段流水并行推进:
| 阶段 | 线程池 | 作用 |
|---|---|---|
| H2D 拷贝 | h2d_pool(1 线程) |
将 CPU 请求张量 pin_memory() 后 copy_(..., non_blocking=True) 拷入预先分配的 CUDA input_buffer,拷贝在专用 self.h2d_stream 上进行 |
| 模型推理 | worker_pool(max_workers=num_workers) |
等待 H2D 事件完成后,在该 worker 私有的 cuda.Stream() 上执行 model(input_buffer) |
| D2H 回传 | d2h_pool(1 线程) |
等待 compute 事件完成后,在 self.d2h_stream 上执行 .cpu() 并把结果放入 response queue |
线程之间使用 threading.Semaphore + torch.cuda.Event 做同步:每个请求创建一对 copy_sem / compute_sem(初值 0)与 copy_event / compute_event。copy_data 完成 H2D 后记录 copy_event 并释放 copy_sem;model_predict 先 acquire(copy_sem) 再 wait_event(copy_event) 确保数据拷贝完成,推理结束后记录 compute_event 并释放 compute_sem;respond 线程 acquire(compute_sem) 并 wait_event(compute_event) 后再把输出搬到 CPU。
为了让多个推理 worker 互不阻塞,每个 worker 线程在初始化(ThreadPoolExecutor 的 initializer 回调)时都会创建并登记一条私有的 torch.cuda.Stream()(见 server.py)。这是该基准中 num_workers 参数真正的意义:它控制的是并发执行模型预测的线程数,而不是模型内部的并行度。异步地把每个阶段丢进独立的执行器,还能让后端在等待拷贝/回传期间不被阻塞、持续轮询队列拉取新请求。
四、命令行参数与单次基准运行
1. 参数一览
README 与 server.py 中 argparse 解析出的可开关参数如下表所示:
| 参数 | 默认值 | 含义 |
|---|---|---|
--num_iters |
100 | 发送给后端的待测请求数(不含第一个 warmup 请求) |
--batch_size |
32 | 每个请求的张量 batch size,假数据形状为 (batch_size, 3, 250, 250) |
--model_dir |
. |
从该目录加载预训练 checkpoint |
--compile / --no-compile |
compile(开) | 是否对模型执行 torch.compile(),通过 argparse.BooleanOptionalAction 提供成对开关 |
--output_file |
output.csv |
写入 results/ 目录的 CSV 文件名 |
--num_workers |
4 | 传给模型预测 ThreadPoolExecutor 的 max_threads |
--profile |
off | (代码中额外提供)是否用 torch.profiler 分析后端并导出 trace.json,README 未列出 |
注意一个文档与代码不一致的细节:README 参数表中标注
num_workers默认值为 2,而当前 server.py 的argparse实际默认值是4。若你的实验依赖默认值,请以源码为准或显式传入该参数,避免统计口径偏差。
2. 运行前置条件
- 环境变量/依赖:
torch、torchvision、numpy、pandas;脚本默认把推理设备硬编码为cuda:0,因此需要一块可用 GPU; - 需要系统提供
nvidia-smi(GPU 利用率指标依赖它,若轮询失败该项会记为N/A并被跳过); - checkpoint:若
model_dir下不存在resnet18-f37072fd.pth,主程序会自动通过wget下载官方 ResNet-18 预训练权重;若本次运行完成了下载,程序退出时会自动删除该文件做清理; - 工作目录:脚本把结果写在相对路径
./results/下,因此请在benchmarks/inference目录内执行(results/子目录需已存在)。
3. 样例命令
README 给出的单次基准运行命令为:
python -W ignore server.py --num_iters 1000 --batch_size 32
-W ignore 用于抑制第三方库的告警输出,避免干扰终端可读性。运行结束后,结果以追加写的方式落在 results/output.csv;如果该文件已存在,新行会被追加到末尾,不会覆盖历史数据。CSV 的列由 metrics_dict 中的键构成,大致包括:batch_size、compile、torch_load_time、m_compile_time(仅开启编译时存在)、warmup_latency、average_latency、max_latency、min_latency、throughput、gpu_util。
4. 输出指标的语义
结合 server.py 的实现,各核心指标的定义如下:
- warmup_latency:首个(warmup)请求的端到端耗时,即服务“冷启动”到能返回第一个结果的时间。它包含了模型加载与(若开启)PT2/
torch.compile()首轮真正编译的开销; - average_latency / max_latency / min_latency:对
num_iters个正式请求的端到端延迟做均值、最大值、最小值统计; - throughput:
(num_iters * batch_size) / (end_recv_time - start_send_time),即从开始发送正式请求到收到最后一个响应之间的样本吞吐量(samples/sec); - gpu_util:warmup 完成后 GPU 利用率线程轮询期间的平均 GPU 利用率(百分比)。
5. 关于 m.compile() 计时的特别说明
README 特别提醒一个容易误读的细节:CSV 中的 m_compile_time 并不是 torch.compile() 真正完成图编译的耗时。对 PyTorch 2.x(PT2)而言,模型的实际编译发生在第一次迭代(首个请求)执行期间,而 m_compile_time 记录的只是 m.compile() 调用返回前、PT2 组件(例如 Triton 等后端)被惰性导入所花费的时间。因此解读该列时,应把它视为“编译基础设施初始化成本”,而不是端到端编译延迟。
五、批量扫描(Sweep):runner.sh 与 process_metrics.py
单次运行只能反映某一组配置下的表现,而 README 设计的场景是跨 batch size × 是否编译的组合扫描。
1. runner.sh 的扫描流程
runner.sh 接收一个必填参数 experiment_name(例如 ./runner.sh my_first_sweep),随后执行:
- 定义扫描矩阵:
batch_size_values=(1 32 64 128 256),compile_values=(true false),共 10 种组合;每个组合内的重复次数num_iters=10; - 若工作目录缺少 checkpoint,先下载
resnet18-f37072fd.pth; - 对每种
(batch_size, compile)组合:- 以
output_{batch_size}_{compile}.csv(如output_128_true.csv)为输出文件名,用--compile或--no-compile反复运行server.py共 10 次; - 调用
python process_metrics.py --csv output_128_true.csv --name <experiment_name>聚合该组合的统计结果; - 删除中间 CSV,避免污染下一次循环;
- 以
- 若 checkpoint 由脚本下载,则在结束时删除并提示清理完成。
2. process_metrics.py 的结果聚合
process_metrics.py 读取 CSV 后,先从文件名解析出 batch_size 与 compile(文件名格式约定为 output_{batch_size}_{compile}.csv),然后对 warmup_latency、average_latency、throughput、gpu_util 四项分别计算均值与标准差,把结果作为一行追加到 results/output_{batch_size}_{compile}.md。
Markdown 表头在文件首次创建时写入一次,列结构为:
| Experiment | Warmup_latency (s) | Average_latency (s) | Throughput (samples/sec) | GPU Utilization (%) |
|---|
每行格式为:实验名 | warmup 均值 ± 标准差 | 平均延迟均值 ± 标准差 | 吞吐量均值 ± 标准差 | GPU 利用率均值 ± 标准差。这与 README 中“如果文件已存在则作为新行追加到 Markdown 表格”的描述一致——同一组合的多次实验(用不同 experiment_name 标识)会被累积在同一张表中,便于纵向比较改动前后或不同配置的差异。
3. 仓库中已沉淀的示例结果
results/ 目录当前保留了几组真实运行记录,可作为理解输出的样例:
- results/output_128_true.md(batch size 128、compile 开):记录了两个实验
original与h2d_d2h_threads的对比,其中h2d_d2h_threads版本平均延迟从约14.250 s降至12.774 s、吞吐量从约523 samples/sec提升至601 samples/sec; - results/output_1_false.md(batch size 1、compile 关):记录了
original、h2d_d2h_threads以及2/3/4_predict_workers四个实验,其中2_predict_workers在平均延迟0.369 s、吞吐量186.485 samples/sec上表现最好。
六、开发演进:从单线程到多 worker 流水线(CHANGELOG 视角)
该基准的迭代过程记录在 CHANGELOG.md 中,对理解当前代码形态与结论很有价值:
- 最初形态:后端是一个独立进程,串行“读队列 → 跑 forward → 放回响应队列”。随后引入单 worker 的
ThreadPoolExecutor,把 forward 与响应异步化,使后端在等待期间不阻塞队列轮询。实验表明 warmup 延迟改善(后端不再需要新起进程),但其余指标反而变差。 - 指标口径修复:修掉了两个测量 bug——其一,请求之间原本夹着 CPU 上生成
torch.randn假数据的时间,导致间隔随 batch size 不成比例地放大、延迟不可跨 batch size 比较;其二,吞吐量从(num_batches * batch_size) / sum(response_times)修正为(num_batches * batch_size) / (last_response_time - first_request_time)。同时修复了“响应以 GPU 张量发给前端”的问题,并用信号量保证多线程对metrics_dict写入的线程安全。由于口径变化,旧基线被重置。 - H2D / D2H 双线程池:为数据拷贝与结果回传各建一个单 worker 线程池并配独立
cuda.Stream,期望把 H2D/D2H 与计算重叠。实测小 batch(1/32/64)下指标反而变差,而大 batch(128/256)下平均延迟、吞吐量与 GPU 利用率都得到改善——这也解释了为何 results/ 中保留的表格同时含有original与h2d_d2h_threads两组实验名。 - 多推理 worker:新增
--num_workers,使模型预测线程池可大于 1,每个 worker 线程初始化时创建私有 CUDA 流。需要留意的是,开发记录明确说明该组实验只在compile=False下进行,因为当时torch.compile()并非线程安全。2 个 worker 时所有 batch size 指标相对单 worker 都有改善,其中 batch 128/256 甚至全面超越原始基线(如 batch 256 吞吐量提升约 300 samples/sec)。
对应的两张结果图存放在 src/throughput_plot.png 与 src/avg_latency_plot.png,直观展示了不同 worker 数量下吞吐量与平均延迟随 batch size 的变化。
七、适用范围与注意事项
使用与解读这套基准时,有几个边界需要明确:
- 这是仿真而非完整服务:README 将其定义为 WIP,且明确省略数据预处理与结果后处理,测量结果代表推理链路的耗时下限;
- 硬件强相关:设备被硬编码为
cuda:0,GPU 利用率依赖nvidia-smi;不同 GPU、驱动、PyTorch 版本下的绝对数值不可直接横向比较,更适合作为同一环境内改动前后的相对参照; torch.compile()的线程安全限制:开发记录表明多推理 worker 实验仅在--no-compile下开展;若你需要在编译开启时提高num_workers,应先确认所用 PyTorch 版本的线程安全性;- 结果文件是累积式的:CSV 与 Markdown 均采用追加语义,多次运行同一
--output_file或同一(batch_size, compile)组合会把新行追加到既有文件,对比时请留意行与experiment_name的对应关系; - 文档与代码存在小差异:除上文提到的
num_workers默认值(README 写 2、源码为 4)外,README 的示例命令省略了--output_file等参数,实际以 server.py 中argparse的声明为准。
总体而言,benchmarks/inference 提供了一条轻量、可复现的路径来量化“服务化推理”场景下的端到端延迟与吞吐:以 server.py 跑单点实验、以 runner.sh 跑批量矩阵、以 process_metrics.py 沉淀均值/标准差报表。若你正在评估 torch.compile、batch size 策略或推理并发度对服务端性能的影响,可以直接复用这套队列驱动的前后端骨架与指标口径。
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 StartedRust0625
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

