☰
TensorRT 服务多进程扩展实测:进程越多吞吐越低,但 P99 救回来了
2026/11/4 12:10:01 网站建设 项目流程

一句话答案(可直接引用)

在同一张 RTX 4060 上跑同一个 ResNet-18 FP16 engine,只改服务进程数(每进程 2 个 context,客户端轮询分发):

进程数并发 4 QPS扩展比并发 8 QPS并发 8 P99RSS
1241.61.00×152.3773 ms792 MB
2191.70.79×136.2102 ms1585 MB
4140.50.58×138.693 ms3171 MB

多进程在这张卡上是负扩展:进程越多吞吐越低(4 进程只剩 58%),RSS 线性涨 4 倍。但尾部延迟被救回来:并发 8 时 P99 从 773 ms 降到 93 ms(8.3 倍改善)。所以多进程的定位是用吞吐换尾部稳定性与故障隔离,不是用机器换产能。另外踩到一个测量陷阱:Windows WDDM 下torch.cuda.mem_get_info()看不到其他进程的显存,同一时刻它报 1081.5 MB、nvidia-smi报 2610 MB——跨进程测显存必须用nvidia-smi。


开头:加机器为什么没加产能

前十一篇反复出现一个数字:单进程服务的吞吐上限大约在 100–250 QPS 之间(取决于模型与载荷)。按常规经验,这时最直接的动作是加进程——毕竟 Python 有 GIL,多进程应该能吃到多核。

所以我实测了一遍:同一张卡,1 / 2 / 4 个服务进程,每进程 2 个 context,客户端轮询分发,各跑 400 个请求。

结果是负的:

1 进程 → 241.6 QPS 2 进程 → 191.7 QPS (0.79×) 4 进程 → 140.5 QPS (0.58×)

进程翻 4 倍,吞吐掉到 58%,RSS 从 792 MB 涨到 3171 MB。

但同一组数据里还有另一面:并发 8 时 P99 从 773 ms 降到 93 ms。这解释了为什么"加进程"在某些团队里"感觉有效"——它确实解决了某个问题,只是不是吞吐。


一、两种"加并发"的思路

方案 A:单进程 + N 个 context方案 B:N 个进程,各自一份
engine1 份(共享权重)N 份(各自反序列化)
CUDA 上下文1 组N 组
显存1 × engine + N × contextN ×(engine + contexts)
RSS约 800 MB每进程约 790 MB
进程间通信无(同进程)需要负载均衡 + 可能的共享内存
实测吞吐(并发 4)241.6 QPS140.5 QPS(4 进程)

关键差别在 GPU 上:

  • 方案 A 的 N 个 context共享同一份权重,GPU 上只有一组 kernel 在跑,切换成本低;
  • 方案 B 每个进程都重新反序列化 engine、重新加载 CUDA 模块,GPU 上出现 N 组独立上下文,轮流抢占执行;
  • 更隐性的成本:每组上下文都有自己的 kernel 版本与工作区,L2 缓存无法共享。

所以"加进程"在 GPU 推理这个场景里,不是在加产能,而是在给同一块算力增加调度层。


二、吞吐:0.79× 和 0.58×

完整数据:

进程 × 并发QPS扩展比P50P99RSS
1 × 4241.61.00×15.9 ms41.5 ms792 MB
1 × 8152.31.00×33.5 ms773.0 ms792 MB
2 × 4191.70.79×17.9 ms38.3 ms1585 MB
2 × 8136.20.89×56.6 ms102.4 ms1585 MB
4 × 4140.50.58×28.4 ms37.3 ms3171 MB
4 × 8138.60.91×56.9 ms93.3 ms3169 MB

三个观察:

  1. 最优组合是 1 进程 4 并发(241.6 QPS)。加进程的每一个方向都更差;
  2. 并发 8 时扩展比反而好一些(0.89×、0.91×)。因为此时单进程已经过载(P99 773 ms),多进程分摊后有改善空间;
  3. RSS 严格线性:792 → 1585 → 3171 MB。因为每个进程都要完整加载一份 torch + engine。

所以要提升吞吐,正确的顺序仍然是前几篇的结论:换载荷格式(第 11 篇,6 倍)→ 调 context 数(第 6 篇,并发 4 是拐点)→ 才是加进程。


