本文系 trycua 团队的工程实践分享,Cua 是由该团队打造的一个面向 macOS 设计的开源 AI Agent 框架。下文采用第一视角来讲述他们在 RTX 3090 上的提速实践。

我们为 Qwen3.5-27B Q4_K_M 构建了一个独立的 C++/ggml 投机解码器(speculative decoder),并在 draft 阶段采用了 DFlash 的块扩散(block-diffusion)生成候选 token 块。

01.gif

图解:Autoregressive vs DFlash

上方的演示视频显示了 DFlash 的运行速度为 207.6 tok/s,相比自回归 Autoregressive(下面简称 AR)快 5.46 倍;在 HumanEval 10-prompt 基准测试(包含 10 个 HumanEval prompt 的基准测试)中,当 DDTree budget = 22 时,单张 24 GB 的 RTX 3090 平均速度达到了 129.5 tok/s。这比自回归 Q4_K_M 基准快了 3.43 倍,比公开的 SGLang AWQ 最佳实践数据快了 2.8 倍。

自回归 Autoregressive vs DFlash 性能对比

重点结论

  • 演示视频(上图 gif)中最高 token 生成速度达到 207 tok/s,DFlash 峰值在 207.6 tok/s 而 AR 只是 38.0 tok/s,速度提升 5.46 倍。HumanEval 10-prompt 基准测试中,DDTree budget = 22 时,DFlash 的平均 token 生成速度为 129.5 tok/s。
  • 相比 Q4_K_M 自回归基线,DFlash 的 129.5 tok/s 比 AR 的 37.78 tok/s 快 3.43 倍。
  • 相比在同一张 RTX 3090 上记录的 SGLang AWQ 参考数据(46.6 tok/s),DFlash 要快 2.8 倍。
  • 128K 上下文可容纳在 24 GB 显存中,我们采用了 Q4_0 KV 缓存 + 滚动的 4096-slot 目标特征(target feature)缓冲区的方式。HE 基准测试显示,当 ctx = 131,072 时,token 生成速度能达到 134.78 tok/s。
  • 纯 ggml 实现,并未链接 libllama。推理引擎围绕 ggml_gated_delta_net 构建,其中包含约 2,000 行 C++/CUDA 代码,编译为 libdflash27b.a。

image.png

为什么我们要做这个测试

Qwen3.5-27B 是一个混合架构模型:模型每 4 层插入一层完整的 softmax attention 层,其余层(64 层中的 48 层)是 Gated DeltaNet。它采用维度划分为 [11, 11, 10, 0] 的 M-RoPE 编码机制,24 个 Q attention head 和 4 个 KV attention head,其中 key/value head 维度为 256。在常规 KV 缓存之外,它还维护了一个 SSM 状态缓存。

这种架构组合,目前在单张 RTX 3090 上还没有很好的解码方案:

  • llama.cpp 有 GGUF 加载器和 ggml_gated_delta_net,但没有 DFlash 投机解码。
  • vLLM / SGLang 都集成了 z-lab 的 DFlash,但仅支持 BF16 权重,这需要 54 GB 显存,无法在 24 GB 的 RTX 3090 上运行。此外,两种运行时都没有针对 Qwen3.5-27B 的 GGUF 方案;截至 2026 年 4 月,SGLang 上这个路径仍处于不可用状态。SGLang 上的 AWQ 目标模型在单张 RTX 3090 上,以纯自回归方式运行时速度为 46.6 tok/s,但 24 GB 显存装不下 BF16 draft 模型 + DDTree 树状态。
  • 参考基准测试是在 NVIDIA B200 上运行的 BF16,这属于 54+ GB 显存级别的硬件环境。目前,没有公开的方案能适配 24 GB 的消费级显卡。

我们希望在 24 GB 的消费级显卡上,尽可能地获得单张 3090 的最快解码速度。最终的方案是:只将计算图胶水代码(graph glue)移植到 ggml,保留现有的 DeltaNet 内核,运行带有 DDTree 验证器的 DFlash 块扩散 draft 模型,并将 KV cache 压缩为 Q4_0,以支持长上下文。

架构设计

这个库是针对一组固定的模型对(model pair)进行了硬编码:

image 11.29.12.png

使用贪心验证(Greedy verify),DFlash draft 的块大小设为 16,仅支持 CUDA,面向单张 RTX 3090 运行。构建过程会链接到 libggml*.a,完全不链接 libllama 的任何内容。因此,即使你删除掉 deps/llama.cpp/src/ 目录,这个库仍然能编译成功。

代码结构

1280X1280 (1).PNG

Oracle(对照参考实现):PyTorch 参考实现位于 megaqwen3_27b_dflash/reference/dflash_reference.py,并与 z-lab AutoModel 的 forward 输出进行交叉验证,余弦相似度达到了 0.999812。

从自回归到 DDTree

这是在相同的 HumanEval 10-prompt 基准测试下的配置:n_gen=256,RTX 3090,目标模型 Q4_K_M,draft 模型 BF16。

700693be-f692-422e-8800-56f5f1c5152f.png

第 1–5 行是历史调优记录:commit f1cb9bf,AR 基准为 37.44 tok/s。第 6 行是 2026 年 4 月 20 日在 commit 5bb7f8c 上的最新运行结果,AR 基准为 37.78 tok/s。

image (1) 11.29.12.png

加速比是相对于同期的 AR 速度计算的:

  • AL 为平均接受长度 average accept length,即每个验证步骤接受的 token 数量。DDTree 论文提到,纯 attention 机制的 Qwen3-4B/8B/30B-MoE(A100/B200,BF16),比链式 DFlash 提升 35–42%。在这次混合 Q4_K_M/RTX 3090 的组合中,我们观察到 DDTree 比链式 DFlash 提升了 15%。我们认为这个差距源于 Q4 量化抹平了 draft 模型的 softmax 分布,我们在 build_ddtree 中通过 chain pre-seed 做了部分修复。
  • 在 f16 中间缓存下,对 budget 进行 20/30/40 的扫描,AL 稳定在 8.9 左右。budget 在 30 的 AL 为 8.86(120.49 tok/s),budget 在 40 的 AL 为 8.90(105.10 tok/s)。我们受限于 draft 精度的上限,而不是达到了验证阶段的内存瓶颈:更大的 DDTree 树也无济于事,只有更好的 draft 模型才能提升性能。

关键突破(浓缩版日志)

  • f16 中间缓存:内存宽带减半,在相同 DDTree Budget 下 tok/s 提升 5%。在 40 个 token 的测试中,使用 f16 中间缓存后的 DFlash 输出结果与自回归逐 bit 完全一致(bit-identical)。
  • 持久化写入内核(ggml_gated_delta_net_tree_persist):每步跳过耗时约 9 ms 的 ggml_cpy,所有 prompt 的性能提升了 11%。
  • D2D draft target_feat 拷贝:消除了一次 GPU→CPU→GPU 的往返数据传输,cudaMemcpyAsync 每步节省约 3 ms,性能提升在 3.3%+。
  • OpenMP top-K 提取,K=32→8:draft_logits 步骤的开销降低了 7%。
  • 树感知的 ggml_ssm_conv_tree:兄弟节点沿着父链收集各自的卷积窗口,而不是按 DFS 顺序收集。该实现移植了 SGLang 的 causal_conv1d_triton HAS_EAGLE_TREE_CUSTOM_ATTN_MASK 逻辑。
  • 兄弟节点被接受后的 target_feat 压缩:修复了遍历树分支时过期的 draft 特征,在 HumanEval 10-prompt 中的 9 个 prompt 上真正地启用了 tree rescue。
  • budget = 15 且 K = 1 的快速路径:当不需要兄弟节点时,跳过耗时 11 ms 的 CPU top-K 提取。
  • extract_draft_topk 反转 bug:sort_heap+cmp_greater 本身已经生成降序结果;额外的 std::reverse 反而把最差的候选 token 送到了树的根节点,导致每步 accept = 1。一行代码修复了。
  • verify_logits_buf 溢出:缓冲区大小原本设为 vocabq_len,但 DDTree 在 budget = 15 后会读取到 vocab(budget+1)。这会导致静默内存损坏,同样通过一行代码修复了缓冲区大小问题。

24 GB 显存上的 128K 上下文

