ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

八卡GPU推理并发估算:显存、算力、带宽瓶颈计算器

八卡GPU推理并发估算:显存、算力、带宽瓶颈计算器

1. 从一张显卡到八张显卡:并发估算为什么总让人心里没底

做模型推理服务的人,几乎都绕不开一个特别现实的问题:手里这台八卡机器,到底能同时扛住多少个请求?这个问题听起来简单,但真到要写容量规划文档、要给业务方一个明确答复的时候,绝大多数人给出的数字都是拍脑袋来的。我见过太多团队在压测之前先估一个数,结果上线之后要么资源大量闲置,要么高峰期直接排队超时,两头不讨好。

这个项目的出发点就来自这里。与其每次靠感觉猜,不如把影响并发能力的几个核心变量拆开,做成一个能反复调参、能直观看到瓶颈在哪的计算器。它要解决的不是"精确预测线上真实并发"这种不可能完成的任务,而是帮你在动手压测之前,先建立一个数量级正确的心理预期,知道显存、算力、带宽这三者里,哪一个会先成为限制条件。

适合读这篇内容的人有三类。第一类是刚接手推理服务部署的工程师,需要快速判断现有硬件能支撑什么规模的业务;第二类是做容量规划和成本核算的同学,要回答"再加几张卡能撑住翻倍的流量";第三类是对 GPU 推理原理感兴趣、想搞明白显存和算力到底怎么互相制约的开发者。不管你之前有没有做过性能压测,只要跟着把变量一个个理清楚,最后都能自己动手把这个计算器搭出来,并且知道每个数字背后的道理。

我先把结论性的框架摆出来:八卡并发能力不是一个单一数字,而是由单卡显存上限、单卡计算吞吐、卡间通信开销、请求本身的输入输出长度分布这四个维度共同决定的。计算器的作用,就是把这四个维度参数化,让你改一个参数就能立刻看到并发数的变化,从而找到当前配置下的真实短板。

2. 并发估算的底层逻辑:先搞清楚瓶颈到底在哪

2.1 显存、算力、带宽:三个互相拉扯的限制条件

很多人一上来就问"八卡能跑多少并发",这个问题本身缺少前提。就像问"一辆卡车能拉多少货",你得先知道货有多重、路有多远、车斗多大。GPU 推理也是一样,并发数是被多个约束同时卡住的,最终能跑多少,取决于最紧的那个约束。

显存约束是最直观的。每个并发请求在推理过程中都要占用一部分显存,主要包括模型权重、KV Cache、激活值以及框架自身的开销。模型权重是固定的,八张卡如果做张量并行,权重会被切分到每张卡上,单卡权重占用会下降;但 KV Cache 是随并发数线性增长的,请求越多、上下文越长,这部分占用越大。所以显存往往决定了并发的上限天花板。

算力约束则决定了响应速度。就算显存够放下一百个并发请求,如果 GPU 的计算单元忙不过来,每个请求的延迟就会飙升,用户体验照样崩掉。算力约束通常用每秒能处理的 token 数来衡量,它和模型大小、精度、batch size 都有关系。

带宽约束容易被忽略,但在多卡场景下非常关键。张量并行意味着每层计算都要在卡之间做 all-reduce 通信,卡间带宽不够的话,GPU 计算单元会大量时间花在等数据上,利用率上不去。八卡机器如果走的是 PCIe 而不是 NVLink,这个瓶颈会非常明显。

提示:估算并发时不要只盯着显存,很多"显存明明够但就是跑不快"的情况,根源都在带宽或者算力上。

2.2 为什么不能简单用"显存除以单请求占用"来算

新手最容易犯的错误,就是拿总显存除以单个请求的显存占用,得出一个看起来很美的并发数。这个算法忽略了三件事。

第一,KV Cache 不是一次性分配的,而是随着生成过程逐步增长的。你在估算时用的是最大长度还是平均长度,结果能差好几倍。第二,框架本身有预留显存,PyTorch 的缓存分配器、CUDA context、通信缓冲区都会吃掉一部分,实际可用显存往往只有标称值的百分之八九十。第三,batch 内的请求长度不一致时,padding 会浪费大量计算和显存,实际有效并发远低于理论值。

所以一个靠谱的计算器,必须把这些"损耗项"显式建模进去,而不是用一个理想化的除法糊弄过去。我在设计这个计算器的时候,专门留了几个可调的损耗系数,就是为了让估算结果更贴近真实情况。

2.3 计算器的整体设计思路

这个计算器我最终做成了一个单页的交互工具,核心是一组输入参数加一组实时计算的输出指标。输入侧包括:GPU 型号与数量、单卡显存、模型参数量与精度、并行策略、平均输入长度、平均输出长度、目标延迟等。输出侧则给出:显存允许的最大并发、算力允许的最大并发、带宽允许的最大并发,以及三者取最小值后的推荐并发数。

