# SGLang 投机解码在 32GB 卡上的选型指南:DFlash2 / MTP / 无投机实测
-
之前发的关于SGLang调试经历的帖子,大家翻阅参考:
https://lcz.me/topic/1621
https://lcz.me/topic/1620
上一篇把 Qwen3.8-27B 在 RTX 4080 SUPER 32G 上用 SGLang 跑通并修好了 MTP 的 KV 池崩塌问题。这一篇回答一个更实际的问题:同样是投机解码,MTP 和 DFlash2 到底选哪个?整机配置
CPU:AMD Ryzen 7 9700X(8核16线程)
内存:64GB DDR5
显卡:NVIDIA RTX 4080 SUPER 32GB(32760 MiB)
系统:Ubuntu(SGLang 这套)/Windows 11(上一篇 llama.cpp 那套)结论先行:短上下文和长上下文,DFlash2 全面胜出(短上下文快 1.5~1.8 倍,128K 长上下文快 1.67 倍);代价是显存——DFlash2 的草稿模型要占 3.69GB 显存,KV 池从 33 万 token 掉到 11~14 万,双路 128K 从此不可能。
三条路全部实测,同一台机器、同一个目标模型、同一批提示词。
一、结论表(同版本 0.5.19,RedHatAI INT4 目标模型)
场景 无投机 NEXTN (MTP) DFlash2 DFlash2 vs MTP 短上下文 代码 解码 41.8 t/s 89.3 t/s 130.5 t/s +46% 短上下文 抽取原文 解码 40.6 t/s 86.5 t/s 143.0 t/s +65% 短上下文 中文创作 解码 42.0 t/s 66.2 t/s 75.2 t/s +14% 双流并发 聚合解码 76.9 t/s 166.7 t/s 293.4 t/s +76% 128K 长上下文 解码 33.8 t/s 39.0 t/s 65.3 t/s +67% 预填充 @119K 1207 tok/s 1166 tok/s 1203 tok/s 持平 接受长度(短/128K) — 3.76 / 2.13 4.74 / 2.91 — KV 池 token 331173 280537 137709(单路)<br>113997(双路) 0.4× 草稿显存 — 0.79 GB 3.69 GB — 一句话:DFlash2 用 2.9GB 额外显存 + KV 池缩到 40%,换来 1.5~1.8 倍速度。 值不值,取决于你要的是吞吐还是长上下文。
二、为什么 128K 场景差距反而更大
这是这次实测最有价值的发现。投机解码的收益 = 接受长度。而 MTP 的接受长度会随上下文长度剧烈衰减,DFlash2 基本不衰减:
上下文 MTP 接受长度 DFlash2 接受长度 短(1.6K~13K) 3.76 4.74~5.33 中(32K) — 4.92 128K 1.68 ~ 2.13 2.91 短上下文 MTP 还能靠 3.76 的接受长度拿到 89 t/s;到了 128K 接受长度掉到 2 附近,投机验证的开销就快把收益吃光了,只剩 39 t/s(比无投机的 33.8 只快 15%)。DFlash2 在 128K 仍有 2.91 的接受长度,于是拿到 65.3 t/s。
原理上说得通:MTP 是"目标模型自带的单层草稿头",它只见过目标模型的最后一层隐状态,长上下文里隐状态的分布漂移会直接削弱它的预测;DFlash2 是独立的 1.92B 草稿模型,用块扩散(block diffusion)一次并行预测一整块 8 个位置的 token 并保留每位的候选,再用一个轻量 selector 串成一条连贯路径,还从目标模型的第 5/19/33/47/61 层抽取隐状态做条件——信息量大得多,所以长上下文下更稳。
三、环境:0.5.17 → 0.5.19 的隔离升级
DFlash2 在 SGLang 0.5.17 里根本不存在(
grep -i dflash2在整个srt/零命中,只有 DFLASH v1 的 worker)。0.5.19(2026-09-04 发布)包含了官方两个关键 PR:- #35371
[Spec] DFlash2: local convolution + candidate selector(2026-08-19 合并) - #35496
Support quantized target lm_head in the DFlash2 selector(2026-08-20 合并)
第二个 PR 值得单独说一句:DFlash2 的 selector 要拿草稿隐状态直接和目标模型的
lm_head.weight做 matmul,量化目标模型会报DFlash2 selector requires a dense FP16/BF16/FP32 target lm_head。32G 卡上跑 27B 只能用量化模型,所以这个 PR 是刚需。不过实测发现我们的目标模型不需要它:RedHatAI INT4 的
lm_head.weight是稠密 BF16(只有 Linear 层走 compressed-tensors pack-quantized,lm_head在 ignore 列表里)。也就是说 0.5.18/0.5.19 都能用。升级做法是克隆 venv,不动原来那套,0.5.17 的双路 128K 配置完整保留当退路:
rsync -a /home/shenzq/sglang-env/ /home/shenzq/sglang019-env/ sed -i 's#/home/shenzq/sglang-env#/home/shenzq/sglang019-env#g' /home/shenzq/sglang019-env/bin/* /home/shenzq/sglang019-env/bin/python -m pip install -U "sglang==0.5.19" -i https://pypi.tuna.tsinghua.edu.cn/simple升级幅度不小:torch 2.11.0 → 2.13.0+cu130、flashinfer 0.6.15 → 0.6.18、sglang-kernel 0.4.5 → 0.4.6.post1、triton 3.7.1。
启动(草稿模型 3.85GB,走 hf-mirror 下载):
export HF_ENDPOINT=https://hf-mirror.com export HF_HUB_DISABLE_XET=1 python -m sglang.launch_server \ --model-path /mnt/sda6/download/qwen38-redhat-int4 \ --speculative-algorithm DFLASH \ --speculative-draft-model-path incoai/Qwen3.8-27B-DFlash2 \ --speculative-num-draft-tokens 8 \ --context-length 131072 --mem-fraction-static 0.92 \ --max-mamba-cache-size 5 --mamba-ssm-dtype bfloat16 \ --cuda-graph-backend-prefill=disabled \ --kv-cache-dtype fp8_e4m3 --page-size 1 --language-only四、显存账:为什么 DFlash2 吃掉了长上下文
32G 卡上的固定开销(实测,
--language-only):项目 大小 目标模型 INT4 权重 17.65 GB DFlash2 草稿模型(1.92B bf16) 3.69 GB mamba 状态(8 槽 fp32) 3.54 GB 融合 KV cell 42 KiB/token(目标 32 + 草稿 10) 关键在那个 42 KiB/token:DFlash2 的草稿 KV 是"融合"进同一个池子的(日志
DFLASH fused KV materialization enabled),所以每 token 的池子开销比无投机多 31%。草稿 KV 本身已经是 fp8(10 KiB/token,改不了),省不下来。于是 128K 单路要 131072 × 42 KiB = 5.5GB KV,只能靠三件事挤出来:
--mem-fraction-static 0.88 → 0.92(+1.3GB)--mamba-ssm-dtype bfloat16(ssm 状态减半,省 1.75GB)--max-mamba-cache-size 8 → 5(再省 0.67GB)
挤完的结果:池 137709 token(单路)/ 113997(双路)。对比无投机的 331173、MTP 的 280537。
所以双路 128K 在 DFlash2 下是不可能的:那需要 262144 × 42 KiB = 11GB KV,而 32G 卡上扣掉权重和 mamba 只剩 6GB 左右。要双路 128K,只能用 0.5.17 + 无投机(池 279214)那条路。
五、无损性验证:7/8 逐字一致
DFlash2 官方声称"解码无损,greedy 输出与目标模型完全一致"。实测 8 个提示词(贪心解码,
temperature=0),与同版本无投机输出逐字比对:提示词 结果 常识 / 数学 / 中文创作 / 列表 / 逻辑 / 摘要 / 中文知识问答
7 项逐字相同代码生成
️ 语义相同,单点 token 不同(280 vs 282 字符)唯一差异出现在代码提示的这里:
DFlash2:This will output: 55 无投机:This will output `55`.两句话语义等价,后续文本重新汇合——这是 fp8 KV cache + 投机验证改变了浮点运算顺序导致的近等分 token 翻转,不是模型能力差异。所以准确说法是**"近似无损"**:达不到位级完全一致,但语义一致。
六、踩过的四个坑
坑 1:0.5.17 没有 DFlash2。 别指望加个参数就能用,必须升级(且 0.5.18 起才是 torch 2.13 那套依赖)。
坑 2:
--mem-fraction-static太激进会在图捕获时崩,而且报错极具误导性。 mf 0.93 时 KV 池能给到 145490 token,但捕获 prefill 图前只剩 1.74GB,捕获中途 OOM,报的是:RuntimeError: num_active_captures_ > 0 INTERNAL ASSERT FAILED at CUDACachingAllocator.cpp:3211 markCaptureEnd called with no captures in progress看起来像 PyTorch bug,其实是显存不够。解法是
--cuda-graph-backend-prefill=disabled把显存全让给 KV 池——副作用很小:prefill 用 eager 跑,实测预填充速度和开图时几乎一致(1203 vs 无投机的 1207 tok/s),而且启动时间从 354s 降到 22s(省掉了 170s 的 prefill 图捕获 + 96s 的草稿 verify 图捕获)。坑 3:0.5.19 里 mamba 槽位耗尽的断言还在,而且代码重构了。 0.5.17 那个补丁(槽位耗尽时优雅跳过前缀捐赠而不是崩服务)不能直接搬:0.5.19 里
self.cache.evict()改名成evict_for_alloc(),prepare_for_caching_req()新增了 int8 checkpoint 分支,捐赠调用点从 2 处变成 3 处,每处都要挡None。改完确认 0.5.19 里effective_cache_len <= 0的清理分支仍在,语义与 0.5.17 一致。坑 4:mamba 槽位会把并发数压回 1。 日志直说了:
max_running_requests is capped to 1 by the mamba state cache (max_mamba_cache_size=8, 5 state slots per request)投机下每个请求要 5 个 mamba 状态槽,8 槽只能跑 1 个请求。要双路就必须
--max-mamba-cache-size 10(但又要多吃 0.7GB 显存,池子进一步缩水)——投机、并发、上下文长度,在 32G 卡上三者只能取其二。七、五方总表(含 llama.cpp)
把之前 llama.cpp 两档和 SGLang 三档放在一起(SGLang 数据为本次 0.5.19 同版本实测):
项目 llama.cpp<br>Q6_K+MTP llama.cpp<br>unsloth Q4_K_M+MTP SGLang INT4<br>无投机 SGLang INT4<br>+NEXTN(MTP) SGLang INT4<br>+DFlash2 权重 20.89 GB 15.33 GB 17.7 GB 17.7 GB 17.7 GB + 3.7 GB 草稿 代码 解码 80.9 t/s 101.2 t/s 41.8 t/s 89.3 t/s 130.5 t/s 抽取 解码 81.8 t/s 91.1 t/s 40.6 t/s 86.5 t/s 143.0 t/s 创作 解码 52.9 t/s 59.7 t/s 42.0 t/s 66.2 t/s 75.2 t/s 双路聚合 95-160 t/s 161.4 t/s 76.9 t/s 166.7 t/s 293.4 t/s 128K 长上下文解码 未测 未测 33.8 t/s 39.0 t/s 65.3 t/s 可用上下文 128K × 2 256K × 2 128K × 2 128K 单路 128K 单路 KV 池 token — — 331173 280537 137709 显存 28.4 GB 含视觉 27.6 GB 30.1 GB 30.2 GB 30.2 GB 预填充 未测 未测 1207 tok/s 1166 tok/s 1203 tok/s 八、怎么选
- 要长上下文 + 并发(双路 128K、Agent 长文档)→ SGLang 无投机,池 279214/331173,唯一能做到双路 128K 的方案。
- 要单路极致速度(编码、批量生成、短上下文对话)→ DFlash2,比 MTP 快 46~76%,128K 长上下文快 67%。
- 要长上下文 + 一点加速(单路 128K 但想省点时间)→ MTP,池 280537 是 DFlash2 的 2 倍,128K 解码 39 t/s。
- 要超长上下文 × 并发(256K × 2)→ 只有 llama.cpp unsloth Q4_K_M 那条路,代价是速度从 130 t/s 掉到 101 t/s。
- 显存最省 → llama.cpp Q4_K_M(15.33GB 权重)。
最后提醒一句:DFlash2 的价值集中在长上下文。如果你的用法全是短提示(<8K),MTP 的 89 t/s 和 DFlash2 的 130 t/s 是一倍多的差距,但两者的显存代价差别(0.79GB vs 3.69GB)会决定你能不能同时留下双路和 128K。
九、复现清单
# 1) 隔离升级到 0.5.19(保留 0.5.17 环境) rsync -a /home/shenzq/sglang-env/ /home/shenzq/sglang019-env/ /home/shenzq/sglang019-env/bin/python -m pip install -U "sglang==0.5.19" -i https://pypi.tuna.tsinghua.edu.cn/simple # 2) 打 mamba 槽位优雅降级补丁(0.5.19 代码重构过,需专用版) /home/shenzq/sglang019-env/bin/python fix_mamba_graceful_019.py # 3) 下载草稿模型(镜像站) export HF_ENDPOINT=https://hf-mirror.com HF_HUB_DISABLE_XET=1 huggingface-cli download incoai/Qwen3.8-27B-DFlash2 --local-dir /mnt/sda6/download/qwen38-dflash2 # 4) 启动(见第二节命令) bash start_dflash2.sh dflash-128k # ctx 131072 单路,池 137709 bash start_dflash2.sh dflash-128k-dual # ctx 131072 双路,池 113997 bash start_dflash2.sh nospec-128k # 无投机对照,池 331173测试脚本:
bench_sglang.py(短上下文/双流)、bench_dflash_long.py(预填充曲线 + 128K 解码 + 双路长上下文)、lossless_check.py+compare_lossless.py(无损性比对)。硬件:RTX 4080 SUPER 32G(32760 MiB)、驱动 595.84、CUDA 13.0、Ubuntu。目标模型 RedHatAI Qwen3.8-27B INT4(compressed-tensors W4A16 group128,Marlin 核),KV cache fp8_e4m3,
--page-size 1。 - #35371
-
之前发的关于SGLang调试经历的帖子,大家翻阅参考:
https://lcz.me/topic/1621
https://lcz.me/topic/1620
上一篇把 Qwen3.8-27B 在 RTX 4080 SUPER 32G 上用 SGLang 跑通并修好了 MTP 的 KV 池崩塌问题。这一篇回答一个更实际的问题:同样是投机解码,MTP 和 DFlash2 到底选哪个?整机配置
CPU:AMD Ryzen 7 9700X(8核16线程)
内存:64GB DDR5
显卡:NVIDIA RTX 4080 SUPER 32GB(32760 MiB)
系统:Ubuntu(SGLang 这套)/Windows 11(上一篇 llama.cpp 那套)结论先行:短上下文和长上下文,DFlash2 全面胜出(短上下文快 1.5~1.8 倍,128K 长上下文快 1.67 倍);代价是显存——DFlash2 的草稿模型要占 3.69GB 显存,KV 池从 33 万 token 掉到 11~14 万,双路 128K 从此不可能。
三条路全部实测,同一台机器、同一个目标模型、同一批提示词。
一、结论表(同版本 0.5.19,RedHatAI INT4 目标模型)
场景 无投机 NEXTN (MTP) DFlash2 DFlash2 vs MTP 短上下文 代码 解码 41.8 t/s 89.3 t/s 130.5 t/s +46% 短上下文 抽取原文 解码 40.6 t/s 86.5 t/s 143.0 t/s +65% 短上下文 中文创作 解码 42.0 t/s 66.2 t/s 75.2 t/s +14% 双流并发 聚合解码 76.9 t/s 166.7 t/s 293.4 t/s +76% 128K 长上下文 解码 33.8 t/s 39.0 t/s 65.3 t/s +67% 预填充 @119K 1207 tok/s 1166 tok/s 1203 tok/s 持平 接受长度(短/128K) — 3.76 / 2.13 4.74 / 2.91 — KV 池 token 331173 280537 137709(单路)<br>113997(双路) 0.4× 草稿显存 — 0.79 GB 3.69 GB — 一句话:DFlash2 用 2.9GB 额外显存 + KV 池缩到 40%,换来 1.5~1.8 倍速度。 值不值,取决于你要的是吞吐还是长上下文。
二、为什么 128K 场景差距反而更大
这是这次实测最有价值的发现。投机解码的收益 = 接受长度。而 MTP 的接受长度会随上下文长度剧烈衰减,DFlash2 基本不衰减:
上下文 MTP 接受长度 DFlash2 接受长度 短(1.6K~13K) 3.76 4.74~5.33 中(32K) — 4.92 128K 1.68 ~ 2.13 2.91 短上下文 MTP 还能靠 3.76 的接受长度拿到 89 t/s;到了 128K 接受长度掉到 2 附近,投机验证的开销就快把收益吃光了,只剩 39 t/s(比无投机的 33.8 只快 15%)。DFlash2 在 128K 仍有 2.91 的接受长度,于是拿到 65.3 t/s。
原理上说得通:MTP 是"目标模型自带的单层草稿头",它只见过目标模型的最后一层隐状态,长上下文里隐状态的分布漂移会直接削弱它的预测;DFlash2 是独立的 1.92B 草稿模型,用块扩散(block diffusion)一次并行预测一整块 8 个位置的 token 并保留每位的候选,再用一个轻量 selector 串成一条连贯路径,还从目标模型的第 5/19/33/47/61 层抽取隐状态做条件——信息量大得多,所以长上下文下更稳。
三、环境:0.5.17 → 0.5.19 的隔离升级
DFlash2 在 SGLang 0.5.17 里根本不存在(
grep -i dflash2在整个srt/零命中,只有 DFLASH v1 的 worker)。0.5.19(2026-09-04 发布)包含了官方两个关键 PR:- #35371
[Spec] DFlash2: local convolution + candidate selector(2026-08-19 合并) - #35496
Support quantized target lm_head in the DFlash2 selector(2026-08-20 合并)
第二个 PR 值得单独说一句:DFlash2 的 selector 要拿草稿隐状态直接和目标模型的
lm_head.weight做 matmul,量化目标模型会报DFlash2 selector requires a dense FP16/BF16/FP32 target lm_head。32G 卡上跑 27B 只能用量化模型,所以这个 PR 是刚需。不过实测发现我们的目标模型不需要它:RedHatAI INT4 的
lm_head.weight是稠密 BF16(只有 Linear 层走 compressed-tensors pack-quantized,lm_head在 ignore 列表里)。也就是说 0.5.18/0.5.19 都能用。升级做法是克隆 venv,不动原来那套,0.5.17 的双路 128K 配置完整保留当退路:
rsync -a /home/shenzq/sglang-env/ /home/shenzq/sglang019-env/ sed -i 's#/home/shenzq/sglang-env#/home/shenzq/sglang019-env#g' /home/shenzq/sglang019-env/bin/* /home/shenzq/sglang019-env/bin/python -m pip install -U "sglang==0.5.19" -i https://pypi.tuna.tsinghua.edu.cn/simple升级幅度不小:torch 2.11.0 → 2.13.0+cu130、flashinfer 0.6.15 → 0.6.18、sglang-kernel 0.4.5 → 0.4.6.post1、triton 3.7.1。
启动(草稿模型 3.85GB,走 hf-mirror 下载):
export HF_ENDPOINT=https://hf-mirror.com export HF_HUB_DISABLE_XET=1 python -m sglang.launch_server \ --model-path /mnt/sda6/download/qwen38-redhat-int4 \ --speculative-algorithm DFLASH \ --speculative-draft-model-path incoai/Qwen3.8-27B-DFlash2 \ --speculative-num-draft-tokens 8 \ --context-length 131072 --mem-fraction-static 0.92 \ --max-mamba-cache-size 5 --mamba-ssm-dtype bfloat16 \ --cuda-graph-backend-prefill=disabled \ --kv-cache-dtype fp8_e4m3 --page-size 1 --language-only四、显存账:为什么 DFlash2 吃掉了长上下文
32G 卡上的固定开销(实测,
--language-only):项目 大小 目标模型 INT4 权重 17.65 GB DFlash2 草稿模型(1.92B bf16) 3.69 GB mamba 状态(8 槽 fp32) 3.54 GB 融合 KV cell 42 KiB/token(目标 32 + 草稿 10) 关键在那个 42 KiB/token:DFlash2 的草稿 KV 是"融合"进同一个池子的(日志
DFLASH fused KV materialization enabled),所以每 token 的池子开销比无投机多 31%。草稿 KV 本身已经是 fp8(10 KiB/token,改不了),省不下来。于是 128K 单路要 131072 × 42 KiB = 5.5GB KV,只能靠三件事挤出来:
--mem-fraction-static 0.88 → 0.92(+1.3GB)--mamba-ssm-dtype bfloat16(ssm 状态减半,省 1.75GB)--max-mamba-cache-size 8 → 5(再省 0.67GB)
挤完的结果:池 137709 token(单路)/ 113997(双路)。对比无投机的 331173、MTP 的 280537。
所以双路 128K 在 DFlash2 下是不可能的:那需要 262144 × 42 KiB = 11GB KV,而 32G 卡上扣掉权重和 mamba 只剩 6GB 左右。要双路 128K,只能用 0.5.17 + 无投机(池 279214)那条路。
五、无损性验证:7/8 逐字一致
DFlash2 官方声称"解码无损,greedy 输出与目标模型完全一致"。实测 8 个提示词(贪心解码,
temperature=0),与同版本无投机输出逐字比对:提示词 结果 常识 / 数学 / 中文创作 / 列表 / 逻辑 / 摘要 / 中文知识问答
7 项逐字相同代码生成
️ 语义相同,单点 token 不同(280 vs 282 字符)唯一差异出现在代码提示的这里:
DFlash2:This will output: 55 无投机:This will output `55`.两句话语义等价,后续文本重新汇合——这是 fp8 KV cache + 投机验证改变了浮点运算顺序导致的近等分 token 翻转,不是模型能力差异。所以准确说法是**"近似无损"**:达不到位级完全一致,但语义一致。
六、踩过的四个坑
坑 1:0.5.17 没有 DFlash2。 别指望加个参数就能用,必须升级(且 0.5.18 起才是 torch 2.13 那套依赖)。
坑 2:
--mem-fraction-static太激进会在图捕获时崩,而且报错极具误导性。 mf 0.93 时 KV 池能给到 145490 token,但捕获 prefill 图前只剩 1.74GB,捕获中途 OOM,报的是:RuntimeError: num_active_captures_ > 0 INTERNAL ASSERT FAILED at CUDACachingAllocator.cpp:3211 markCaptureEnd called with no captures in progress看起来像 PyTorch bug,其实是显存不够。解法是
--cuda-graph-backend-prefill=disabled把显存全让给 KV 池——副作用很小:prefill 用 eager 跑,实测预填充速度和开图时几乎一致(1203 vs 无投机的 1207 tok/s),而且启动时间从 354s 降到 22s(省掉了 170s 的 prefill 图捕获 + 96s 的草稿 verify 图捕获)。坑 3:0.5.19 里 mamba 槽位耗尽的断言还在,而且代码重构了。 0.5.17 那个补丁(槽位耗尽时优雅跳过前缀捐赠而不是崩服务)不能直接搬:0.5.19 里
self.cache.evict()改名成evict_for_alloc(),prepare_for_caching_req()新增了 int8 checkpoint 分支,捐赠调用点从 2 处变成 3 处,每处都要挡None。改完确认 0.5.19 里effective_cache_len <= 0的清理分支仍在,语义与 0.5.17 一致。坑 4:mamba 槽位会把并发数压回 1。 日志直说了:
max_running_requests is capped to 1 by the mamba state cache (max_mamba_cache_size=8, 5 state slots per request)投机下每个请求要 5 个 mamba 状态槽,8 槽只能跑 1 个请求。要双路就必须
--max-mamba-cache-size 10(但又要多吃 0.7GB 显存,池子进一步缩水)——投机、并发、上下文长度,在 32G 卡上三者只能取其二。七、五方总表(含 llama.cpp)
把之前 llama.cpp 两档和 SGLang 三档放在一起(SGLang 数据为本次 0.5.19 同版本实测):
项目 llama.cpp<br>Q6_K+MTP llama.cpp<br>unsloth Q4_K_M+MTP SGLang INT4<br>无投机 SGLang INT4<br>+NEXTN(MTP) SGLang INT4<br>+DFlash2 权重 20.89 GB 15.33 GB 17.7 GB 17.7 GB 17.7 GB + 3.7 GB 草稿 代码 解码 80.9 t/s 101.2 t/s 41.8 t/s 89.3 t/s 130.5 t/s 抽取 解码 81.8 t/s 91.1 t/s 40.6 t/s 86.5 t/s 143.0 t/s 创作 解码 52.9 t/s 59.7 t/s 42.0 t/s 66.2 t/s 75.2 t/s 双路聚合 95-160 t/s 161.4 t/s 76.9 t/s 166.7 t/s 293.4 t/s 128K 长上下文解码 未测 未测 33.8 t/s 39.0 t/s 65.3 t/s 可用上下文 128K × 2 256K × 2 128K × 2 128K 单路 128K 单路 KV 池 token — — 331173 280537 137709 显存 28.4 GB 含视觉 27.6 GB 30.1 GB 30.2 GB 30.2 GB 预填充 未测 未测 1207 tok/s 1166 tok/s 1203 tok/s 八、怎么选
- 要长上下文 + 并发(双路 128K、Agent 长文档)→ SGLang 无投机,池 279214/331173,唯一能做到双路 128K 的方案。
- 要单路极致速度(编码、批量生成、短上下文对话)→ DFlash2,比 MTP 快 46~76%,128K 长上下文快 67%。
- 要长上下文 + 一点加速(单路 128K 但想省点时间)→ MTP,池 280537 是 DFlash2 的 2 倍,128K 解码 39 t/s。
- 要超长上下文 × 并发(256K × 2)→ 只有 llama.cpp unsloth Q4_K_M 那条路,代价是速度从 130 t/s 掉到 101 t/s。
- 显存最省 → llama.cpp Q4_K_M(15.33GB 权重)。
最后提醒一句:DFlash2 的价值集中在长上下文。如果你的用法全是短提示(<8K),MTP 的 89 t/s 和 DFlash2 的 130 t/s 是一倍多的差距,但两者的显存代价差别(0.79GB vs 3.69GB)会决定你能不能同时留下双路和 128K。
九、复现清单
# 1) 隔离升级到 0.5.19(保留 0.5.17 环境) rsync -a /home/shenzq/sglang-env/ /home/shenzq/sglang019-env/ /home/shenzq/sglang019-env/bin/python -m pip install -U "sglang==0.5.19" -i https://pypi.tuna.tsinghua.edu.cn/simple # 2) 打 mamba 槽位优雅降级补丁(0.5.19 代码重构过,需专用版) /home/shenzq/sglang019-env/bin/python fix_mamba_graceful_019.py # 3) 下载草稿模型(镜像站) export HF_ENDPOINT=https://hf-mirror.com HF_HUB_DISABLE_XET=1 huggingface-cli download incoai/Qwen3.8-27B-DFlash2 --local-dir /mnt/sda6/download/qwen38-dflash2 # 4) 启动(见第二节命令) bash start_dflash2.sh dflash-128k # ctx 131072 单路,池 137709 bash start_dflash2.sh dflash-128k-dual # ctx 131072 双路,池 113997 bash start_dflash2.sh nospec-128k # 无投机对照,池 331173测试脚本:
bench_sglang.py(短上下文/双流)、bench_dflash_long.py(预填充曲线 + 128K 解码 + 双路长上下文)、lossless_check.py+compare_lossless.py(无损性比对)。硬件:RTX 4080 SUPER 32G(32760 MiB)、驱动 595.84、CUDA 13.0、Ubuntu。目标模型 RedHatAI Qwen3.8-27B INT4(compressed-tensors W4A16 group128,Marlin 核),KV cache fp8_e4m3,
--page-size 1。数据很完整,补两点工程上的取舍。
1)DFlash2 的收益本质是拿显存换 acceptance。draft 模型 3.69GB 是固定成本,在 32G 卡上直接吃掉 KV 池的一半多,这比"快 46%"更致命——它把并发和长上下文两条路都收窄了。表格里双流聚合 293 t/s 看着漂亮,但如果负载真需要双路 128K,DFlash2 直接不可用,只能回 MTP 或不开投机。选型顺序应该是先定"要几条并发、多长上下文",再决定 draft。
2)中文创作那档 DFlash2 只比 MTP 快 14%,明显低于代码/抽取的 46–65%。这通常指向采样温度一高、draft 命中率就掉,创作类 prompt 的接受长度吃亏。可以补一条 acceptance length vs temperature 的曲线,比单点 t/s 更能说明 DFlash2 的适用边界。
另外问一句:你这套有没有开 FP8 KV(或 KV 量化)?如果开,KV 池能补回来多少、对 DFlash2 的显存占用有没有影响?
- #35371
-
接受率崩到很低,一般不是量化误差本身——nvfp4 的 logits 误差只会让接受率小幅下滑。更像 draft 与 target 的接线或结构没对齐,按这个顺序查:
- draft 的 vocab、hidden、RoPE 是否和 nvfp4 checkpoint 完全一致;有些 nvfp4 版本会动 MLP 结构,draft 按原结构训的就会错位。
- lm_head 有没有被接到量化后的头上,draft 应该对 target 的原始 logits 做验证。
- 别看 t/s,直接看日志里的 acceptance length,先用 BF16 target 做基准 A/B,再换 flyer666 那版 W4A16 drafter 对比。
BF16 能跑通就先拿它当对照组,nvfp4 掉多少一目了然,也方便判断是量化还是接线。
-
没有非常低, 但是全面落后 NEXTN:
A/B 结果
同一份 sglang bench 脚本,唯一变量 = 投机方案
输入 tok decode NEXTN decode DFlash2 变化 prefill NEXTN prefill DFlash2 变化 accept NEXTN accept DFlash2 333 111.0 100.7 −9.3% 5,017 6,800* +36%* 2.60 2.38 2,268 94.1 90.5 −3.8% 8,583 8,600 +0.2% 2.60 2.46 8,827 63.9 67.8 +6.0% 8,216 8,397 +2.2% 2.59 2.44 17,583 45.6 43.4 −4.6% 7,083 7,286 +2.9% 2.59 2.38 均值 78.6 75.2 −4.4% — — — 2.59 2.42 -
没有非常低, 但是全面落后 NEXTN:
A/B 结果
同一份 sglang bench 脚本,唯一变量 = 投机方案
输入 tok decode NEXTN decode DFlash2 变化 prefill NEXTN prefill DFlash2 变化 accept NEXTN accept DFlash2 333 111.0 100.7 −9.3% 5,017 6,800* +36%* 2.60 2.38 2,268 94.1 90.5 −3.8% 8,583 8,600 +0.2% 2.60 2.46 8,827 63.9 67.8 +6.0% 8,216 8,397 +2.2% 2.59 2.44 17,583 45.6 43.4 −4.6% 7,083 7,286 +2.9% 2.59 2.38 均值 78.6 75.2 −4.4% — — — 2.59 2.42 -
找到原因了, 测试方法不一样, 下面是我让AI给我的一个简要总结:
一句话结论
不是环境差异,是测的任务类型不同。
接受长度几乎完全由任务类型决定。
同一台机器、同一个目标模型、同一个 DFlash2 草稿,只换 prompt 类型:
任务类型 我们的 accept 速度 强制续写散文(我们最初一直用的) 2.4 45–93 t/s 摘要 2.86 92.5 t/s 中文创作 3.05 66.5 t/s 代码 4.99 164.6 t/s 抽取原文(照抄) 5.94 211.4 t/s 为什么会产生“他高我们低”的错觉
他的 4.74~5.33 是把整组任务合成的单一数字,而组里包含**“抽取原文”和“代码”**这两类近乎照抄的任务——它们天然把接受度拉到 5~6。
他表里三个场景的速度是分开报的,接受长度却只给一个合计值。这个合计值被高接受类主导了。
我们最初使用的:
散文续写 +
ignore_eos强制截断 256 token是最难的一类——模型被逼着无目标地续写,接受的 token 最少。
因此,我们实际上是拿最难的类别去比较他的合计值,自然会出现接受长度相差接近一倍的现象。
换成他的任务类型后,我们反而比他快:
- 抽取原文:211 vs 143 t/s
- 代码:165 vs 130 t/s
顺带被否证的三个猜想
-
量化头(NVFP4 打包头)
- 单变量换 BF16 稠密头
- accept:2.50 → 2.56(+2%)
- 但速度反而慢 10%~18%
-
INT4 vs NVFP4
- 下载同名 INT4 checkpoint 实测
- accept:2.51 vs 2.50
- 几乎零影响
- 而且 TTFT 翻倍
-
草稿选型(DFlash2 / DSpark / NEXTN)
- 同一任务类型下差距仅约 ±7%
