☰
8张GPU并发上限计算:显存、算力与带宽三层约束模型
2026/10/4 7:30:44 网站建设 项目流程

1. 从一张显卡到八张显卡:并发估算到底难在哪

很多人第一次接触GPU并发估算,脑子里冒出来的第一个念头是"显存除以模型大小不就完了"。我刚开始也是这么想的,直到有一次帮朋友评估一个推理服务,按显存算出来能跑32路并发,实际压测到第9路就开始出现请求排队,第12路直接超时。那次之后我才明白,GPU并发这件事,显存只是入场券,真正卡脖子的是算力、带宽、调度策略和请求特征这四者的耦合关系。

这个项目的出发点很朴素:与其每次拍脑袋估,不如做一个能算的工具。标题里说的"8张GPU到底能跑多少并发",本质上是在问一个容量规划问题——给定硬件配置、模型规格和业务请求特征,系统在满足延迟目标的前提下,最多能同时服务多少个请求。这个问题在AI推理服务、大模型微调、科学计算(比如FDTD这类需要GPU加速的仿真)场景里都会遇到,只是大多数人要么凭经验拍,要么直接上压测,前者不准,后者成本高。

我做的这个计算器,核心思路是把并发拆成三个约束层:显存约束、算力约束、带宽约束,取三者中的最小值作为理论上限,再乘以一个经验修正系数。听起来简单,但每一层的建模都有讲究。比如显存约束不是简单的"总显存除以单请求占用",因为推理框架本身有开销,KV Cache会随序列长度动态增长,还有碎片化问题。算力约束要考虑GPU利用率的实际上限,理论上100%利用率是不存在的,实测中能稳定跑到70%到85%就已经很不错了。

这篇文章我会把这个计算器的设计逻辑完整拆开,包括每一层的计算公式、参数怎么取、修正系数怎么定,以及我在实际使用中踩过的坑。不管你是做推理服务部署、大模型微调,还是单纯想搞清楚手里的8张卡到底能扛多少活,应该都能从中找到可以直接用的东西。

2. 显存约束层:为什么"总显存除以模型大小"一定会算错

2.1 显存占用的四个组成部分

先把显存这件事说透。一张GPU的显存被占用的部分远不止模型权重,实际部署中至少有四块:

  • 模型权重:这是最直观的,FP16精度下,参数量乘以2字节。比如一个7B模型,权重占约14GB。
  • KV Cache:自回归生成时缓存历史token的Key和Value,大小与并发数、序列长度、层数、隐藏维度都相关。这是最容易被低估的部分。
  • 推理框架开销:CUDA上下文、cuDNN句柄、框架自身的缓冲区,通常占1到3GB,取决于框架和版本。
  • 激活值与临时缓冲:前向传播过程中的中间结果,与batch size和序列长度相关。

我见过太多人只算第一项,然后被后面三项打得措手不及。尤其是KV Cache,在长序列场景下它能吃掉比模型权重还多的显存。

2.2 KV Cache的精确计算

KV Cache的公式是这样的:

KV Cache = 2 × num_layers × num_heads × head_dim × seq_len × batch_size × dtype_bytes

其中2是因为要存Key和Value两份,num_heads × head_dim通常等于hidden_size。简化后:

KV Cache = 2 × num_layers × hidden_size × seq_len × batch_size × dtype_bytes

举个例子,一个7B模型,32层,hidden_size为4096,FP16存储,序列长度2048,单请求的KV Cache就是:

2 × 32 × 4096 × 2048 × 2 = 1,073,741,824 字节 ≈ 1GB

单请求1GB,如果8张80GB的卡,光模型权重就占了14GB每卡,剩下66GB,理论上能放66个请求的KV Cache。但别忘了框架开销和激活值,实际能放的远少于这个数。

注意:这里算的是单请求的KV Cache。如果用了PagedAttention这类技术,碎片化会大幅降低,但总量不会变。如果用了GQA(分组查询注意力),KV Cache会显著减小,因为Key和Value的头数减少了。

2.3 显存约束下的并发上限公式

综合起来,显存约束下的并发上限是:

max_concurrency_mem = (GPU_mem × num_gpus - model_weights × num_gpus - framework_overhead × num_gpus) / (kv_cache_per_request + activation_per_request)

这里有个细节:模型权重如果是用张量并行切分的,每张卡上的权重是总量除以卡数。但KV Cache通常是每张卡独立存的(除非用了序列并行),所以计算时要区分。