设计上我刻意避免了"给一个数字就完事"的做法,而是把三个约束分别算出来并列展示。这样你一眼就能看出当前配置下谁是瓶颈。如果显存是瓶颈,那就考虑量化或者换更大显存的卡;如果算力是瓶颈,那就考虑加卡或者换更小的模型;如果带宽是瓶颈,那就得重新考虑并行策略。这种"分约束展示"的设计,比单一数字有用得多。

3. 核心参数拆解:每个数字是怎么算出来的

3.1 显存占用的精细计算

显存占用这块,我把它拆成了四个部分:模型权重、KV Cache、激活值和框架开销。

模型权重好算,参数量乘以每个参数的字节数就行。FP16 是 2 字节,INT8 是 1 字节,INT4 是 0.5 字节。如果是八卡张量并行,单卡权重就是总权重除以卡数,但要注意有些层(比如 LayerNorm)可能不做切分,实际会略高于理论值。

KV Cache 是重头戏。它的计算公式是:2 × 层数 × 注意力头数 × 头维度 × 序列长度 × batch_size × 精度字节数。这里的 2 是因为要同时存 Key 和 Value。序列长度要按输入加输出的总长度算。这个公式里每一项都能从模型配置里查到,比如 Llama 3 70B 是 80 层、64 个注意力头、头维度 128。

激活值相对小一些,但在大 batch 下也不能忽略,一般按权重的百分之几来估。框架开销我通常留 1 到 2 GB 的余量,具体看框架版本和是否开了 CUDA Graph。

# KV Cache 单请求占用估算(字节) def kv_cache_bytes(num_layers, num_heads, head_dim, seq_len, dtype_bytes=2): return 2 * num_layers * num_heads * head_dim * seq_len * dtype_bytes # 以 Llama 3 70B 为例,序列长度 4096 per_request = kv_cache_bytes(80, 64, 128, 4096) print(f"单请求 KV Cache: {per_request / 1024**3:.2f} GB")

跑一下就知道,单请求的 KV Cache 在长上下文下能到好几个 GB,这就是为什么长上下文场景并发数会断崖式下跌。

3.2 算力约束下的并发上限

算力约束的核心指标是 GPU 每秒能处理的 token 数。这个数字和模型大小、精度、GPU 型号强相关。一个粗略的估算方法是:用 GPU 的峰值算力除以单 token 所需的计算量。

单 token 计算量大约是2 × 参数量次浮点运算(前向传播)。比如 70B 模型,单 token 约需 140 GFLOP。如果一张卡的 FP16 算力是 300 TFLOPS,理论上一秒能算两千多个 token。但实际利用率通常只有百分之三四十,所以真实吞吐要打对折甚至更多。

算力允许的并发数,等于总吞吐除以单请求的 token 生成速率。如果你要求每个请求每秒至少生成 20 个 token,那并发数就是总吞吐除以 20。这个约束在长输出场景下会变得非常紧。

3.3 卡间带宽对并发的隐性限制

多卡推理时,张量并行的每一层都要做 all-reduce。通信量和隐藏层大小、batch size 成正比。如果卡间带宽是 600 GB/s(NVLink 水平),而 PCIe 只有 32 GB/s,差距接近二十倍。

带宽约束的估算方式是:算出每生成一个 token 需要传输的总数据量,再除以可用带宽,得到通信耗时。如果这个耗时接近甚至超过计算耗时,说明带宽成了瓶颈,加卡反而可能因为通信开销增加而变慢。

我在计算器里用一个"通信占比"指标来体现这一点。通信占比超过百分之五十,就说明这套并行策略在当前硬件上不太划算,应该考虑换并行方式或者减少卡数。

约束类型关键参数典型瓶颈场景缓解手段
显存单卡显存、KV Cache长上下文、大模型量化、换大显存卡
算力峰值算力、利用率长输出、高 QPS加卡、换小模型
带宽卡间互联带宽张量并行层数多换并行策略、减少通信

4. 动手实现:从参数输入到并发数输出

4.1 环境准备与依赖选择

这个计算器我做了两个版本,一个是纯前端的网页版,一个是 Python 脚本版。网页版方便分享给不写代码的同事,脚本版方便集成到自动化流程里。这里重点讲 Python 版,因为它更容易验证计算逻辑。

依赖非常简单,只需要标准库就够了,不需要装 PyTorch 这种重家伙。计算器本身只是做数学运算,不实际跑模型,所以保持轻量很重要。如果你想让它自动读取模型配置,可以加一个transformers依赖,从模型的 config.json 里直接拉层数和头数,省得手动填。