ggml-cuda 中的 Flash-attention 原生支持 Q4_0 格式的 KV,因此 KV 缓存的压缩在写入时只要调用一次 ggml_cpy,并用内置的 F32→Q4_0 量化器完成写入量化。相比 f16,KV 缓存实现了 8 倍的压缩比。

结合滚动的 4096-slot target_feat 环形缓冲区,也就是写入时循环覆盖、读取时再环形边界处拆分读取。在 128K 上下文情况下,target_feat 从 6.6 GB 缩小到了 0.2 GB。整个方案使用同一个二进制文件,并通过环境变量切换配置:

image (2).png

f59b21f3-bb7c-40b2-8a95-642f9de0cf86.png

这里有个权衡点:在短上下文下,Q4_0 格式的 KV 在 HE 上的质量会损失约 3%,AL 会从 8.56 降至 8.33,但在长上下文中,它的整体表现会显著提升。这是唯一能让 24 GB 显存塞进 128K 上下文的方法。

Prefill 阶段

  • 短 prompt(≤2048 tok):PREFILL_UBATCH 设定为 16 时,这个设置与 DFlash 的块大小和 chain-verify 阶段的 q_len 保持一致,可以尽量减少 Flash-attention tile 偏移。
  • 长 prompt(>2048 tok):系统会自动将 PREFILL_UBATCH 设为 192。对于 13K token 的 prefill,耗时从 40.9 s 降到 15.07 s,prefill 吞吐率提升了 2.7 倍,约为 913 tok/s。更大的批处理大小会导致 ggml_gated_delta_net 内部嵌入的中间状态区域出现 OOM。
  • 可以通过环境变量 DFLASH27B_PREFILL_UBATCH=N 手动覆盖 UBATCH 设置。
  • 如果你想达到完全与 llama.cpp 对齐的 prefill 速度(约 1500 tok/s),还需要一个跳过嵌入式 dst 区域的 no_inter 子变体,来解除 UBATCH > 192 的限制。

后续计划

  • 守护进程模式(Daemon mode):在多轮对话中让模型常驻内存,使首 token 延迟从 10 秒级降至毫秒级。目前,Chat REPL 和 OpenAI 服务器还是会在每次请求时重新拉起 test_dflash。
  • 在验证路径中加入 Temperature / top-k 采样:目前,硬编码仅支持贪心解码;OpenAI 服务器上接收的 temperature / top_p 参数会被直接忽略掉。
  • 支持 Q5_K_M / Q6_K 目标模型:相比 DDTree 论文中的 BF16,Q4_K_M 在逐位置接受率上损失了约 30 个点;如果有更高质量的 GGUF 量化版本能装进显存里,应该能恢复大部分接受率损失。
  • 全面集成到 llama.cpp:新增 qwen35 架构支持,编写 llama-speculative-dflash.cpp,并打通 llama-cli / llama-server。
  • 开发 no_inter 算子变体:用来解锁更大的 PREFILL_UBATCH 限制,上文提过目前上限为 UBATCH=192;对齐 llama.cpp 的全速 prefill 吞吐率,让吞吐达到 1,500 tok/s。
  • Metal/Vulkan 后端:暂无计划。只维护单二进制、仅支持 CUDA 的实现。想要 Metal 支持的同学,可以自行 fork 开发。

补充测试

本文作者在上周 Qwen3.6-27B 发布之后,做了新的测试:

image (3).png

在单张 RTX 3090 上,使用 Qwen3.6-27B 和 60K 上下文,解码速度达到了 89.7 tok/s。比 full attention 机制快 3.64 倍,投机接受率达到了 100%。

我们刚把 sliding window Flash-attention 和 two-phase 缓存合并进 Luce DFlash。现在,Flash-attention 只用关注最近 2,048 个 KV 位置,而不是完整的 60K 上下文,因此解码速度从 25 tok/s 跃升到了 91 tok/s。two-phase 缓存会在 prefill 阶段跳过约 1.4 GB 的 rollback tensors,并在之后再迁移它们。这会释放足够的显存,让 PREFILL_UBATCH 可以从 192 提升到 384。


原文出处:https://x.com/pupposandro/status/2046264488832213174

标签: none

添加新评论