针对双GH200优化的DSv4-Flash:SGLang上PP达10,000 tok/s,TG超过300 tok/s
摘要
DeepSeek V4 Flash在双GH200工作站上的详细基准测试,比较了SGLang和vLLM在1M上下文长度下使用DSpark投机解码的表现,发现SGLang更快,约为317 tok/s对比276 tok/s。
暂无内容
查看缓存全文
缓存时间: 2026/08/04 12:06
# 用于 LLM 推理的双 GH200,第 4 部分:DeepSeek V4 Flash —— 1M 上下文下的 SGLang vs vLLM
## 引言
第 1 部分(https://dnhkng.github.io/posts/gh200-benchmarking/)将这台双 GH200 工作站作为内存系统进行了测量。第 2 部分(https://dnhkng.github.io/posts/gh200-benchmarking-part-2/)将这些数字用于**预览版** DeepSeek V4 Flash 检查点,其中有趣/痛苦的部分是如何让多 token 预测跑起来:手写的 vLLM O 投影回退、一个配置传播修复,以及一个狭窄的上游 PR。第 3 部分(https://dnhkng.github.io/posts/gh200-benchmarking-part-3-glm52/)则通过 GLM-5.2 和专家卸载将这台机器推到了极限。
系列文章:
1. LLM 推理的内存路径(https://dnhkng.github.io/posts/gh200-benchmarking/)
2. vLLM、DeepSeek V4 Flash/Pro 与 MTP(https://dnhkng.github.io/posts/gh200-benchmarking-part-2/)
3. GLM-5.2、专家卸载与 CPU 之问(https://dnhkng.github.io/posts/gh200-benchmarking-part-3-glm52/)
4. DeepSeek V4 Flash 正式发布、DSpark,以及 SGLang vs vLLM
这是我想在第 2 部分之后做的后续——那时测的是*预览版*。DeepSeek 现在已发布 GA 版本(*正式发布版*),即**DeepSeek-V4-Flash-0731**。投机解码路径不再是手工 MTP 移植:该版本自带**DSpark**,DeepSeek 的半自回归投机解码模块,作为 vLLM 的一等解码方法。它更快,并且可以服务**完整的 1,048,576 token 上下文,无需 CPU 卸载**。在把 vLLM 调到极限之后,我在同一台机器上引入了**SGLang**,看看 ~276 tok/s 到底是 DeepSeek 的天花板还是 vLLM 的天花板。在修复了一个静默降低草稿模型性能的加载器 bug,并得到一些上游帮助之后,SGLang 在我测试的每个解码工作负载上都更快。
***TL;DR:***
> 在这台双 GH200 机器上,你可以从源码构建**vLLM** `v0.26.0`,合入已合并的 DSV4 缓存布局补丁(PR \#48993),禁用异步调度,并以 6 个预测 token 运行 DSpark,从而获得:*~276 解码 tok/s*,以及 192 GB HBM 中的 1M 上下文。**SGLang** 一旦能在 ARM64 上构建、并修复其 DSpark 加载器 bug,在每个解码工作负载上都更快,可达 *~317.0 tok/s*。
## 系统提醒
与系列其余部分相同的机器,所以简短说明:
| 组件 | 规格 |
|---|---|
| GPU | 2x GH200 Hopper,每颗 96 GB HBM3 |
| CPU | 2x Grace,每颗 72 核 |
| 主机内存 | 每个 Grace 480 GB LPDDR5X,共 960 GB |
| GPU 本地内存 | 共 192 GB HBM |
| CUDA | 13.0 |
| PyTorch | `2.11.0+cu130` |
| OS | Ubuntu 24.04,aarch64 |
来自第 1 部分(https://dnhkng.github.io/posts/gh200-benchmarking/)的拓扑事实仍然支配一切:~3.7 TB/s 本地 HBM、~377 GB/s 本地 Grace-to-Hopper,而分段的 Hopper-to-Hopper 路径只有 ~58 GB/s。下面的每个部署决策都归结为同一条规则:让热数据留在本地,避免跨 GPU 路径。
## 第 2 部分以来发生了什么变化
两件事:
- **DeepSeek 检查点发布了,并广受好评。** `DeepSeek-V4-Flash-0731`,按 vLLM 的报告,磁盘上 155.43 GiB。其 `compress_ratios` 元数据包含 5 个未压缩条目、21 个 C4 条目和 20 个 C128 条目。主模型使用前 43 个;其余是草稿/投机层。
- **MTP 升级为 DSpark。** 在第 2 部分中,多 token 预测必须打进 vLLM 才能运行。发布的检查点自带 DSpark(https://huggingface.co/deepseek-ai/DeepSeek-V4-Flash-DSpark),即 DeepSeek 的*基于置信度调度的半自回归投机解码*(公开实现见 DeepSpec(https://github.com/deepseek-ai/DeepSpec))。操作上,它取代了预览版的 MTP 配置:你传入 `{"method":"dspark","num_speculative_tokens":6}`,vLLM 会将其路由到部分相同的草稿模型和 `mtp.*` 管道(它会将草稿模型转换为 `DSparkDraftModel`,而部分存储权重仍保留 MTP 名称)。这是一种独立的解码方法,不是改名后的 MTP 模式,因此第 2 部分的手工接线被一个单独的标志取代。
> 注意:发布的模型具有 **5 的 DSpark 块大小**,这使得任何 `num_speculative_tokens < 5` 都无效。
## 在此系统上构建 vLLM v0.26.0 需要打补丁
我从确切的 `v0.26.0` 标签(commit `568afb3a1`)、CUDA 13.0、针对 Hopper 的 `TORCH_CUDA_ARCH_LIST=9.0a` 构建。稳定标签缺少的东西是一个**在发布*之后*才合并的 DeepSeek V4 KV 缓存修复**:vllm-project/vllm\#48993(https://github.com/vllm-project/vllm/pull/48993),*"Compact MXFP4 indexer KV cache and packed group overlays."*
没有它,v0.26.0 使用旧的填充式缓存组布局,会在 DSV4 的异构压缩缓存和压缩器状态组之间浪费内存。有了它,布局变得紧凑:
1. MXFP4 索引器行的大小为 68 字节,而不是保留一个 132 字节的 FP8 行;
2. 每个 DSV4 缓存组在逐块 slab 中紧凑布局,重叠块 ID 命名空间相互独立的组;
3. 去掉了不再需要的跨组滑动窗口页面填充。
这是已合入上游的行为(在他们的 GB200 配置上约多出 12% 的可用块),不是本地 hack;只是它在该标签之后才落地。实用规则:**使用 v0.26.0 加 PR \#48993,或任何已包含它的更新版本。**
## TorchVision 版本固定
*即使是纯文本模型*也要固定匹配的 PyTorch/TorchVision 对:vLLM 的全局内核预热无论你服务什么都会导入 MiniMax M3 处理器,因此纯文本的 DSV4 服务器在启动时因 `ModuleNotFoundError: No module named 'torchvision'` 而失败。安装*最新*版 TorchVision 后又出现 ABI 不匹配(`operator torchvision::nms does not exist`);解决办法是安装与 PyTorch 2.11 匹配的版本:
`1 uv pip install --no-deps 'torchvision==0.26.0'`
已验证的组合是 PyTorch `2.11.0+cu130` 和 TorchVision `0.26.0+cu130`。
## 避免错误的 26.33 GiB OOM 失败
我第一次无卸载启动使用了 `max_num_batched_tokens=32768`,在每颗 GPU 加载 74.85 GiB 后拒绝启动:
`1 2 3 26.33 GiB KV cache is needed 3.57 GiB KV cache memory is available estimated maximum model length is 8884`
这很奇怪:该模型公布的 1M token KV 估计约为 10 GiB。26 GiB 这个数字是真实的但具有误导性。其机制:
`1 max_in_flight_tokens = max_concurrent_batches * max_num_batched_tokens`
在 `max_num_batched_tokens=32768` 时,vLLM 悄悄启用了**异步调度**,这会将 `max_concurrent_batches` 设置为 2。于是调度器为 **65,536 个在飞 token** 保留了瞬态状态。DeepSeek V4 为其 C4 和 C128 层携带了大量 FP32 压缩器状态,这些滑动窗口状态缓存是按*在飞*限制来设定大小的,而不是按 `max_model_len`。在大的调度器批次下,这个压缩器状态预留主导了启动准入。
所以 26.33 GiB 是依赖调度器的压缩器状态加上填充加 profiling 的结果,**不是**压缩后的模型 KV。官方 vLLM DeepSeek V4 文章(https://github.com/vllm-project/vllm-project.github.io/blob/main/_posts/2026-04-24-deepseek-v4.md)将 1M token 的 BF16 DSV4 KV 缓存估计为约 9.62 GiB,而一旦瞬态状态得到控制,这台机器也同意这一点。降低 `max_model_len` 只会隐藏问题而不是修复问题,并且会把方向引向这里并不需要的 CPU 卸载。
## 单用户修复
对于单用户代码工作负载,正确的做法是停止为我没有使用的并发付费:
`1 2 3 max_num_batched_tokens=8192 max_num_seqs=1 no_async_scheduling=true`
8K 的调度器分块仍然会分四次处理 32K 的代码提示;对于一次一个请求,较低的瞬态预留比异步重叠更有价值。CUDA graph 保持**开启**:此前在这台主机上的测量显示 `--enforce-eager` 会拖垮解码,因此 eager 不是候选。
使用该配置,在 65,536 token 上下文下,无投机启动报告:
`1 2 3 Available KV cache memory: 12.67 GiB GPU KV cache size: 303,419 tokens Maximum concurrency for 65,536 tokens: 4.63x`
在将字节除以 token 与下面的 1M 数字比较之前要提醒一点:vLLM 报告的 token 容量是感知请求形状的,不是平坦的每 token 字节换算。在 65K 时,DSV4 固定的每请求压缩器状态预留主导了计算,约为 44 KB/token。在 1M 时,同样的开销分摊到长得多的上下文上,接近 ~6.5 KB/token 的长上下文边际成本。同一台机器,同样的固定状态,不同的平均值。
## DSpark 结果:k 值扫描
8K token 代码提示、代码审查生成、最多 2048 个输出 token,三次运行的中位数:
| DSpark `k` | 中位 TG | 中位 TPOT | 备注 |
|---|---|---|---|
| 0 | 92.9 tok/s | 10.76 ms | 基线,无投机 |
| 4 | n/a | n/a | **无效**,低于块大小 5 |
| 5 | 268.2 tok/s | 3.73 ms | 有效 |
| **6** | **275.9 tok/s** | **3.62 ms** | **最佳** |
| 7 | 262.2 tok/s | 3.81 ms | 有效 |
| 8 | 216.3 tok/s | 4.62 ms | 回退 |
| 10 | 223.9 tok/s | 4.47 ms | 回退 |
`k=6` 是**无投机解码速率的 2.97 倍**,且比 `k=5` 快约 3%。第 2 部分的 MTP 经验仍然成立:更深不是免费的,过了最佳点之后,额外的草稿深度不再值回票价,TG 会下降。此外还有一个硬性下限:由于检查点的 DSpark 块大小为 5,`k=4` 会被直接拒绝,而不只是变慢。
这个扫描针对的是代码审查,其草稿接受率很高。开放式的散文则不然,而后续的 SGLang 对比也使用这种工作负载,因此需要有一个 vLLM 基线。在五个固定的故事提示(温度为 0,最多 1,024 个 token,同样的五个提案)上,vLLM 的中位数是 **156.2 tok/s**(范围 151.1-159.4),远低于代码审查的数字,因为 DSpark 的接受率依赖于工作负载。这个 156.2 就是 SGLang 故事聊天行与之对比的数字。
## 预填充
未缓存的预填充(每个提示使用唯一 nonce,因此没有任何内容来自前缀缓存,见下方说明),无投机:
| 请求 token 数 | 实际提示 token 数 | 中位 TTFT | 预填充速率 |
|---|---|---|---|
| 2,048 | 2,070 | 0.291 s | 7,112 tok/s |
| 8,192 | 8,214 | 0.941 s | 8,725 tok/s |
| 32,768 | 32,789 | 3.176 s | 10,323 tok/s |
投机解码会损失一点预填充吞吐量(草稿路径增加了工作),但由于真实工作负载是长时间代码审查*生成*,解码端的收益占据主导。
> **值得重复的基准测试陷阱:**我的第一个测试框架复用了相同的提示,因此第 2 次和第 3 次运行由 vLLM 的前缀缓存服务,并报告出具有误导性的 25K-80K 预填充 tok/s。在每次运行时前置一个唯一的运行 nonce,会使第一个缓存块失效,并强制每次重复都进行真正的预填充。如果你在重复提示上基准测试预填充,你测量的是缓存复用,而不是预填充。
## 完整 1M 上下文,无卸载
第 2 部分的 V4 Pro 仅为了塞下就需要 CPU 专家卸载。发布的 Flash 不需要。在 DSpark `k=6` 下,最终服务器将完整上下文保存在 HBM 中:
`1 2 3 4 Model allocation: 79.15 GiB per GPU Available KV cache: 8.28 GiB per GPU GPU KV cache size: 1,371,214 tokens Max concurrency @ 1M: 1.31x`
8.28 GiB 分摊到 1.37M token,约为 6.5 KB/token,也就是单用户一节中的长上下文摊薄边际成本,这与 FP8 KV 和紧凑缓存布局生效后约 9.62 GiB 的 BF16 1M 估计相符。在 `k=6` 下余量很窄。在 `gpu-memory-utilization 0.95` 下差了约 300 MB:
`1 2 KV cache needed: 7.63 GiB KV cache available: 7.33 GiB`
> 将利用率提高到 `0.96` 恰好补上了这个余量。
这就是为什么发布的 Flash 比预览版更适合这台机器:它把"在慢速桥接上搬运权重"变回了"把所有东西留在本地",而这正是第 1 部分所说的这台机器想要的运行方式。
## vLLM 启动命令
通用化(换成你自己的模型路径):
`1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 python -m vllm.entrypoints.cli.main serve \\ /path/to/DeepSeek-V4-Flash-0731 \\ --served-model-name dsv4-0731 \\ --tokenizer-mode deepseek_v4 \\ --trust-remote-code \\ --distributed-executor-backend mp \\ --tensor-parallel-size 2 \\ --max-model-len 1048576 \\ --max-num-batched-tokens 8192 \\ --max-num-seqs 1 \\ --block-size 256 \\ --gpu-memory-utilization 0.96 \\ --kv-cache-dtype fp8 \\ --disable-custom-all-reduce \\ --no-async-scheduling \\ --speculative-config \\ '{"method":"dspark","num_speculative_tokens":6,"draft_sample_method":"greedy"}'`
再加上前几篇文章中针对该主机的 NCCL / FlashInfer 采样器 / TileLang / DeepGEMM / 分配器环境变量。没有 `--enforce-eager`,没有 CPU/UVA 卸载。
## 遗留问题:TileLang 延迟尖峰
vLLM 已调好,1M 上下文已验证,*而且它能工作*。唯一没有消失的问题是 TileLang 的运行时编译,它在服务模型时会造成严重破坏。当我尝试在聊天中测试系统时,延迟超过了 10 秒!mHC 内核在*服务过程中*编译,因此在 100 个请求的运行中,中位 TTFT 没问题(~0.28 秒),**但 p99 上升到约 11.5 秒**:偶尔会有一个实时请求因为 TileLang 编译一个未预热过的形状而卡住十秒以上。将 mHC 内核换成 PyTorch/Triton 回退可以将 p99 压到约 2.6 秒,*但会损失约 65% 的解码性能*,因此在 vLLM 内部这是个糟糕的选择:要么保留吞吐量并接受尾部停顿,要么移除停顿并把大部分解码加速还回去。
这个问题*大概*会在上游解决;它本质上是一个预热覆盖问题,每个版本都会在服务前编译更多形状。目前这里的 vLLM 配置是一个可用的解决方案,包括尾部尖峰等等。但我发现它作为日常驱动实在痛苦。
## SGLang,既然它能在 ARM64 上运行了
在我写第 2 和第 3 部分的时候,SGLang 在这台机器上还不是候选:它无法在 aarch64 上干净地构建,所以这个系列一直是 vLLM(以及 llama.cpp),这是出于必要而非偏好,因为 RadixAttention 非常棒,能加速"真实世界"的推理。**SGLang 现在有了真正的 ARM64 支持**,所以它可以在这台 GH200 机器上运行。我以原生方式把它跑了起来:上游 Dockerfile 现在处理 ARM,跳过缺失的 `sgl-flash-attn3` cubin,并在运行时以 JIT 方式编译它们。两颗 GPU 都以计算能力 9.0 出现。经过一些修复后,SGLang 在我测试的每个解码工作负载上都击败了调好的 vLLM 配置。
### 修复静默的共享专家加载器 bug
即使宽度匹配并选择了 Marlin,SGLang 的草稿接受率仍然很差,看起来比 vLLM 慢得多。唯一的线索是
相似文章
@Ex0byt: 更新:通往GLM-5.2之路:我们快到了,各位!未量化、未剪枝的DeepSeek-v4-Flash。单台……上11 tok/s
关于在单台DGX Spark上使用sglang推理和自定义mega-kernel以11 tok/s运行未量化的DeepSeek-v4-Flash模型的更新,正在向GLM-5.2迈进。
Deepseek V4 flash 在 DGX Spark 上的性能
一位 Reddit 用户分享了在双华硕 GX10 DGX Spark 配置上运行 DeepSeek V4 Flash 的经验,详细介绍了性能指标、配置和功耗,并提供了不同上下文长度下的吞吐量基准测试结果。
Deepseek V4 Flash 在两块 Nvidia 4090d 48G (ada) 上以 vLLM 运行,速度约 105 t/s
技术文章:详细介绍如何使用自定义Triton内核和vLLM在两块Nvidia 4090d GPU上运行DeepSeek V4 Flash,在262k上下文环境下实现约105 tokens/秒的推理速度。
@MiaAI_lab: 刚刚为您的 2 台 DGX Sparks 升级了 DeepSeek v4 Flash。单次每秒 66.6 tokens,6 个并发会话时可达 153.7 tokens/秒……
MiaAI Lab 发布了一项升级方案,用于在两台 DGX Spark 节点上使用 vLLM 结合 DSpark 推测解码和 NVFP4 KV-cache 来部署 DeepSeek V4 Flash,在六个并发会话中实现了高达 153.7 tokens/秒 的吞吐量。
DeepSeek-v4-Flash-Mini 54GB GGUF 运行速度约 20.5 t/s
一个社区构建将 DeepSeek-V4-Flash 压缩为 54GB 的 IQ2_XXS GGUF 变体,采用激进的 2 位量化,在本地硬件上实现了约 20.5 tokens/s 的速度,同时大幅降低了显存/内存占用。