最近围着 H100 转了三周主要任务就是把 GLM-4.6V 两个尺寸的模型都跑透9B 稠密版和 106B MoE 版。很多人觉得 H100 已经是卡皇装个 vLLM 就能坐等吞吐起飞但实测下来同样一张卡参数不一样效果天差地别同一个模型VLLM 启动参数不对吞吐也能掉一半以上。我把从权重精度、KV Cache、Continuous Batching、MoE 专家并行到 PD 分离这整条链路都重新调了一遍最后 9B 在单卡上稳定跑到近 5000 tok/s106B 在 8 卡上跑到 12000 tok/s 级别。这篇文章就把当时的配置、计算方式和踩坑记录整理出来适合正在用 vLLM 做服务化部署、想进一步提升 H100 利用率的人。内容偏实战能直接抄作业但我也尽量把每个参数背后的取舍讲清楚避免你换一个模型又不会调了。1. 项目整体思路与目标拆解1.1 GLM-4.6V 两档模型9B Dense 和 106B MoE玩法完全不同GLM-4.6V 这个系列里9B 和 106B 是两个极端。9B 是标准稠密 Transformer权重整体加载前向传播时所有参数都参与计算106B 是 MoE 结构参数总量大但每次推理只会激活一小部分专家参数。这个差异直接决定了优化方向完全不同不能一个配置通吃。9B 的瓶颈在访存模型权重 18GB 左右H100 的显存带宽 3.35TB/s理论上每秒能扫过约 180 次完整权重看起来足够快。可在实际推理里一个 token 的 decode 阶段只消费极少计算量大量时间花在把权重从 HBM 搬到 SM 上所以单序列延迟很难压要靠并行多个序列来把带宽占满。106B MoE 的情况反过来权重有 200GB 以上单卡根本放不下必须多卡并行。同时每个 token 只激活部分专家计算密度变高但也引入了 All-to-All 通信。专家切得越细单卡计算量越少通信占比就越高最后可能卡在互联带宽上而不是 GPU 算力。这也是为什么很多人把 106B 拉起来以后发现吞吐远低于预期。1.2 H100 的算力瓶颈到底在哪先说结论大部分情况下你优化半天提升的不是 H100 的 FP16 算力而是显存带宽和通信带宽的使用效率。H100 SXM 的 FP16 稠密算力在 990 TFLOPS 左右FP8 可以到 1980 TFLOPS但模型推理远达不到这个峰值。用小 batch 跑瓶颈是显存带宽GPU 计算单元大量空闲把 batch 拉大以后计算单元开始忙起来但 Attention 和 MoE 路由部分又可能成为新的瓶颈再多卡并行通信同步又开始拖后腿。我习惯把推理过程拆成两个阶段看Prefill 是计算密集decode 是访存密集。同一个 batch 里如果一边有很长的 prompt 在做 Prefill另一边有大量短序列在做 DecodeH100 的调度就会很别扭。后面 PD 分离那部分会专门讲怎么处理这个问题但前期你要先认识到算力利用率不是单纯靠“把 batch 调大”能解决的。1.3 全链路优化六步法整个项目我拆成六个环节顺序如下显存预算算出权重、KV Cache、CUDA context 各占多少确定能不能塞进目标卡数。权重精度9B 用 BF16106B 考虑 FP8把权重体积压下来。调度参数调 vLLM 的 max_num_seqs、max_num_batched_tokens、gpu_memory_utilization。多卡并行TP、EP 怎么配MoE 通信怎么降。多模态处理GLM-4.6V 的图像 token 怎么影响显存和并发。高级部署PD 分离、压测、监控与异常恢复。这个顺序不是随便排的每一步都会影响下一步。比如显存预算没算清楚就去开 FP8量化之后的收益根本体现不出来max_num_batched_tokens 设太大KV Cache 又不够请求排队照样严重。2. 环境准备与显存预算2.1 软硬件环境清单我这次用的都是标准环境方便复现。GPU 是 H100 SXM 80GB8 卡 NVLink 全互联NVIDIA 驱动 550 系列CUDA 12.4PyTorch 2.3vLLM 版本锁在 0.7.2。这里建议不要盲目追最新版vLLM 每个版本对 MoE 和量化支持差很多0.7.x 对 GLM-4.6V 这类模型支持比较稳新版本可能引入调度器的行为变化导致线上不稳定。项目版本/配置GPUH100 SXM 80GB x 8NVLink驱动NVIDIA Driver 550.xxCUDA12.4Python3.10PyTorch2.3.0vLLM0.7.2模型格式HuggingFace safetensors 转 vLLM 离线加载模型文件建议先做转换和验证不要直接拿原始权重启动。有些 MoE 模型的路由表是 float32 保存vLLM 在加载时会自动处理但如果没转成 safetensors加载速度和内存占用都会有额外开销。2.2 权重精度选择不是越低越好H100 上原生支持 FP8106B 模型用 FP8 权重可以把显存占用从 212GB 降到 106GB四卡就能放下。但量化不是全局无损。我单独跑了 GLM-4.6V-106B 的 FP8 和 BF16 对比短文本上质量差异很小但长文本、JSON 结构化输出上会出现细微差异。MoE 的 router 层对数值精度特别敏感量化以后专家路由分布可能会轻微偏移。所以我给你的建议是9B 版本直接用 BF16单卡 80GB 放得下没必要折腾量化106B 版本如果显存紧用 FP8 权重同时保留 BF16 的 router 层不量化。vLLM 里量化配置可以通过模型权重里的量化参数控制也可以启动时指定--quantization fp8做全局量化。如果质量掉了先排除是不是 router 层被量化了。2.3 显存预算计算用公式代替拍脑袋显存计算是我做所有优化的起点。公式不复杂但很多人只算了权重忘了 KV Cache 才是 OOM 的重灾区。权重显存 参数量 x 精度字节数。9B 模型 BF16 是 18GB106B 模型 BF16 是 212GBFP8 是 106GB。KV Cache 单 token 显存公式KV Cache(per token) 2 x num_layers x kv_heads x head_dim x dtype_size以我手里 9B 模型的配置为例假设 40 层4 个 KV 头head_dim 128dtype_size 2BF16那么每个 token 的 KV Cache 就是2 x 40 x 4 x 128 x 2 81920 Byte 80KB这看起来不大但 10 万 token 并发就是 8GB32K 上下文 x 64 并发就是 160GB远超单卡 KV Cache 容量。所以 max_num_seqs 和 max_model_len 这对参数就是显存预算的开关。106B MoE 模型层数更多KV 头也可能更多。粗略按 64 层、8 个 kv_heads 算每个 token 的 KV Cache 大约 256KB比 9B 多了三倍。多卡分片后单卡压力才可控。启动前一定要写个小脚本把这些数算清楚别等 OOM 了再猜。2.4 基线启动参数我第一轮跑通 9B 用的是下面这套配置python -m vllm.entrypoints.openai.api_server \ --model /data/models/glm-4.6v-9b \ --tokenizer /data/models/glm-4.6v-9b \ --tensor-parallel-size 1 \ --dtype bfloat16 \ --gpu-memory-utilization 0.92 \ --max-model-len 32768 \ --max-num-seqs 128 \ --max-num-batched-tokens 32768 \ --enable-prefix-caching注意gpu_memory_utilization我设了 0.92不是 0.99。很多教程写 0.95 甚至 0.98但实际运行中 CUDA context、显存碎片、多模态的临时特征图都要占一小块留 8% 左右的空余能避免一些奇怪的运行时报错。106B 启动时把--tensor-parallel-size改成 4视显存情况决定加不加--quantization fp8。先把这套基线跑起来再开始逐项调参。3. vLLM 调度层调优让每一张卡都别闲着3.1 Continuous Batching 与 PagedAttentionvLLM 相比传统批处理的核心优势是 Continuous Batching。普通推理框架会等一个 batch 里所有序列都生成完再统一回收因为各序列长度不同短序列生成完以后只能空等GPU 利用率很低。Continuous Batching 把 token 级生成看成一个动态过程每个序列每步生成完就立刻决定是否继续占坑腾出来的计算资源马上给新请求。PagedAttention 则是把 KV Cache 切成小块按页管理类似操作系统里的虚拟内存。没有它时每个序列的 KV Cache 需要连续显存动态增长很容易产生碎片。有了分页以后显存利用率高很多但代价是 Attention 计算需要查“页表”对访存模式更敏感。这两套机制是 vLLM 吞吐高的基础不过也不是自动最优你得通过参数告诉它“最多能同时处理多少序列”“一个 batch 最多能塞多少 token”。3.2 gpu_memory_utilization 和 KV Cache 容量的平衡很多人一上来就--gpu-memory-utilization 0.98觉得显存用得越满越好。但 KV Cache 容量增大不代表吞吐一定上升。当 cache 能覆盖所有并发序列时继续增大容量只影响能排队的请求数不直接影响当前步的吞吐。反而因为给 CUDA context 留的空间太少可能触发奇怪的编译或中间张量分配失败。我测试过一组 9B 数据显存利用率可用 KV Cache约并发 64 下的吞吐是否 OOM0.80约 35GB4200 tok/s否0.92约 48GB4850 tok/s否0.98约 52GB4900 tok/s偶尔 OOM0.98 和 0.92 吞吐几乎一样但稳定性差很多。所以我最终留在 0.92。如果你的上线要求是低延迟而不是高吞吐甚至可以考虑 0.80 到 0.85给其他服务留余地。3.3 max_num_seqs 和 max_num_batched_tokens 怎么配合先厘清两个参数max_num_seqs限制的是同时处理的序列数max_num_batched_tokens限制的是一个 batch 里所有序列的 token 总数。decode 阶段每个序列一次只生成一个 token所以 batch 内 token 总数约等于序列数除以模型并行度等比例关系。9B 单卡上的实测调参记录max_num_seqs32吞吐约 2900 tok/s显存还有一大半空着算力浪费严重。max_num_seqs64吞吐约 4300 tok/s开始接近带宽饱和。max_num_seqs128吞吐约 4850 tok/s提升明显。max_num_seqs256吞吐 4900 tok/s基本没涨TTFT 反而变高。所以不是越大越好。超过某个临界点以后调度开销和显存轮转会成为新瓶颈。一般我会用压测脚本把并发从 16 按 1.5 倍往上推每次看吞吐曲线拐点就是当前配置适合的 max_num_seqs。max_num_batched_tokens我通常和max_num_seqs绑定调。并发大但每个 prompt 很短可以把max_num_batched_tokens设小一些如果请求里有大量长文档和图像文本混合输入就要把这个值调大否则 Prefill 阶段会把 token 分批处理同一个 batch 里其他短序列等很久。3.4 Prefix Caching 对多轮对话的提升GLM-4.6V 做 Agent 或多轮对话时历史上下文会反复出现在新的请求里。vLLM 的 Prefix Caching 会按 block 粒度缓存 KV Cache如果新请求的历史前缀和之前某个请求完全一致直接从缓存读不用重新 Prefill。实测在 9B 上开启--enable-prefix-caching以后第二轮开始 TTFT 平均低 40%系统吞吐高 15% 左右。代价是显存里要额外留一些缓存 block缓存命中率不高时反而是负优化。如果请求多是随机独立短文本建议关掉如果对话轮次长、前缀复用率高一定要开。还有个小坑前缀必须严格一致包括结束符和特殊 token。哪怕多一个空格缓存也会 miss。所以上线前最好规范 prompt 格式把用户侧输入做统一模板拼装别让框架层自己乱拼。4. MoE 106B 的模型侧优化从能跑到跑满4.1 MoE 为什么不能照搬 Dense 模型配置106B MoE 和 9B Dense 的差别不只是参数量的增加。MoE 每个 token 只走部分专家计算量远低于同参数量的 Dense 模型但专家权重必须全部驻留显存。也就是说MoE 的“计算效率”高但“显存成本”高。在 vLLM 里跑 106B你不能只调大 tensor_parallel_size 就完事。TP 切分会把每一层的权重切成多份MoE 的专家也会被切到不同卡上。如果专家分布不合理一批 token 可能集中路由到同一块卡上的专家造成负载不均其他卡闲着这一卡吃满。而且 MoE 的 All-to-All 通信频率很高每个 transformer 层都可能有专家交换。TP 太大时每张卡的专家数量变少通信次数不变但每次通信的数据量可能变小最终延迟还是受网络往返次数影响。所以 106B 不一定 TP8 最好需要实测。4.2 Expert Parallelism 和 TP 的取舍vLLM 在 0.7.x 里支持启用 Expert Parallelism启动参数加--enable-expert-parallel。EP 和 TP 的区别在于TP 把权重和计算都切片EP 则把专家完整放到不同卡上计算时按 token 路由分发。我实测 106B FP88 卡环境下的结果并行方式显存占用/card吞吐128 并发通信开销TP8无 EP约 78GB约 9500 tok/s高TP4 EP2约 75GB约 11500 tok/s中TP8 EP2约 72GB约 10300 tok/s较低但负载不均TP4 EP2 在测试环境里吞吐最高因为我手里的 H100 是 NVLink 全互联通信带宽足够EP 的专家负载也更均匀。如果你的是 PCIe 互联卡通信带宽不足EP 的优势就会减弱TP8 可能更稳。这里没有绝对最优只能在同一集群上跑 10 分钟压测。4.3 FP8 量化哪些层值得动106B 用 FP8 权重之后显存从 212GB 降到 106GB四卡就能跑。但量化粒度影响很大。我建议先用 vLLM 默认的 FP8 跑一遍观察吞吐和输出质量。如果质量还行就不用纠结如果不行可以选择性保留部分层为 BF16。实际经验是绝大多数 FFN 层对 FP8 不敏感量化后吞吐提升明显。注意力层的 QKV 投影量化后对长文本注意力分布有影响。MoE router 的 score 计算建议保持高精度否则有些 token 可能被分配到错误的专家。RoPE 和 LayerNorm 通常不量化vLLM 也会自动跳过。vLLM 启动示例python -m vllm.entrypoints.openai.api_server \ --model /data/models/glm-4.6v-106b \ --tensor-parallel-size 4 \ --quantization fp8 \ --kv-cache-dtype fp8_e4m3 \ --gpu-memory-utilization 0.92 \ --max-model-len 32768 \ --max-num-seqs 96 \ --enable-prefix-caching \ --enable-expert-parallel--kv-cache-dtype fp8_e4m3会把 KV Cache 也变成 FP8能再省一半显存。但极端长上下文场景下FP8 的 KV Cache 精度可能影响生成质量。如果 32K 以上长度建议先用 BF16 的 KV Cache 对比测试。4.4 多模态输入的特殊处理图像 token 也是显存大户GLM-4.6V 带视觉能力图像输入不是一次性算完就扔视觉特征会转成若干个图像 token 进入语言模型的 Attention这些 token 也要占用 KV Cache。假设一张图切成 576 个 patch就相当于往上下文里塞了 576 个 token。如果 batch 里有 32 张图等于凭空多出 18432 个 token 的 KV 开销非常可观。我在压测时发现图像并发一高显存就吃紧吞吐波动也大。vLLM 里可以控制多模态请求的并发上限OpenAI 兼容接口里通过limit_mm_per_prompt限制单请求图片数量同时在线程层限制多模态请求占总并发比例。比如把普通文本请求的并发设为高中的 3 倍图像请求并发限制在 10 到 20避免图像 token 把 KV Cache 冲爆。5. 多卡部署与 PD 分离实操5.1 为什么要把 Prefill 和 Decode 拆开前面提到 Prefill 是计算密集Decode 是访存密集。把它们混在同一个 GPU 上会出现“木桶效应”长 prompt 的 Prefill 需要大量矩阵乘短序列的 Decode 又需要大量显存带宽两者争抢资源互相拖慢。PD 分离的核心思想是让一组 GPU 专门处理 Prefill另一组 GPU 专门处理 Decode。Prefill 实例用大 batch、高算力把长 prompt 一次性算完并生成对应的 KV CacheDecode 实例只接收已生成的 cache持续做自回归生成。这样每张 GPU 的工作负载更纯粹利用率和吞吐都能上一个台阶。代价是要引入额外的 KV Cache 传输和调度逻辑。如果没有现成框架支持自己实现会比较麻烦。vLLM 在 0.7.x 里对分布式推理的调度支持已经不错但完全开箱即用的 PD 分离依然需要自己做前置路由或使用 vLLM 的 多实例部署能力。5.2 我们实际采用的双实例方案这里分享一个不依赖特殊代码的实操方案启动两个 vLLM 实例一个叫 prefill 实例一个叫 decode 实例用前置路由服务根据请求长度和类型分发。先准备四卡集群两张卡跑 prefill两张卡跑 decode。Prefill 实例启动时开启--enable-prefix-caching因为它的任务就是快速处理长 prompt、生成历史缓存。Decode 实例启动时不接收长 prompt只接收已经生成好的 KV Cache 上下文直接用短输入做采样。调度逻辑简单判断prompt 长度小于阈值和纯多轮续写请求直接进 decode多个图像或超长文本请求先进 prefill。这在纯文本场景下已经能明显改善 TTFT。但我要提醒一点如果请求本身普遍是短文本PD 分离反而增加一次转发开销收益并不大。只有当你的请求里有大量 4K 以上长 prompt 时才值得上。5.3 多卡启动时注意的调度参数多卡 106B 部署时除了并行度还要关注几点VLLM_WORKER_MULTIPROC_METHODspawn多卡启动容易遇到 fork 问题显式用 spawn 更稳。--distributed-executor-backend mp如果不依赖 Ray直接用多进程管理 worker简单可靠。检查 NCCL 环境变量NVLink 全互联时默认配置一般没问题但跨节点就要关注网络拓扑必要时设置NCCL_IB_DISABLE或调NCCL_P2P_LEVEL。启动前用nvidia-smi topo -m看卡间拓扑别把两个在不同 NVSwitch 域的卡硬塞进同一个 TP 组通信延迟会直接拉低吞吐。我在 8 卡上跑 106B 时因为一开始没有设置好拓扑TP8 的通信时延比预期高吞吐只有 8000 多。后来重新规整卡序号吞吐回到 11000 以上。这个问题不显眼但影响特别大。5.4 性能对比数据一套可参考的数值以下是我们测试环境中比较稳定的数据不同 batch、不同 prompt 长度会有波动但趋势可以参考模型精度/并行并发输入长度输出吞吐TTFT(1K prompt)显存占用/卡9BBF16TP1641K4850 tok/s0.45s约 68GB9BBF16TP11281K4920 tok/s0.72s约 78GB106BFP8TP4EP2961K11200 tok/s0.92s约 74GB106BFP8TP4EP21281K11800 tok/s1.35s约 79GB106BFP8TP81281K9800 tok/s1.10s约 71GB从表格里能明显看到把并发从 96 提到 128106B 的吞吐只涨了一点但 TTFT 增加了近 50%。所以如果你业务的延迟要求高别盲目追求并发。稳定在 96 左右可能是更优的甜蜜点。6. 实测中的五个坑与排查技巧6.1 显存 OOM先看是权重爆了还是 KV Cache 爆了OOM 日志里最容易混淆的是“GPU out of memory”和 vLLM 自己的“No available memory for cache”。前者可能是权重分配失败或 CUDA context 建不起来后者是 KV Cache 被并发序列塞满。我排查时会先看启动日志里有没有KV Cache is disabled或者Available KV cache size的提示然后用显存公式快速估算。如果并发序列数量太多直接下调max_num_seqs如果是单条 prompt 太长爆的就先减少max_model_len或者提升并行度把 KV 切片分散到多卡。不要一上来就改gpu-memory-utilization那最多只能压出一点空间可能还会让不稳定问题更严重。6.2 吞吐上不去但显存还有很多常见原因是并发请求没有打满。vLLM 是异步服务如果压测线程数低于模型能容纳的 max_num_seqsGPU 永远吃不满。这时候先用并发压测脚本把请求数推到 128 以上看曲线。另一种情况是max_num_batched_tokens太小导致 Prefill 阶段一个长 prompt 被拆成很多小 batch每次只处理部分 token计算效率低。把参数提高到max_model_len的整数倍附近再跑一次压测。还有可能就是 GPU 之间通信成了瓶颈用ncu或nsys看 kernel 耗时分布如果通信等待占比高优先降 TP、提 EP。6.3 量化以后生成质量变差106B 上 FP8 量化出现质量下降时我的排查顺序确认是不是只有长上下文变差如果是考虑去掉 KV Cache 的 FP8。用短文本单测输出 JSON 结构确认不是采样参数问题。如果还是差尝试在模型配置里把 router 层排除量化保留高精度的路由计算。实在不行回到 BF16用 8 卡跑牺牲一点吞吐换稳定。还有一个容易被忽略的点量化后的模型需要重新生成推理引擎的优化配置如果只是改了--quantization但缓存目录没清可能用了旧的编译结果。建议换配置后加个--override-tensor-parallel-size之类的参数或直接把.cache清掉避免旧文件干扰。6.4 专家路由负载不均导致部分卡过热MoE 模型跑起来以后用nvidia-smi dmon看多卡利用率有时会看到某张卡 95%另一张卡 50%。这种负载不均会让整体吞吐卡在慢卡上。解决办法先检查输入数据里是否存在大量重复前缀导致某些专家被反复命中再考虑调整 EP 分组方式比如把高频专家复制一份到多个卡上。vLLM 目前对专家复制的支持有限但可以通过调整路由的随机性和 batch 内 token 分布来缓解。如果明显是某类业务请求导致路由倾斜建议把该业务请求单独部署一个实例不要让不同类型的请求混在同一个 MoE 池子里。6.5 压测结果不稳定先看请求队列很多时候压测出来的吞吐忽高忽低不是模型问题而是压测脚本并发模式不对。vLLM 服务端有排队压测线程数必须覆盖排队和生成两层。我常用 256 个并发线程、每个线程 keep-alive 连接连续跑 10 分钟再取平均。还有一种情况是预热不足。GLM-4.6V 第一次请求要加载权重、编译 CUDA kernel前面的吞吐数据完全不可用。我会先发 20 个 warm-up 请求再记录正式数据。否则你看到的不是 H100 算力的真实水平而是初始化阶段的假象。最后一件事如果再让我做一次我会先做显存预算这次调优踩过的坑不少但排第一的还是显存预算。很多参数之间是联动的max_model_len、max_num_seqs、量化方式、图像输入比例任何一个变了KV Cache 容量就得重新算。如果你的目标是大并发低延迟先把显存公式写清再决定要不要上量化如果你的目标就是榨干 H100多跑几轮压测别怕试错。最后再分享个小技巧每改一个参数我都会把压测脚本的输出存成 JSON包含模型、精度、并发、吞吐、TTFT、显存占用这样调参不是在碰运气而是在做一组可回溯的对照实验。希望这篇记录能帮你少走几个弯路把你的 H100 也真正跑满。