
拿 B200 单卡去跑 Qwen3-8B第一反应基本都是“杀鸡用牛刀”。8B 的 BF16 权重也就 16GB 出头B200 可是背着 192GB 显存、8TB/s 带宽的大家伙怎么可能跑不动但真正上手会发现卡你的根本不是算力而是大模型推理里最容易被忽略的带宽瓶颈。这篇文章就从 B200 单卡跑 Qwen3-8B 这个场景出发把 prefill 和 decode 两条路径的带宽账一笔笔算清楚再给出一套能直接复现的 vLLM 配置和测速方法适合想搞懂大模型推理性能瓶颈、或者正在给推理服务选型和调参的工程师。1. 为什么单卡 192GB 显存还会被“带宽”卡住1.1 先看清 B200 的牌面算力、显存、带宽三个数字B200 这卡的三围数字需要分开看。显存192GB HBM3e这个数字对 8B 模型来说确实大到离谱。带宽8TB/s 左右。这是从 H100 的 3.35TB/s、H200 的 4.8TB/s 一路提上来的但注意它的涨幅远没有算力那么夸张。算力FP4/FP8 张量核心的峰值已经到了 PFLOPS 级别官方口径接近 20 PFLOPSFP4 稀疏。问题就出在这算力翻了几十倍带宽只翻了两三倍。大模型推理在生成阶段恰恰是一个带宽敏感负载所以你拿 B200 跑 8B 模型感觉上不是“算不过来”而是“数据喂不过来”。打个比方算力是厨师颠勺的速度带宽是传菜员端盘子的速度。B200 相当于请了一批顶级大厨但后厨到餐桌的走廊就那么窄传菜员用 8TB/s 匀速上菜菜量稍微一大大厨全在等菜。用 roofline 模型看会更清楚算力除以带宽就是这个平台的算术强度拐点。B200 的带宽 8TB/s算力是 PFLOPS 级这个拐点在几千 FLOP/byte。而 decode 阶段每生成一个 token只做大约 2×参数量的一次矩阵乘法却要把模型权重完整读一遍算术强度只有 1 FLOP/byte。1 远远小于几千所以直接落在“内存受限”区域这是硬件决定死的不是软件调参能绕开的。1.2 8B 模型的真实显存账本权重 KV Cache很多人以为 8B 模型在 B200 上只占 16GB剩下一百多 GB 全空着。实际上跑服务时显存是这样分配的。Qwen3-8B 的 BF16 权重是 16.2GB 左右FP8 权重约 8.1GB。除了权重KV Cache 是大头。以 Qwen3-8B 的 GQA 结构为例36 层、8 个 KV 头、head_dim 128、BF16 存储每个 token 的 KV Cache 体积是2 × 36 × 8 × 128 × 2 bytes 147,456 bytes ≈ 144KB/token也就是说单用户 32K 上下文就需要 4.6GB KV Cache64 个用户各 32K 上下文就得 294GB直接把 192GB 撑爆。所以 B200 跑 8B 并不是“显存用不完”而是“显存总量决定了你能同时挂多少人、开多长上下文”。这个账本还有一个容易被忽略的细节KV Cache 不止占显存它在 decode 阶段也是要反复读取的。你服务的人越多、每个用户的上下文越长KV Cache 占的带宽就越大最后甚至超过权重本身的搬运量。这个问题放到第 2 节具体算。2. Prefill 和 Decode两个阶段两种瓶颈2.1 Decode 阶段是纯内存搬运算力基本在看戏先看最核心的 decode生成 token阶段。模型每生成一个 token理论上要做一次完整的 forward其中对权重最重的操作是把每一层的参数从 HBM 搬到 SM算一个很小的矩阵向量乘再继续下一层。8B 模型 BF16 权重 16GB每生成一个 token 都至少要读 16GB 的权重。B200 带宽 8TB/s这个动作的理论耗时是16GB / 8,000GB/s 0.002 秒 2ms单卡单用户的理论上限就是每秒 500 token。如果把权重压到 FP8变成 8GB耗时就降到 1ms上限变 1000 token/s。这就是为什么量化对生成速度的影响是“线性”的——它不改变算法复杂度它改变的是需要跨过带宽搬运的数据量。这时候你会发现 GPU 的利用率再高也没用因为 SM 大部分时间不是在做运算而是在等内存数据。更准确地说decode 阶段对每个权重字节只做约 1 次浮点运算B200 的算力在这种情况下基本是闲置的。真正的主力是 HBM 控制器和内存总线。2.2 KV Cache 的带宽账单并发和长上下文的隐藏代价decode 阶段读的不只是权重还有 KV Cache。这里有一个很多人都会踩的认知误区模型权重是“所有用户共享一份”的但 KV Cache 是“每个用户各自一份”的每个 step 都要把这一批用户各自的 KV 全读一遍。举个例子。假设你现在服务 32 个并发用户每个用户的上下文平均 8K token。权重按 BF16 算 16GB每个序列 8K 上下文的 KV Cache 是144KB/token × 8192 ≈ 1.18GB一个 decode step 需要读的总量是权重 16GB 32 个序列各自的 KV 1.18GB × 32 ≈ 16GB 37.8GB ≈ 53.8GB53.8GB 除以 8TB/s一个 step 大约 6.7ms。这个 step 里你生成了 32 个 token所以系统总吞吐是32 / 0.0067s ≈ 4776 token/s平均到每个用户是每秒 149 token。注意KV Cache 的读取量已经占到总带宽的 70%权重反而只占 30%。并发数越多、上下文越长这个比例还会继续倾斜。这个例子解释了为什么你明明用 B200 跑一个 8B 小模型单个用户的速度也没有想象中那么快而且并发一上去单用户速度就会往下掉。不是并发调度做得差是 KV Cache 的带宽账单在那里等着你。想要提速方向应该是减少每 token 的 KV 字节数用 FP8/E4M3 存 KV Cache体积减半减少上下文长度能 4K 解决的别开到 32K减少重复前缀开 prefix caching让相同前缀的 KV 被复用而不是重新计算和反复读取。2.3 用算术强度判断你的场景到底是算力瓶颈还是带宽瓶颈把两个阶段放到一起对比逻辑就清楚了。decode 阶段每生成一个 token计算量大约是 2 × 参数量 16 GFLOP读取的字节数等于权重加 KV Cache。所以算术强度在 1 附近任何 GPU 跑 decode 都是带宽瓶颈。prefill 阶段输入 1 个 token 的计算量同样是 2 × 参数量但输入 1000 个 token就能复用这 16GB 权重做 1000 次计算算术强度就是 1000 FLOP/byte 量级。这时 B200 的算力才真正派上用场prefill 变成算力瓶颈。换句话说同一个 GPU 上短输入、长输出是带宽瓶颈长输入、短输出是算力瓶颈。这也是为什么在做性能优化时一定要分清当前主要矛盾。你拿 B200 单卡跑 Qwen3-8B如果业务的 prompt 很短、主要靠长文本生成那优化方向就是让数据体积变小如果是复杂推理、思维链很长经常需要 prefill 几万 token那优化方向就是算力调度和并行策略。3. 实操在 B200 单卡上把 Qwen3-8B 跑到接近带宽上限3.1 vLLM 启动参数与显存分配逻辑实操我直接以 vLLM 为准当前版本对 Qwen3-8B 支持很成熟。启动命令大致是这样vllm serve Qwen/Qwen3-8B \ --dtype bfloat16 \ --max-model-len 32768 \ --gpu-memory-utilization 0.97 \ --max-num-seqs 128 \ --max-num-batched-tokens 8192 \ --enable-prefix-caching几个关键参数背后的逻辑要说明白。--gpu-memory-utilization 0.97vLLM 会把 97% 的显存预留给模型权重和 KV Cache 池。B200 上有 192GB模型权重占 16GB剩下的全部归 KV Cache 管理这样单卡能撑住的并发数远比你想象的大。--max-num-seqs 128限制同时参与调度的序列数。8B 模型显存富余开 128 通常没问题。但注意这个值越大KV Cache 的带宽占比越高单序列速度越慢。它是用单用户体验换总吞吐。--max-num-batched-tokens 8192限制一个 batch 累积的 token 数。prefill 和 decode 混跑时如果 prefill 的 chunk 太大会长时间占住 GPU让正在等待生成的 decode 序列全部卡住。这个参数就是在“prefill 吞吐”和“decode 延迟”之间做取舍。--enable-prefix-caching多用户共享系统提示词、工具定义或 few-shot 前缀时前缀 KV 可以被复用。B200 带宽这么贵能省一笔是一笔。如果确认自己的推理质量容忍 FP8可以加--quantization fp8权重加载直接从 16GB 变 8GBdecode 理论上限翻倍。不过实际用下来FP8 对 Qwen3-8B 的精度损失很小但需要先确认你用的 vLLM 版本和硬件驱动支持动态 FP8 量化否则老老实实用 BF16 先跑通。3.2 不改一行代码从带宽里榨速度的几个有效手段不写 CUDA、不改模型结构也能做不少事。这些手段的收益都来自“减少跨 HBM 搬运的字节数”。第一KV Cache 用 FP8。在 vLLM 里可以对 KV Cache 单独设精度BF16 的 KV 是每 token 144KBFP8 直接减半到 72KB。注意这和权重量化是两回事。长上下文场景下KV 减半意味着同样的显存能缓存更多序列同时每个 decode step 要读的 KV 体积直接变小。之前那个 32 并发的例子如果 KV 用 FP8KV 读取总量从 37.8GB 降到 18.9GB一个 step 的总读取量从 53.8GB 降到 34.9GB系统总吞吐能从 4776 token/s 提到 7335 token/s 左右。收益非常可观。第二合理设置--max-model-len。很多人图省事直接开到 128K但如果业务很少用到超长上下文这个设置会一直为长上下文保留显存还会让页调度表变大。实测中把 max-model-len 从 128K 降到 32K显存占用和 KV Cache 池的效率都有改善。按需设置别追大。第三开 prefix caching 并设计好 prompt 模板。推理服务如果有一段时间固定不变的前缀比如系统提示词vLLM 能自动检测并复用前缀 KV。在 B200 这种高带宽但依旧资源紧张的场景里让一段 KV 从“每次重算”变成“一次计算、多次复用”省的不只是算力更是带宽。实测一个 4K 公共前缀命中后 decode 阶段的输入 KV 读取量可以少掉相当大一块尤其在多用户场景下呈倍数放大。3.3 用脚本实测 TTFT、TPOT 和吞吐启动服务后我需要实际的数字来验证是否接近带宽瓶颈。下面这个脚本通过 OpenAI 兼容接口做压测统计 TTFT首 token 延迟、TPOT每 token 平均耗时和吞吐。import time from openai import OpenAI client OpenAI(base_urlhttp://localhost:8000/v1, api_keyEMPTY) prompt 请用 200 字解释大模型推理中的带宽瓶颈。 * 5 payloads [prompt] * 20 ttft_list [] tpot_list [] total_tokens 0 start_all time.time() for p in payloads: start time.time() resp client.chat.completions.create( modelQwen/Qwen3-8B, messages[{role: user, content: p}], max_tokens256, temperature0.7 ) first_token_time start # 由于同步接口拿不到逐 token 时间这里用简单方式统计整体 ttft_list.append(resp.created) # 占位说明需要异步逐 token 打点 total_tokens len(resp.choices[0].message.content) elapsed time.time() - start_all print(总耗时: %.2fs % elapsed) print(总生成 token 数:, total_tokens) print(吞吐: %.2f token/s % (total_tokens / elapsed))实际工程中用同步接口统计 TTFT 不太方便建议直接看 vLLM 的日志指标或者用异步流式接口给每个 chunk 打时间戳第一个 chunk 的等待时间就是 TTFT后续 chunk 间隔的平均值就是 TPOT。简单起见vLLM 服务日志里本身会打印平均 prompt 吞吐和生成吞吐压测时观察这两个数就够了。测完之后的判断方法和预期值如果单用户 TPOT 在 2.2ms 到 3ms 之间说明 BF16 权重的带宽基本吃满了再调参空间不大。如果 TPOT 远高于 3ms先排查是不是系统里有 prefill 任务在挤带宽或者并发序列过多导致 KV Cache 读取量过大。如果多用户总吞吐长期在 4K 到 5K token/s 量级并且继续加并发吞吐不再明显上涨说明已经顶在带宽墙上了下一步该考虑 KV Cache 量化和缩短上下文。4. 从 B200 单卡往外看带宽瓶颈的影响范围4.1 不同单卡的带宽账对比B200 单卡是现在单卡带宽的巅峰之一但把它放到不同显卡的横向对比里更能看出 8B 模型推理对带宽的依赖。GPU显存带宽8B BF16 权重读取耗时decode 理论上限RTX 409024GB约 1TB/s约 16ms约 62 token/sH100 SXM80GB3.35TB/s约 4.8ms约 208 token/sH200141GB4.8TB/s约 3.4ms约 294 token/sB200192GB8TB/s约 2ms约 500 token/s这组数字很直观一个 8B 的“小模型”在 4090 上生成速度理论上限只有 60 多 token/s很多人觉得 4090 推理慢不是 4090 芯片不行而是它的显存带宽撑不起每 token 读一遍完整权重。B200 把上限拉到 500 token/s本质上是 HBM 堆出来的。这个视角也解释了一个反直觉现象GPU 算力翻倍对短输出场景的加速效果不明显显存带宽翻倍decode 速度立刻翻倍。原因还是算术强度——decode 阶段根本不缺算力缺的是把数据喂进 SM 的那条通道。4.2 为什么单卡放得下时多卡张量并行反而更慢有人会问B200 单卡都这样了我用 4 卡跑 8B 会不会更快答案通常是否定的尤其是在单机多卡互联带宽有限的情况下。张量并行TP会把模型权重切成多份每张卡只读自己那份乍看单卡读取量变小了。但 TP 在每个 Transformer 层之间需要做两次 AllReduce把各卡的部分结果同步成完整结果。8B 模型虽然不大但每生成一个 token 都要同步一批中间张量通信频率极高。在 memory-bound 的 decode 阶段TP 引入的通信延迟和调度开销很容易超过“每卡读少量权重”省下来的时间。所以对于 8B 模型只要单卡显存放得下就不要上 TP。B200 单卡 192GB 是这种模型的最佳形态。如果以后要跑 70B、几百 B 的模型情况会反过来显存放不下才是硬约束TP 是不得已而为之而且要优先考虑 NVLink 带宽高的机型。这是两个完全不同的决策场景。4.3 用 nano-vllm 从代码层面理解带宽去哪了调参调到一定程度光看指标还是不够最好从代码层面理解一次。我推荐读 nano-vllm 这类极简实现几百行代码就把 vLLM 的核心骨架拆出来了调度器选序列、显存管理器分配 KV block、逐层做 GEMM/GEMV。读完最大的收获是意识到 decode 阶段的主循环长什么样每个序列拿自己分配到的 KV block到每一层去读权重和 KV算完注意力再写一小段新 KV。这个循环里权重是固定开销KV 是随并发和上下文增长的变动开销两个都要过 HBM。nano-vllm 里没有复杂的 kernel fusion代码路径看得一清二楚你省不掉“读权重”这动作就只能想办法让读的东西更小、复用率更高。这就是带宽瓶颈的本质。5. 常见问题与排查实录5.1 问题速查表自己跑了几天把现场踩过的几个典型问题整理成一个速查表供参考。现象直接原因处理方式单用户 TPOT 只有 3ms远高于理论 2msKV Cache 读取和调度开销叠加很难跑满理论值检查是否混入 prefill降低 KV Cache 精度用更长输出压测取稳态值显存只用了 30GBGPU 利用率却 90%工作集远超 L2数据主要在 HBM 上搬这是带宽瓶颈的典型特征转换优化思路重点看字节搬运量并发加到 128总吞吐反而停滞权重共享收益已被 KV Cache 线性读取抵消收缩上下文长度KV Cache 用 FP8启用 prefix caching降低 max-num-batched-tokens 后 TTFT 变好prefill chunk 变小不再长时间阻塞 decode保留该参数为较低值代价是 prefill 吞吐下降同一份配置换 H200 后性能提升不到两倍8B model 的带宽需求没变瓶颈搬移受限直接把期望值按带宽比例折算再对比实测值5.2 排查带宽上限的实操心得排查时最容易犯的错误是拿nvidia-smi的利用率去判断 GPU 是否满载。decode 阶段 kernel 普遍偏小SM 利用率可能只有 50%但 HBM 已经满负荷了。nvidia-smi显示的是 SM 活动状态不是内存带宽利用率。要看带宽真实情况理想工具是 Nsight Compute 的dram__bytes_read.sum和dram__bytes_write.sum但云上和虚拟化环境常常没有性能计数器权限。退而求其次的办法是直接用实测 token/s 反推把单用户 TPOT 乘以当前权重体积加 KV 体积估算出实际有效带宽再除以 8TB/s 得到利用率。我自己的经验是有效利用率能到 70% 以上就已经算把这张卡吃得很干净了。另外提一个容易混淆的地方很多人把“并发上不去”直接归因于显存不够但 B200 单卡跑 8B 的显存通常不是第一限制带宽才是。先看 KV Cache 池还剩下多少再看每 step 读取总量。如果 KV Cache 池还大把空余但总吞吐已经不再涨基本就是带宽墙。5.3 一个值得警惕的坑不要为了跑分刻意压低并发做性能对比时单并发测出来的 token/s 通常很好看但真实业务往往是多用户混合负载。我在实测中遇到过一个典型场景单用户能跑到 450 token/s但服务一接入真实流量平均速度直接掉到 100 token/s 以下。这不是模型变慢了而是多个用户的 KV Cache 同时在抢带宽。所以做容量评估时不要只看单用户速度要测试“固定并发数下的系统总吞吐”和“P99 单用户速度”这两个指标。对 B200 单卡跑 Qwen3-8B 这个组合合理的容量预期可以按 2ms 到 3ms 一个 decode step 来估算一个 step 读一遍权重加一批 KV生成 batch 大小个 token。想要维持单用户体验在 100 token/s 以上batch 就不能开太大这是一个明确的工程取舍。最后再分享一个小技巧。如果你在做推理服务的带宽优化最划算的第一步永远是打印一行日志模型权重字节数除以 GPU 带宽得到的就是 decode 单 token 的理论最低耗时。拿这个数去和实测比比任何 profiling 工具都直观。先把这笔账算清后面调的每一个参数都有了参照系。我自己后来做任何 GPU 推理方案都会先按这个公式过一遍少走了很多弯路。