# 创建虚拟环境 python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 可选:安装 transformers 用于自动读取模型配置 pip install transformers

注意:不要把计算器和实际压测混为一谈。计算器给的是理论估算,真实并发一定要用压测工具(比如 vLLM 自带的 benchmark)验证,两者结合才有意义。

4.2 核心计算函数的编写

整个计算器的核心就是三个函数,分别对应显存、算力、带宽三个约束。我尽量让每个函数保持独立,方便单独测试和调整。

def max_concurrency_by_memory(total_mem_gb, weight_gb, framework_gb, kv_per_request_gb, activation_gb): """显存约束下的最大并发""" available = total_mem_gb - weight_gb - framework_gb - activation_gb if available <= 0: return 0 return int(available // kv_per_request_gb) def max_concurrency_by_compute(total_tflops, model_params_b, utilization, tokens_per_sec_per_req): """算力约束下的最大并发""" # 单 token 计算量约 2 * 参数量(单位:GFLOP) flops_per_token = 2 * model_params_b * 1e9 effective_flops = total_tflops * 1e12 * utilization total_tokens_per_sec = effective_flops / flops_per_token return int(total_tokens_per_sec // tokens_per_sec_per_req) def max_concurrency_by_bandwidth(bandwidth_gbps, hidden_size, num_layers, batch_tokens, dtype_bytes=2): """带宽约束下的最大并发(简化模型)""" # 每层 all-reduce 传输量约 2 * hidden_size * batch_tokens * dtype_bytes bytes_per_layer = 2 * hidden_size * batch_tokens * dtype_bytes total_bytes = bytes_per_layer * num_layers # 假设通信和计算可以部分重叠,取 0.5 作为重叠系数 effective_bytes = total_bytes * 0.5 time_sec = effective_bytes / (bandwidth_gbps * 1e9) # 返回在目标时间内能处理的并发(这里简化处理) return max(1, int(1 / time_sec)) if time_sec > 0 else 1

这三个函数跑出来的最小值,就是当前配置下的推荐并发数。我特意让每个函数都返回整数,避免出现"0.7 个并发"这种没意义的输出。

4.3 参数选择与实测校准

光有公式还不够,参数填得对不对直接决定结果准不准。我整理了一份常用 GPU 的参数表,填的时候直接查表就行。

GPU 型号显存FP16 算力卡间带宽适用场景
A100 80G80 GB312 TFLOPS600 GB/s大模型主力
H100 80G80 GB989 TFLOPS900 GB/s高吞吐场景
L40S48 GB362 TFLOPS64 GB/s中等模型
消费级 24G24 GB80 TFLOPS32 GB/s小模型实验

利用率这个参数最需要校准。我实测下来,vLLM 在 batch 较大时利用率能到百分之四十到五十,TensorRT-LLM 更高一些,能到百分之六十。刚开始可以先用百分之三十五这个保守值,压测之后再回来修正。

4.4 把结果可视化出来

纯数字不够直观,我加了一个简单的文本柱状图,把三个约束的并发数并排显示,一眼就能看出谁是瓶颈。

def visualize(mem, comp, bw): max_val = max(mem, comp, bw, 1) scale = 40 / max_val for name, val in [("显存", mem), ("算力", comp), ("带宽", bw)]: bar = "#" * int(val * scale) print(f"{name:4s} | {bar} {val}") # 示例输出 # 显存 | ######################## 128 # 算力 | ############## 72 # 带宽 | #################### 96

看到算力那根柱子最短,就知道该往哪个方向优化了。这种直观的反馈,比看一堆数字有用得多。

5. 实测验证:计算器到底准不准

5.1 用 vLLM 做对照压测

计算器写完不能自说自话,得拿真实压测数据对照。我用 vLLM 在一台八卡 A100 上跑了 Llama 3 70B 的推理服务,然后用计算器估算同样的配置,对比两者结果。

压测用的是 vLLM 自带的 benchmark_serving 脚本,逐步增加并发数,记录吞吐和延迟。当延迟超过设定阈值(我设的是首 token 延迟 2 秒、每 token 延迟 100 毫秒)时,就认为达到了并发上限。

python -m vllm.entrypoints.openai.api_server \ --model meta-llama/Llama-3-70b-instruct \ --tensor-parallel-size 8 \ --max-model-len 4096 \ --gpu-memory-utilization 0.9

实测下来,八卡 A100 跑 70B 模型,在 4096 上下文、平均输出 256 token 的场景下,稳定并发在 60 到 70 之间。计算器给出的估算值是 72,误差在百分之十以内,这个精度对于容量规划来说已经够用了。

5.2 误差来源分析

计算器和实测之间有差距是正常的,关键是要知道差距从哪来。我总结了几个主要误差源。

第一个是请求长度分布。计算器用的是平均值,但真实流量里长短请求混杂,长请求会拖慢整个 batch。第二个是调度策略。vLLM 的 continuous batching 会动态调整 batch 组成,实际效率和静态 batch 不一样。第三个是显存碎片。长时间运行后显存会有碎片,可用量比理论值低。

提示:如果你的业务请求长度方差很大,建议在计算器里把平均长度往上调百分之二十到三十,给长请求留出余量。

5.3 不同模型规模的对照结果

为了验证计算器的通用性,我又测了几个不同规模的模型,把结果整理成表。

模型规模卡数实测并发计算器估算误差
7B1180195+8%
13B2110118+7%
70B86572+11%
70B(INT8)49098+9%

可以看到,模型越大、卡数越多,误差越大,这符合预期,因为通信开销和调度复杂度的占比都上升了。但整体误差都在百分之十五以内,作为估算工具完全合格。

6. 常见问题与排查技巧实录

6.1 估算结果和实测差太多怎么办

这是被问得最多的问题。如果差得离谱,先检查三个地方。第一,确认模型精度填对了,FP16 和 INT8 的显存占用差一倍,填错直接导致结果翻倍。第二,确认并行策略填对了,张量并行和流水线并行的显存分布完全不同。第三,确认利用率参数是否合理,填百分之八十这种理想值肯定不准。

如果这三项都没问题,那就看是不是遇到了显存碎片或者调度瓶颈。这种情况计算器本身没法建模,只能靠实测发现。

6.2 加卡之后并发没涨多少

加卡不涨并发,八成是带宽瓶颈。八卡从 PCIe 换成 NVLink,通信带宽提升十几倍,并发可能直接翻倍。但如果本来就是 NVLink,再加卡收益就会递减,因为通信开销的增长速度超过了算力的增长速度。

判断方法很简单,看计算器里带宽约束那根柱子。如果它明显低于显存和算力,那加卡就是白花钱,应该先优化并行策略。

6.3 长上下文场景的特殊处理

长上下文是并发杀手。序列长度从 4096 涨到 32768,KV Cache 直接涨八倍,并发数掉到原来的八分之一。这种场景下,显存几乎必然是瓶颈。

应对手段有几个:用 PagedAttention 这类技术减少显存浪费,用 KV Cache 量化把精度降到 INT8,或者干脆上更大显存的卡。计算器里我把序列长度做成了可调参数,你可以直接拖动看并发数的变化曲线,非常直观。

问题现象可能原因排查方向解决手段
并发远低于估算显存碎片看显存实际占用重启服务、调小 batch
加卡不涨并发带宽瓶颈看通信占比换并行策略
长上下文并发暴跌KV Cache 过大算 KV 占用量化、PagedAttention
延迟忽高忽低调度不均看请求长度分布长度分桶调度

6.4 几个我踩过的坑

第一个坑是忽略了 CUDA Graph 的显存开销。开了 CUDA Graph 之后,框架会预留额外的显存做图捕获,这部分在计算器里没体现,导致估算偏高。后来我加了一个"高级选项"开关,开了 CUDA Graph 就自动扣掉一部分显存。

第二个坑是低估了 tokenizer 和预处理的开销。这部分虽然不占 GPU 显存,但会占用 CPU 和内存,高并发下可能成为新的瓶颈。计算器只管 GPU 侧,CPU 侧得另外评估。

第三个坑是拿实验室数据直接套生产环境。实验室里请求都是均匀的,生产环境流量有波峰波谷,峰值并发可能是平均值的两三倍。所以计算器给出的数字要再打个折,留出应对突发的余量。

7. 这个计算器还能怎么扩展

目前这个版本覆盖了最常见的稠密模型推理场景,但实际业务里还有不少情况没考虑到。比如 MoE 模型,它的显存占用和算力消耗模式跟稠密模型完全不同,激活的专家数量直接影响计算量。再比如多模态模型,图像编码器会额外占用显存和算力,这部分也得单独建模。

另一个扩展方向是成本维度。现在只算了技术上的并发上限,但业务方更关心的是"每个请求的成本是多少"。把 GPU 租用价格、电力成本、利用率都加进去,就能算出一个请求的边际成本,这对定价和预算规划很有价值。

我还想加一个"反推"功能:给定目标并发数和延迟要求,反推需要多少张什么型号的卡。这个功能在扩容决策时特别有用,比正向估算更贴近实际需求。

代码我已经整理成了单个 Python 文件,没有外部依赖,复制下来就能跑。参数表也可以按自己手头的硬件改。真正用起来之后你会发现,最有价值的不是那个最终数字,而是搞清楚了自己的系统到底卡在哪一环。把这个想明白了,优化方向自然就清晰了。

返回列表