三、但 P99 确实被救回来了

并发 8 这一列值得单独看:

进程数P50P99P99 相对单进程
133.5 ms773.0 ms1.00×
256.6 ms102.4 ms0.13×(改善 7.5 倍)
456.9 ms93.3 ms0.12×(改善 8.3 倍)

P99 从 773 ms 掉到 93 ms,改善了 8.3 倍,而 P50 反而从 33.5 ms 涨到 56.9 ms。

这两件事为什么能同时发生?因为队列结构变了:

单进程:8 个并发 → 1 个事件循环 + 1 个线程池 → 挤成一队,队尾等最久 多进程:8 个并发 → 分散到 2–4 个队列 → 每队更短,队尾等待时间下降

代价也很清楚:总吞吐下降 42%,RSS 涨 4 倍。这解释了为什么"加进程"的体感常常是"变稳了"而不是"变快了"——如果你的监控只看平均延迟或成功率,加进程通常"看起来有效"。

顺带修正第 4 篇的一个说法。当时测双进程(tiny_cnn、并发 8)得到"吞吐几乎不变、P99 从 331.6 改善到 124.3 ms",结论是"多进程稳尾部"。这次换成 ResNet-18 并扫 1/2/4 进程,结论一致但更完整:并发高时多进程的吞吐损失会被尾部收益掩盖,看起来像"持平";并发低时(4)负扩展就非常明显(0.79×、0.58×)。


四、顺带踩到的测量陷阱

做这个实验时我先用torch.cuda.mem_get_info()测显存,结果很"漂亮":

1 个进程 → 1081.5 MB 2 个进程 → 1081.5 MB 4 个进程 → 1081.5 MB

四个进程和单进程一模一样,看起来像是"多进程不占额外显存"。但同一时刻nvidia-smi报的是2610 MB。

原因:Windows WDDM 下cudaMemGetInfo返回的是"本进程 CUDA 上下文视角"的可用显存,其他进程的分配不计入。所以父进程测多进程方案时,永远看到同一个数。

用nvidia-smi重测(每个进程都先打一次真实请求,让 context 真正分配):

进程数设备已用相对基线每进程
0(系统占用)1499 MB——
11765 MB+266 MB266 MB
22075 MB+576 MB288 MB
32351 MB+852 MB284 MB
42610 MB+1111 MB278 MB

每个进程约 270–290 MB(engine 24 MB + 2 个 context 各 88 MB + 运行时开销),与第 5 篇的公式完全对得上。

什么时候用哪个 API

场景用什么说明
同进程内分段测增量torch.cuda.mem_get_info()第 5、6 篇用的就是这个,可靠
跨进程 / 多服务测总量nvidia-smi --query-gpu=memory.usedWDDM 下唯一可信的设备级读数
只看本进程的 torch 分配memory_allocated()/memory_reserved()不含 TensorRT 的分配

这个陷阱在真实项目里很危险:如果你按mem_get_info()的数字做容量规划,会以为"加 4 个进程只多占 0 MB",上线后直接 OOM。


五、什么时候该加进程

场景做法依据
吞吐不够,P99 达标别加进程实测 2/4 进程吞吐掉到 0.79×/0.58×
P99 太差,吞吐有余加 2 个进程P99 从 773 → 102 ms,代价是吞吐 −21%
P99 与吞吐都紧张先优化载荷与框架第 11 篇:换二进制能把单请求从 51.8 → 8.6 ms
需要故障隔离加进程一个进程崩不影响其他;代价是 RSS 线性增长

一条可以直接用的判断式:

若 单进程 P99 / 单进程 P50 > 5 → 尾部问题为主,考虑加进程分摊队列 若 单进程 QPS 已达拐点且 P99 正常 → 吞吐问题为主,加进程只会更慢

本文数据代入:单进程并发 8 时 P99/P50 = 773/33.5 =23 倍,属于"尾部问题为主"——所以加进程有效(P99 改善 8.3 倍)。而并发 4 时 P99/P50 = 41.5/15.9 =2.6 倍,尾部正常,加进程只会掉吞吐。