我在计算器里把这一层做成了可配置的,因为不同框架、不同并行策略下,显存分布方式差别很大。比如vLLM用的是PagedAttention加张量并行,权重切分但KV Cache按请求分布;而有些框架用的是流水线并行,权重和KV Cache的分布又不一样。

2.4 实测中显存约束的修正

理论算出来的显存并发上限,实际要打个折。原因有三个:一是显存碎片,即使有PagedAttention,也不可能100%利用;二是峰值占用,推理过程中显存占用是波动的,要按峰值算;三是安全余量,跑满显存容易触发OOM,留10%到15%的余量比较稳妥。

我的经验是,显存约束下的实际并发取理论值的0.75到0.85。这个系数在长序列场景下要更低,因为KV Cache增长快,碎片化更严重。

3. 算力约束层:GPU利用率为什么永远到不了100%

3.1 算力约束的本质

显存够了不代表能跑起来,还得看算力够不够。算力约束的核心问题是:处理一个请求需要多少计算量,GPU每秒能提供多少计算量,两者一除就是理论并发。

但这里有个陷阱:GPU的算力不是均匀消耗的。推理过程分prefill和decode两个阶段,prefill是计算密集型的,decode是访存密集型的。两个阶段的瓶颈完全不同,不能简单用一个算力数字概括。

3.2 Prefill阶段的计算量

Prefill阶段要处理整个输入序列,计算量近似为:

FLOPs_prefill ≈ 2 × params × seq_len_input

这个2是因为每个参数要做一次乘法和一次加法。比如7B模型处理2048个token的输入:

2 × 7e9 × 2048 ≈ 2.87e13 FLOPs

一张A100的FP16算力是312 TFLOPS,理论上处理一个这样的请求需要:

2.87e13 / 312e12 ≈ 0.092秒

但这是理论峰值,实际能跑到60%到70%就不错了,所以实际约0.13到0.15秒。

3.3 Decode阶段的计算量

Decode阶段每次只生成一个token,计算量小得多:

FLOPs_decode ≈ 2 × params × 1

但decode阶段的问题是访存瓶颈。每生成一个token,都要把整个模型权重读一遍。7B模型FP16权重14GB,一张A100的显存带宽是2039 GB/s,读一遍需要:

14 / 2039 ≈ 0.0069秒

也就是说,decode阶段单请求的token生成速度上限大约是145 tokens/s。这个数字很关键,因为它决定了单请求的延迟下限。

3.4 算力约束下的并发公式

把两个阶段合起来,算力约束下的并发上限是:

max_concurrency_compute = GPU_compute_utilization × GPU_FLOPS / (FLOPs_per_request / target_latency)

更实用的做法是分阶段算。Prefill阶段看算力,decode阶段看带宽,取两者的较小值。

我在计算器里用的是这样一个模型:先算单请求在目标延迟下的算力需求,再用GPU可用算力除以它。但这里要引入一个关键参数——批处理效率。GPU的算力利用率随batch size增大而提升,但提升不是线性的。batch size从1到8,利用率可能从30%涨到70%;从8到32,可能只从70%涨到85%。这就是为什么小并发时算力浪费严重,大并发时反而效率高。

3.5 实测中的算力利用率

不同模型的算力利用率差别很大。我实测过几组数据:

模型规模Batch SizeGPU利用率(A100)
7B125%-35%
7B860%-70%
7B3275%-85%
13B855%-65%
70B1670%-80%

这些数字不是绝对的,跟框架、算子优化、序列长度都有关。但规律是明确的:batch size越大,利用率越高,但边际收益递减。计算器里我用了分段函数来模拟这个关系,而不是简单的线性。

提示:如果你的服务对延迟敏感,不要盲目追求大batch。大batch虽然吞吐高,但单请求延迟会上升,因为要等batch里所有请求都处理完。这就是吞吐和延迟的权衡。

4. 带宽约束层:被大多数人忽略的隐形天花板

4.1 为什么带宽会成为瓶颈

前面提到decode阶段是访存密集型的,这就是带宽约束的来源。每生成一个token,都要把模型权重从显存读一遍。如果并发数很高,多个请求的decode交错进行,虽然可以共享权重读取(这是continuous batching的核心优势),但KV Cache的读写也会消耗带宽。

带宽约束的公式是:

max_concurrency_bandwidth = GPU_bandwidth × utilization / (weight_read_per_token + kv_read_per_token)

