Qwen3.8 27B / 4×A100 调参记录
目标是把 4 张 A100 上的 Qwen3.8-27B INT8 服务调到更高的实际吞吐。下面只记改动、测量和决定,后续实验继续往下追加。
现在跑什么
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 |
| 批处理 / KV | max-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 / TTFT | TP4 prefill / decode / TTFT |
|---|---|---|
| 1K | 4,249 / 89.6 / 0.323s | 6,825 / 99.6 / 0.229s |
| 8K | 5,150 / 74.3 / 1.673s | 9,012 / 82.2 / 0.988s |
| 32K | 4,642 / 96.6 / 7.141s | 8,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 prefill | TP2+DP2 decode | TTFT | 相对 TP4 的 decode |
|---|---|---|---|---|
| 1K / 1 | 3,278.6 | 64.0 | 391ms | 基本相同 |
| 1K / 2 | 6,036.4 | 136.4 | 317ms | 约 +10% |
| 8K / 1 | 2,859.0 | 53.8 | 2,944ms | 略低 |
| 8K / 2 | 8,332.0 | 136.7 | 1,838ms | 约 +23% |
| 32K / 1 | 4,637.6 | 64.3 | 7,144ms | 接近 |
| 32K / 2 | 8,946.4 | 114.0 | 7,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/s | decode 总 tok/s | 单请求 decode | TTFT |
|---|---|---|---|---|
| 1K / 1 | 3,710.5 | 64.4 | 64.4 | 358ms |
| 1K / 4 | 3,815.3 | 208.8 | 66.1 | 867ms |
| 1K / 8 | 772.4 | 224.5 | 61.3 | 4,992ms |
| 8K / 1 | 3,327.8 | 61.7 | 61.7 | 2,544ms |
| 8K / 4 | 9,894.7 | 171.7 | 49.3 | 3,260ms |
| 8K / 8 | 4,136.0 | 210.8 | 72.0 | 8,127ms |
| 32K / 1 | 4,516.0 | 75.5 | 75.5 | 7,338ms |
| 32K / 4 | 8,998.4 | 116.6 | 54.1 | 12,030ms |
| 32K / 8 | 5,398.0 | 91.6 | 68.9 | 24,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/s | decode 总 tok/s | TTFT |
|---|---|---|---|
| 1K / 4 | 4,814.67 | 250.55 | 668ms |
| 8K / 4 | 6,691.13 | 226.41 | 3,758ms |
| 32K / 4 | 6,353.30 | 110.64 | 16,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。
还要做什么
- 用一长一短的混合请求验证 4096 对短请求 TTFT 的实际改善,同时记录总吞吐损失;
- 跑四条接近 262K 的冷请求 admission test,但不把容量通过写成延迟 SLO 通过;
- floating nightly 每次更新后重跑 PR #48375、模板、tool call、prefix hit 和四流 gate;
- 分别记录单请求延迟、四流总吞吐和 MTP counter delta,不再混成一个“更快”。