Tuning log / GPU inference

Qwen3.8 27B / 4×A100 调参记录

目标是把 4 张 A100 上的 Qwen3.8-27B INT8 服务调到更高的实际吞吐。下面只记改动、测量和决定,后续实验继续往下追加。

开始于 2026-08-17持续更新

现在跑什么

30001 现在使用 4 张 A100-SXM4-80GB,拆成两组 TP2,也就是 TP2 + DP2。模型是 Qwen3.8-27B INT8 W8A16,服务镜像是 vLLM floating nightly。30000 上原有的 397B 服务没有动。

项当前值
并行TP=2, DP=2, PP=1
上下文 / 并发max-model-len=262144;每个 replica max-num-seqs=2,全机最多 4 条 active sequence
批处理 / KVmax-num-batched-tokens=8192;long-prefill-token-threshold=4096;fp8_e4m3
推测解码MTP=3
缓存chunked prefill / prefix cache 开;PR #48375 启动补丁;编译缓存持久化
模板 / 工具固定 froggeric v22.1 模板;qwen3_xml parser;默认 reasoning effort 为 medium
通信NVLink / NCCL P2P 开;vLLM custom all-reduce 关

调参流水

1. 先跑通稳定版:TP2 / 8192 / MTP3

改动:用 vLLM v0.26.0-cu129 启动 compressed-tensors 模型,保留模型卡的 FP8 KV、8192 batched tokens 和 MTP3。

遇到:vLLM custom all-reduce 在这台机器报 CUDA invalid argument。GPU 0-3 的 NVLink 和双向 peer access 已确认正常,所以没有照搬 3090 配置去关 NCCL P2P,只加了 --disable-custom-all-reduce,让通信走 NCCL。

决定:保留 NVLink/P2P;保留 --disable-custom-all-reduce。

2. TP2 和 TP4 单流对照

方法:本地 llama-benchy 调 OpenAI-compatible API;每格一次,输出固定 512 tokens,prefix cache 关闭。

输入TP2 prefill / decode / TTFTTP4 prefill / decode / TTFT
1K4,249 / 89.6 / 0.323s6,825 / 99.6 / 0.229s
8K5,150 / 74.3 / 1.673s9,012 / 82.2 / 0.988s
32K4,642 / 96.6 / 7.141s8,436 / 77.8 / 3.963s

决定:这一轮先留 TP4。它明显改善长输入 prefill 和 TTFT;32K decode 的单次结果变差,后面继续看并发吞吐。

3. batched tokens 从 8192 提到 16384

结果:8K TTFT 从 1.673s 变成 1.710s;32K prefill 从 4,642 降到 3,797 tok/s,TTFT 从 7.141s 变成 8.711s。

决定:回退到 8192。这台机器当前没有从 16384 得到收益。

4. 稳定版切 floating nightly

改动:换到 0.27.2rc1.dev122+g8efa13b70,其他 benchmark 条件不变。

结果:长输入 prefill 基本接近稳定版,但六个对照格的 decode 都下降。单流下降 13%~36%,双流总 decode 下降 9%~23%。没有 OOM、重启或请求错误。

决定:nightly 暂时保留用于 Qwen3.8 兼容性验证;不能把它写成性能升级,decode 回退仍需继续查。

5. MTP2 / MTP3 acceptance 排查

MTP2,默认采样:8K/1024 单流为 1,076 / 1,942 = 55.41%;固定 prompt 三次合计 52.74%。

MTP2,greedy 诊断:8K/512 为 585 / 878 = 66.63%。这说明采样策略会明显改变 accept,但不代表线上应该改成 greedy。

MTP3 旧窗口:混合流量累计约 49.70%。模型卡的 65.5% 没有 prompt、seed 和原始计数,当前无法做一比一复现。

决定:恢复 MTP3。accept 只用同一 prompt、同一采样下的 counter delta 比较;最终仍以 decode tok/s 为准。

6. TP4 改成 TP2 + DP2,同时把上下文降到 128K

改动:四张卡拆成两个 TP2 replica;max-model-len 从 262144 降到 128000;MTP3、batch 8192、FP8 KV 不变。