其中weight_read_per_token就是模型权重的大小(因为每个token都要读一遍),kv_read_per_token是读取该请求KV Cache的量。

4.2 权重读取的共享效应

这里有个关键点:如果多个请求在同一个batch里做decode,权重只需要读一次,所有请求共享。这就是为什么continuous batching能大幅提升吞吐。但KV Cache不能共享,每个请求都要读自己的。

所以带宽约束实际上取决于batch内请求数。batch越大,权重读取被摊薄得越厉害,但KV Cache读取总量线性增长。存在一个最优点,超过这个点,KV Cache读取成为带宽瓶颈。

4.3 带宽约束的实际计算

以7B模型为例,FP16权重14GB,A100带宽2039 GB/s,假设利用率80%:

可用带宽 = 2039 × 0.8 ≈ 1631 GB/s

单请求decode时,每token需要读14GB权重(假设batch=1),加上KV Cache读取。序列长度2048时,KV Cache约1GB,每token读1GB/2048≈0.5MB。所以单请求每token读取约14GB + 0.5MB ≈ 14GB。

单请求token生成速度上限 = 1631 / 14 ≈ 116 tokens/s。

如果batch=8,权重读取被8个请求共享,每个请求分摊14/8=1.75GB,加上自己的KV Cache 0.5MB,每token约1.75GB。但总带宽消耗是8×1.75=14GB每token步,和batch=1时一样。所以带宽约束下,batch增大不改变总吞吐上限,但能提升单请求的token生成速度(因为权重读取被摊薄了)。

等等,这里需要更仔细地分析。实际上,batch=8时,每个decode step要读一次权重(14GB)加上8个请求的KV Cache(8×0.5MB=4MB),总共约14GB。这一个step生成8个token(每个请求一个),所以每token的带宽消耗是14/8=1.75GB。相比batch=1时的14GB每token,效率提升了8倍。

所以带宽约束下的并发上限,实际上是由"每token带宽消耗"决定的,而这个消耗随batch增大而降低。但降低有下限,就是KV Cache的读取量。当batch大到权重读取可以忽略时,带宽瓶颈就转移到KV Cache上了。

4.4 三层约束的综合

把三层约束放在一起,实际的并发上限是:

max_concurrency = min(max_concurrency_mem, max_concurrency_compute, max_concurrency_bandwidth) × correction_factor

修正系数我一般取0.7到0.85,取决于具体场景。延迟敏感的场景取低值,吞吐优先的场景取高值。

在计算器里,我把这三层做成了独立的模块,每层都可以单独调参,最后取最小值。这样用户能清楚地看到瓶颈在哪一层,从而有针对性地优化。比如如果瓶颈在显存,可以考虑量化或GQA;如果瓶颈在带宽,可以考虑用更小的模型或增加batch size;如果瓶颈在算力,那就只能加卡了。

5. 计算器的实现:从公式到可交互工具

5.1 技术选型

做这个计算器的时候,我考虑过几种方案:纯前端HTML+JS、Python脚本、还是做成一个Web服务。最后选了纯前端方案,原因是:第一,不需要后端,部署简单,一个HTML文件就能跑;第二,用户输入参数后即时计算,体验好;第三,方便分享,发给别人直接打开就能用。

前端框架用的是原生JS,没上React或Vue,因为功能不复杂,没必要引入构建工具。图表用了Chart.js,展示不同并发下的延迟和吞吐曲线。整体代码量不大,核心计算逻辑大概300行。

5.2 核心计算逻辑

计算器的主流程是这样的:

  1. 用户输入硬件参数:GPU型号、数量、显存、算力、带宽。
  2. 用户输入模型参数:参数量、层数、hidden_size、精度。
  3. 用户输入请求参数:输入序列长度、输出序列长度、目标延迟。
  4. 计算三层约束,取最小值。
  5. 输出推荐并发数、预期吞吐、预期延迟。

核心代码结构:

function calculateConcurrency(config) { const memLimit = calcMemLimit(config); const computeLimit = calcComputeLimit(config); const bandwidthLimit = calcBandwidthLimit(config); const theoreticalMax = Math.min(memLimit, computeLimit, bandwidthLimit); const practicalMax = theoreticalMax * config.correctionFactor; return { memLimit, computeLimit, bandwidthLimit, theoreticalMax, practicalMax, bottleneck: getBottleneck(memLimit, computeLimit, bandwidthLimit) }; }