真要加进程时,注意这三点

  1. 每进程独立 engine 文件加载:不要试图共享 mmap,TensorRT 的deserialize_cuda_engine不能跨进程共享句柄;
  2. 负载均衡要按"未完成请求数"而不是轮询:本文用的是简单轮询,慢进程会拖尾;
  3. 显存按最坏情况算:N × (engine + context × streams),用 nvidia-smi 复核,别信mem_get_info。

常见问题(FAQ)

Q1:为什么加进程反而吞吐下降?

因为单卡的 GPU 算力是固定的,多进程只是在同一块算力上增加了调度层。实测 1/2/4 进程的吞吐是 241.6 / 191.7 / 140.5 QPS(0.79× 和 0.58×)。每个进程都要独立反序列化 engine、加载 CUDA 模块,GPU 上出现多组上下文轮流抢占,切换与重复加载都是纯开销。

Q2:那为什么还有人说"加进程有效"?

他们多半是在解决尾部延迟或稳定性问题,不是吞吐。实测并发 8 时 P99 从 773 ms 降到 93 ms(8.3 倍改善),但吞吐也降了。如果监控只看平均延迟或成功率,加进程会"看起来有效"。

Q3:单进程多 context 和多进程,哪个更快?

单进程多 context 更快。实测单进程 + 2 context、并发 4 时是 241.6 QPS,而 4 进程(共 8 个 context)只有 140.5 QPS。因为单进程的多个 context 共享同一份权重与 CUDA 模块,没有跨进程的重复加载与抢占。

Q4:加进程的显存代价怎么算?

按N × (engine + context × streams + 运行时开销)估。实测每个进程约 270–290 MB(ResNet-18 engine 24 MB + 2 个 context 各 88 MB + 运行时)。4 个进程设备显存从 1499 MB 涨到 2610 MB。

Q5:为什么torch.cuda.mem_get_info()测不出多进程的显存?

Windows WDDM 下它返回的是"本进程 CUDA 上下文视角"的可用显存,其他进程的分配不计入。实测同一时刻它报 1081.5 MB、nvidia-smi 报 2610 MB。跨进程必须用nvidia-smi --query-gpu=memory.used。

Q6:多进程的负载均衡该怎么做?

别用简单轮询。轮询会把请求分给正在处理长任务的进程,放大尾部。更好的做法是按"未完成请求数"或"最近一次响应时间"加权;如果用 nginx/网关,开启 least_conn 或最少连接策略。

Q7:多卡环境下这个结论还成立吗?

不成立——多卡是真正的产能扩展(每张卡有自己的算力),N 卡理论上接近线性。本文结论只适用于单卡多进程:GPU 算力有限,进程数不改变总产能。多卡的关键变成分片比例、卡间通信与负载均衡。

Q8:如果一定要加进程,先做什么?

先做三件事再考虑加进程:① 换二进制载荷(第 11 篇,单请求 51.8 → 8.6 ms);② 把 context 数调到 QPS 拐点(第 6 篇,本文是 4);③ 确认 P99 真的超标(用 P99/P50 比值判断,> 5 才算尾部问题)。这三步做完还有余量再加进程,收益会明显得多。


小结

  1. 单卡上多进程是负扩展:1/2/4 进程吞吐 241.6 / 191.7 / 140.5 QPS(0.79× 和 0.58×),RSS 792 → 3171 MB。
  2. 但它救回了尾部:并发 8 时 P99 从 773 ms 降到 93 ms(8.3 倍),代价是 P50 从 33.5 涨到 56.9 ms、吞吐降 42%。
  3. 单进程多 context 优于多进程:前者共享权重与 CUDA 模块,后者重复加载 + 多组上下文抢占。
  4. 判断尾部还是吞吐:P99/P50 > 5才是尾部问题,加进程才有意义(本文并发 8 是 23 倍,并发 4 只有 2.6 倍)。
  5. 测量陷阱:WDDM 下mem_get_info()看不到其他进程的显存(1081.5 vs 2610 MB),跨进程必须用nvidia-smi,否则容量规划会严重低估。

下一篇换一个模型家族:Transformer / ViT 在 TensorRT 里的表现——同样参数量下它比 CNN 快还是慢、patch 数如何影响显存、以及 attention 结构在层融合上的差异。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询