输入 / 并发TP2+DP2 prefillTP2+DP2 decodeTTFT相对 TP4 的 decode
1K / 13,278.664.0391ms基本相同
1K / 26,036.4136.4317ms约 +10%
8K / 12,859.053.82,944ms略低
8K / 28,332.0136.71,838ms约 +23%
32K / 14,637.664.37,144ms接近
32K / 28,946.4114.07,149ms约 +26%

决定:当前保留 TP2 + DP2。它没有改善单请求速度,但在至少两条独立请求时,aggregate decode 吞吐更高,这更符合现在的服务目标。

7. 当前 MTP3 计数快照

累计:12,093 个 draft tokens,接受 5,861 个,accept 48.47%;三个位置分别为 66.34%、45.52%、33.54%。

11:40 基线后的业务流量 delta:684 个 draft tokens,接受 400 个,accept 58.48%;三个位置分别为 71.49%、57.89%、46.05%。

记录方式:这是两个不同窗口,不合并成一个“当前 accept”。vLLM counter 是进程累计值;下一轮实验前重新记基线,只比较 delta。

8. prefix cache + PR #48375 试开后回滚

改动:没有重建或拉取镜像。30001 在启动时应用 PR #48375 的 9 行 Python 修复,然后打开 prefix cache。

窄验证:原镜像在 drop_eagle_block=true 时,full-attention/Mamba 命中块数是 4/5;应用补丁后是 4/4。6 次相同的 33,012-token 请求也出现了 30,400~32,000 cached tokens。到这里,只能证明文件被修改、cache 能命中。

真实 gate:本地 max-8-stream 无缓存矩阵完成后,prefix-cache 专项在正式测试前就没有通过 coherence:模型对“法国首都”输出连续重复的中文/阿拉伯 token。唯一 cache_salt、thinking-off 和 cached_tokens=0 都不能恢复正确输出。

回滚验证:恢复 prefix-cache-off compose 并重启 30001 后,相同请求立即恢复为包含 Paris 的正常回答,0 error/abort,30000 未动。

当时决定:先关闭 prefix cache 并保留回滚点,继续定位为什么文件已修改却没有保护真实运行路径。

9. 本地 llama-benchy:并发上限跑到 8

方法:prefix cache 关闭,PP=1K/8K/32K,TG=512,流数 1/2/4/8,每格一次。8 流超过两组 replica 合计 4 个同时执行槽,因此结果包含排队。

输入 / 流数prefill 总 tok/sdecode 总 tok/s单请求 decodeTTFT
1K / 13,710.564.464.4358ms
1K / 43,815.3208.866.1867ms
1K / 8772.4224.561.34,992ms
8K / 13,327.861.761.72,544ms
8K / 49,894.7171.749.33,260ms
8K / 84,136.0210.872.08,127ms
32K / 14,516.075.575.57,338ms
32K / 48,998.4116.654.112,030ms
32K / 85,398.091.668.924,258ms

决定:短输入总 decode 在 8 流最高,但 32K 到 8 流已经明显排队并降低总吞吐。当前更合理的工作区间是 4 流;这些仍是单次 smoke 数据。

10. 找到 PR #48375 被绕过的路径,重新打开 prefix cache

根因:补丁文件路径是对的。实际 hybrid cache page 是 1600 tokens,但 compose 强制了 --prefix-match-unit 16。当前 nightly 在 alignment_tokens < block_size 时会走 Mamba fine-grained 分支并提前返回,位置在 PR #48375 的 9 行改动之前。旧单测把 block size 和 alignment 都设成 16,所以给出了假阳性。

改动:保留 PR #48375,删除 --prefix-match-unit 16,让 vLLM 使用 1600-token 公共 page/hash unit。启动 wrapper 现在会拒绝任何显式 prefix-match-unit,并用真实 1600/1600 条件验证:control full/Mamba 为 5/5,MTP drop 后为 4/4。

真实 gate:27K 共享前缀顺序请求在两个 DP replica 预热后每次命中 16,000 tokens,8/8 正确;三轮 8 路并发共 24 个首都回答全部正确。llama-benchy 在 8K 和 32K context、8 路并发下都通过 coherence,32K 测完后再发 8 路 Paris 仍为 8/8。工具调用 4/4 返回正确的 get_weather({"city":"Paris"}),后两次各命中 8,000 tokens。