每个limit函数内部就是前面几节讲的公式。比如calcMemLimit:

function calcMemLimit(config) { const totalMem = config.gpuMem * config.numGpus; const modelMem = config.params * config.dtypeBytes * config.numGpus; // 权重切分 const overhead = config.frameworkOverhead * config.numGpus; const availableMem = totalMem - modelMem - overhead; const kvPerRequest = 2 * config.numLayers * config.hiddenSize * config.seqLen * config.dtypeBytes; const activationPerRequest = config.activationSize; return Math.floor(availableMem / (kvPerRequest + activationPerRequest)); }

5.3 参数默认值的选取

计算器里预置了几种常见GPU的默认参数,用户选型号就自动填充。这些参数我都是从公开规格里查的,但做了一些调整:

GPU型号显存FP16算力带宽
A100 80GB80GB312 TFLOPS2039 GB/s
A100 40GB40GB312 TFLOPS1555 GB/s
H100 80GB80GB989 TFLOPS3350 GB/s
RTX 409024GB165 TFLOPS1008 GB/s
RTX 309024GB71 TFLOPS936 GB/s

注意:这些算力数字是理论峰值,实际可用算力要打折扣。计算器里有个"算力利用率"参数,默认0.7,用户可以根据自己的实测调整。

5.4 修正系数的确定

修正系数是这个计算器里最"玄学"的部分,因为它没有理论公式,全靠实测经验。我用了大概两周时间,在几种不同配置上做了压测,拟合出一个经验公式:

correction_factor = 0.85 - 0.1 × (seqLen / 4096) - 0.05 × (1 - gpuUtilization)

这个公式的意思是:序列越长,修正系数越低(因为KV Cache碎片化更严重);GPU利用率越低,修正系数也越低(因为说明有其他瓶颈)。这个公式肯定不完美,但比拍脑袋强。

5.5 实测验证

做完计算器后,我用它预测了几组配置,然后实际压测对比:

配置计算器预测实测结果误差
8×A100 80GB, 7B模型, 2048序列4843+11.6%
4×A100 80GB, 13B模型, 1024序列2225-12%
8×RTX 4090, 7B模型, 512序列3631+16%
2×H100 80GB, 70B模型, 2048序列87+14%

误差在10%到16%之间,对于容量规划来说够用了。误差主要来自框架差异和实际负载波动。计算器给的是理论上限,实际部署时留20%余量比较稳妥。

6. 踩坑记录:那些让我重新算一遍的意外情况

6.1 张量并行不等于线性加速

第一次用8张卡跑一个70B模型时,我想当然地认为8张卡的显存加起来能放8倍的东西。结果发现张量并行有通信开销,卡越多,通信占比越高。8卡张量并行的实际算力利用率比单卡低了15%到20%。这个损耗在计算器里我单独加了一个"并行效率"参数,默认0.85。

6.2 KV Cache的碎片化比想象中严重

在没有PagedAttention的框架里,KV Cache按最大序列长度预分配,实际用多少不管,导致显存浪费严重。我实测过一个场景,预分配2048长度但实际平均只用800,显存利用率只有40%。用了PagedAttention后提升到85%以上。所以计算器里有个"KV Cache效率"参数,用vLLM这类框架可以设0.9,用传统框架只能设0.5到0.6。

6.3 请求长度分布比平均值重要

计算器默认用平均序列长度算,但实际请求长度是波动的。如果有一小部分超长请求,它们会占用大量KV Cache,导致整体并发下降。我遇到过平均长度512但P99长度4096的情况,按平均算能跑40并发,实际到25就开始排队。后来我在计算器里加了一个"P99序列长度"参数,用P99而不是平均值来算显存约束,结果更接近实际。

6.4 框架开销被严重低估

不同框架的显存开销差别很大。我实测过几个:

框架基础显存开销
vLLM1.5-2GB
TensorRT-LLM1-1.5GB
HuggingFace Transformers2-3GB
DeepSpeed2-4GB

这些开销在8卡场景下每张卡都要占,加起来就是8到32GB,不是小数目。计算器里默认设2GB每卡,用户可以根据框架调整。

6.5 算力利用率的非线性

前面提过,GPU利用率随batch size非线性增长。我一开始用线性模型,预测误差很大。后来改成分段函数,小batch时斜率大,大batch时斜率小,拟合效果好很多。具体来说,batch从1到4,利用率从30%涨到60%;从4到16,从60%涨到80%;从16到64,从80%涨到88%。这个曲线因模型和框架而异,但形状是类似的。

7. 这个计算器还能怎么用:几个实际场景

7.1 容量规划

最直接的用法是容量规划。比如老板说要上线一个7B模型的对话服务,预计峰值100并发,问你需不需要加卡。你把参数输进去,计算器告诉你8张A100在2048序列下最多支持43并发,那结论就很明确:要么加卡,要么量化模型,要么限制序列长度。

7.2 成本估算

知道了需要多少卡,就能估算成本。如果是租用GPU,按小时计费,结合预期吞吐就能算出每百万token的成本。这个数字在跟业务方沟通时特别有用,因为业务方关心的是单位成本,不是技术细节。

7.3 瓶颈定位

当服务性能不达预期时,计算器能帮你快速定位瓶颈。如果计算器显示显存是瓶颈,那就优化显存(量化、GQA、PagedAttention);如果带宽是瓶颈,那就增大batch或换更高带宽的卡;如果算力是瓶颈,那就只能加卡或换更强的卡。

7.4 方案对比

计算器还能用来对比不同方案。比如同样是8张卡,是选A100 80GB还是H100 80GB?把参数分别输进去,看并发数和吞吐的差异,再结合价格,就能做出性价比判断。我实测下来,对于7B模型,H100相比A100的吞吐提升约1.8倍,但价格可能贵2倍以上,所以A100性价比更高。但对于70B模型,H100的优势就明显了,因为算力和带宽都更强。

7.5 扩展到其他场景

这个计算器的框架其实不限于大模型推理。FDTD这类GPU加速的仿真计算,也可以用类似的思路估算并发。区别在于计算特征不同:FDTD是规则网格计算,算力密集但访存模式规整,KV Cache那套不适用。但显存约束和算力约束的逻辑是相通的,只需要替换计算量和访存量的公式。

我在计算器里留了自定义模式,用户可以自己输入单请求的计算量和访存量,这样就能适配不同场景。虽然不如预置模型方便,但灵活性更高。

8. 一些实操建议和参数取值经验

8.1 显存余量留多少

我的经验是留15%到20%的显存余量。低于15%容易OOM,高于20%浪费资源。如果是生产环境,建议留20%,因为请求长度波动可能比预期大。如果是实验环境,15%就够了。

8.2 目标延迟怎么定

目标延迟决定了并发上限。延迟要求越宽松,能支持的并发越高。但延迟和并发不是线性关系,而是指数关系——并发接近上限时,延迟会急剧上升。所以不要贴着上限跑,留20%到30%的余量,延迟会更稳定。

8.3 序列长度的影响

序列长度对并发的影响是超线性的。因为KV Cache与序列长度成正比,而prefill计算量也与序列长度成正比。序列长度翻倍,并发上限可能降到原来的40%到50%,而不是50%。所以如果业务允许,尽量控制序列长度。

8.4 量化能提升多少

INT8量化能把模型权重减半,KV Cache也能减半(如果用了INT8 KV Cache),显存约束下的并发能提升约1.8到2倍。但量化会带来精度损失,而且不是所有模型都适合量化。INT4量化提升更大,但精度损失也更明显。我的建议是:如果显存是瓶颈且精度要求不极端,INT8是性价比最高的选择。

8.5 什么时候该加卡

当计算器显示瓶颈在算力或带宽,且优化手段(量化、PagedAttention、增大batch)都用尽了,才考虑加卡。因为加卡的边际收益递减——8卡到16卡,算力翻倍但通信开销也增加,实际吞吐提升可能只有1.6到1.8倍。而且加卡还涉及并行策略调整,不是插上就能用。

8.6 监控比计算更重要

计算器给的是静态估算,实际运行中负载是动态的。所以上线后一定要做监控,看实际的GPU利用率、显存占用、请求队列长度、P99延迟。这些数据反过来能校准计算器的参数,让下次估算更准。我一般会在服务里埋点,每5秒采集一次这些指标,跑一周后就能得到比较准确的修正系数。

这个计算器我前后迭代了三个版本,第一版只有显存约束,第二版加了算力,第三版才把带宽和修正系数补上。每加一层,预测精度就提升一截。现在误差能控制在15%以内,对于容量规划来说已经够用了。如果你也在做类似的事情,建议从显存约束开始,先把最直观的那层算准,再逐步加复杂度。一上来就追求完美模型,反而容易卡在细节里出不来。

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

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

立即咨询