ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

8张GPU并发上限计算:显存、算力与带宽三层约束模型

8张GPU并发上限计算:显存、算力与带宽三层约束模型 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为4096FP16存储序列长度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利用率A1007B125%-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权重14GBA100带宽2039 GB/s假设利用率80%可用带宽 2039 × 0.8 ≈ 1631 GB/s单请求decode时每token需要读14GB权重假设batch1加上KV Cache读取。序列长度2048时KV Cache约1GB每token读1GB/2048≈0.5MB。所以单请求每token读取约14GB 0.5MB ≈ 14GB。单请求token生成速度上限 1631 / 14 ≈ 116 tokens/s。如果batch8权重读取被8个请求共享每个请求分摊14/81.75GB加上自己的KV Cache 0.5MB每token约1.75GB。但总带宽消耗是8×1.7514GB每token步和batch1时一样。所以带宽约束下batch增大不改变总吞吐上限但能提升单请求的token生成速度因为权重读取被摊薄了。等等这里需要更仔细地分析。实际上batch8时每个decode step要读一次权重14GB加上8个请求的KV Cache8×0.5MB4MB总共约14GB。这一个step生成8个token每个请求一个所以每token的带宽消耗是14/81.75GB。相比batch1时的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 技术选型做这个计算器的时候我考虑过几种方案纯前端HTMLJS、Python脚本、还是做成一个Web服务。最后选了纯前端方案原因是第一不需要后端部署简单一个HTML文件就能跑第二用户输入参数后即时计算体验好第三方便分享发给别人直接打开就能用。前端框架用的是原生JS没上React或Vue因为功能不复杂没必要引入构建工具。图表用了Chart.js展示不同并发下的延迟和吞吐曲线。整体代码量不大核心计算逻辑大概300行。5.2 核心计算逻辑计算器的主流程是这样的用户输入硬件参数GPU型号、数量、显存、算力、带宽。用户输入模型参数参数量、层数、hidden_size、精度。用户输入请求参数输入序列长度、输出序列长度、目标延迟。计算三层约束取最小值。输出推荐并发数、预期吞吐、预期延迟。核心代码结构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函数内部就是前面几节讲的公式。比如calcMemLimitfunction 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/sA100 40GB40GB312 TFLOPS1555 GB/sH100 80GB80GB989 TFLOPS3350 GB/sRTX 409024GB165 TFLOPS1008 GB/sRTX 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序列484311.6%4×A100 80GB, 13B模型, 1024序列2225-12%8×RTX 4090, 7B模型, 512序列363116%2×H100 80GB, 70B模型, 2048序列8714%误差在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-2GBTensorRT-LLM1-1.5GBHuggingFace Transformers2-3GBDeepSpeed2-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%以内对于容量规划来说已经够用了。如果你也在做类似的事情建议从显存约束开始先把最直观的那层算准再逐步加复杂度。一上来就追求完美模型反而容易卡在细节里出不来。
返回列表