状态:记录时累计 cache hit 892,800 tokens,error / abort / repetition 均为 0,容器重启 0、无 OOM;30000 未动。

决定:30001 打开 prefix cache。保留 source SHA、vLLM commit 和显式 prefix-match-unit 三道 fail-closed 门禁;后续 floating nightly 变化必须重新验证。

11. 固定 Qwen3.8 chat template 和 XML tool parser

改动:不修改只读模型目录。把 froggeric/Qwen-Fixed-Chat-Templates 的 v22.1 模板固定到 revision 2b50d8ef73e9ba606680856aaeb46ab94702e788,SHA-256 为 e4d4955420645f1a65fb89fc1a4ed676fee7cd8214e76a3f520c8c8f33784da7;vLLM parser 从 qwen3_coder 改为当前 nightly 支持的 qwen3_xml。

验证:上游 34/34 模板测试通过。30001 上默认 medium reasoning、逐请求 thinking-off、原生 XML tool call、OpenAI string arguments 和 tool-result continuation 均通过。

决定:保留固定 revision 和 hash,不跟随 Hugging Face main 漂移。默认 medium 不注入额外推理强度;客户端仍可逐请求关闭 thinking。

12. 上下文开到 262K,四流固定 8192 / 4096

改动:max-model-len 从 128000 恢复到 262144;仍是 TP2 + DP2,每个 replica 最多 2 条 active sequence。总 scheduler budget 保持 max-num-batched-tokens=8192,新增 long-prefill-token-threshold=4096。这里的 4096 不是第二个 batch size:它限制单个长 prefill 每个调度步最多占一半预算,给同 replica 的另一条请求留出空间。2048 没有启用。

容量:启动时每个 DP replica 分配 3,153,050 个 KV token,vLLM 打印 262,144-token 最大并发 12.03x。GPU 0-3 各占用约 75,917 MiB,容器重启 0、无 CUDA OOM。这证明 KV 分配能覆盖四条 262K 上限,不代表四条冷 262K prefill 已满足延迟 SLO。

方法:本地 llama-benchy 0.4.0 调 30001 的 OpenAI-compatible API;并发 4、冷 prompt、每格两次、固定生成 256 tokens。

输入 / 并发prefill 总 tok/sdecode 总 tok/sTTFT
1K / 44,814.67250.55668ms
8K / 46,691.13226.413,758ms
32K / 46,353.30110.6416,742ms

MTP3:同一冷缓存四流窗口里,9,204 个 draft tokens 接受 6,180 个,accept 为 67.14%;三个位置分别为 82.4%、65.6%、53.4%。这次在明确的计数窗口里复现并略高于模型卡的 65.5%,位置越靠后 accept 越低的趋势也正常。

Prefix cache:重复 7,018-token prompt 的第二次请求命中 3,200 tokens,回答仍为预期的 CACHE_OK,墙钟时间从 2.47s 降到 1.23s。

决定:停止测试 16384,30001 保留 8192/4096、262K、TP2 + DP2、MTP3 和 prefix cache。4096 会让超长 prefill 多走几个调度步,32K TTFT 的代价需要继续观察;2048 只有在混合负载仍出现明显短请求阻塞时才进入下一轮 A/B。

还要做什么

  1. 用一长一短的混合请求验证 4096 对短请求 TTFT 的实际改善,同时记录总吞吐损失;
  2. 跑四条接近 262K 的冷请求 admission test,但不把容量通过写成延迟 SLO 通过;
  3. floating nightly 每次更新后重跑 PR #48375、模板、tool call、prefix hit 和四流 gate;
  4. 分别记录单请求延迟、四流总吞吐和 MTP counter delta,不再混成一个“更快”。
测试数据均来自本地 llama-benchy 调用远端 OpenAI-compatible API。单格一次的结果只用于选下一步参数,不当作正式性能报告。参考:Qwen3.8-27B-INT8-W8A16-MTP 模型卡。