22 分钟阅读

一台 DGX Spark 上的 Qwen3.8-27B

一台桌面 AI 系统承载两条推理路径

我的目标很具体:在一台 NVIDIA DGX Spark 上,用同一个 vLLM 进程提供两个 Qwen3.8-27B 模型身份:

qwen3.8-27b             干净的 mixed-NVFP4 base
qwen3.8-27b-mode-b      按请求加载的可选 adapter

最直接的实现功能上成功了,架构上却失败了。原先的运行时 output projection hook 会让所有请求——包括把投影强度设为零的“干净请求”——都进入修改后的 CUDA graph。单请求吞吐从大约 45 output tok/s 掉到了 30 output tok/s

最终解决方案不是再往运行时里加一层条件判断,而是把投影严格改写成标准 rank-1 PEFT LoRA,让 vLLM 原生的 per-request LoRA 调度负责路由。没有选择 adapter 的请求完全绕过 LoRA;选择第二个模型 ID 的请求,则在同一进程里加载一个只有 8.3 MiB 的 adapter。

一台 DGX Spark 上的结果是:

  • 旧 always-on hook 下的 clean base:30.32 output tok/s
  • native-LoRA 服务中的 clean base:45.41 output tok/s
  • 同一服务中的 adapter 请求:31.92 output tok/s
  • 相对旧 hook,clean fast path 恢复了 49.8%

这篇文章完整说明这四个数字背后的代数、混合量化权重转换、DFlash2 参数、缓存边界、benchmark 口径和失败过程。脱敏后的完整实现已开源在 liuzl/qwen38-dgx-spark-lab

这是独立社区实验,不是 Qwen、NVIDIA、vLLM、SGLang、RadixArk 或 Z Lab 的官方项目。仓库只发布代码、方法和聚合数据,不发布模型权重、draft 权重、源变换 artifact、生成的 adapter、凭据或内部评测记录。

为什么必须是同一个进程

DGX Spark 的 GB10 Grace Blackwell 系统提供 128 GB coherent unified memory。它让 27B 稠密模型真正进入桌面设备,但仍不足以允许我们毫无节制地复制服务状态。

本次 vLLM 进程实测约占 46 GiB:

target checkpoint       20.42 GiB
DFlash2 drafter          3.58 GiB
FP8 KV cache             16.00 GiB
CUDA graphs               2.06 GiB
runtime workspaces        ~4.0 GiB

而 adapter 只有 8.3 MiB。如果为了暴露两种服务模式而运行两套完整引擎,就会重复加载最昂贵的 target、draft model、KV pool、CUDA graph 和 workspace。原生 LoRA 把第二个身份的额外成本降到了几乎可以忽略的程度。

Qwen vLLM 进程内存账单

参考栈固定为:

这些身份都要固定。checkpoint 名称、nightly tag 和编译缓存都不是实验身份;revision 与 hash 才是。

第一次失败:干净请求进入了不干净的图

可选变换最初表现为若干线性子层输出后的 residual-space projection:

y=yλcr(rTy)y' = y - \lambda c r(r^T y)

其中 rr 是 residual space 中的方向,cc 是每个模块的系数,λ\lambda 控制修改强度。本次方向集合覆盖 128 个写回 residual stream 的模块:

  • 48 个 Gated DeltaNet linear_attn.out_proj
  • 16 个 full-attention self_attn.o_proj
  • 64 个 MLP down_proj

第一版 per-request 实现把一个 adapter 强度标量接进三套 API,并将其加入 cache salt。功能测试全部成功:base 与 adapter 输出稳定,prefix cache 不跨 arm 串值,非法参数返回 HTTP 400,真实 Claude/Codex 风格的工具闭环也能工作。

问题在于,projection buffer 和 projected graph 对所有请求都存在。把 λ\lambda 设为 0 只能消掉数值上的修改,不能恢复原来的执行路径。同一 C1 workload 下:

Per-request hook 模式Output tok/sDFlash acceptance length
lambda=030.323.46
lambda=130.883.52
独立 clean image45.175.15

这次失败给出了整项工作的关键诊断:系数为零,不等于没有进入 adapter path。要保住 clean model 的性能,clean identity 必须完全绕过修改后的图。

把 output projection 改写为 rank-1 LoRA

对线性映射 y=Wxy=Wx,固定 λ=1\lambda=1

y=Wxcr(rTWx)=(Wcr(rTW))x\begin{aligned} y' &= Wx - cr(r^T Wx) \\ &= \left(W - cr(r^T W)\right)x \end{aligned}

所以:

ΔW=BA,B=cr,A=rTW\Delta W=BA, \qquad B=-cr, \qquad A=r^T W

BB 只有一列,AA 只有一行——这就是一个标准 rank-1 LoRA。转换器为 128 个目标模块分别生成 A/BA/B,最后打包成 PEFT adapter。

代数很简单,checkpoint 却不简单。实测 RadixArk 权重不是统一 4-bit,而是混合精度:

存储 dtype大小
packed NVFP4 (U8)8.56 GiB
FP87.79 GiB
BF164.07 GiB
合计20.42 GiB

对于 static-FP8 输出权重,转换器可以直接读取;对于 packed-NVFP4 MLP projection,则要解 E2M1 nibbles,应用 per-block FP8 scale 和 global scale,再按行分块累积 rTWr^T W。整个过程中不需要在内存里展开一个完整的反量化 27B 模型。

还有一个必须诚实说明的近似:对普通线性映射,post-linear projection 与 weight-space LoRA 完全等价;但量化 activation kernel 会引入 rounding,所以两种实现不能假定 bit-identical。代数只证明变换形式正确,最终 artifact 仍必须经过私有 adapter 与能力门禁。

最终服务拓扑

单进程 native-LoRA 服务拓扑

最终拓扑反而非常朴素:

  1. 一个 vLLM 进程加载 mixed-NVFP4 Qwen3.8-27B target;
  2. 同一进程加载 DFlash2 draft checkpoint;
  3. 把 8.3 MiB PEFT adapter 注册成第二个 model ID;
  4. base request 不带 LoRA mapping;
  5. adapter request 使用 vLLM 原生 LoRA identity。

它同时解决两个问题。第一,base request 不进入 adapter graph,因此恢复 clean CUDA fast path;第二,vLLM 会把 native LoRA identity 纳入 prefix-cache key,不再需要自定义 cache salt 协议。

为什么是 probabilistic K7

DFlash2 的配置绝不是无关紧要的细节。早期用 greedy drafting、depth 10,只得到 25.62 tok/s,acceptance length 2.97。显式改成 probabilistic drafting、depth 7 后,同一单请求 workload 达到 45.49 tok/s,acceptance length 5.15——差异达到 77.6%。

把 sampler 改成 probabilistic 后,K10 依然落在低接受区。更多 speculative tokens 只有在 target 接受它们时才有价值;draft depth 是一个依赖 workload 和 checkpoint 的搜索问题,不是越大越快的单调旋钮。

adapter path 还暴露了 target/drafter distribution mismatch 的代价。LoRA 修改了 target,却没有修改 drafter。C1 acceptance length 从 5.13 降到 3.74,吞吐也从 45.41 降到 31.92 tok/s。adapter 本身的计算量很小,真正昂贵的是 draft token 被接受得更少。

性能结果与正确读法

C1 与 C8 实测吞吐

C1 workload 请求 512 个随机 input tokens,tokenizer 实际得到 526 tokens;生成 2,048 tokens,ignore EOS,concurrency 1,temperature 0,关闭 thinking。clean baseline 用五轮确定;native-LoRA 表格采用 warmup 后的一轮 matched qualification。

C8 workload 共 32 个随机请求,并发为 8。每个请求 1,024 input、256 output,实际 input 约 1,036 tokens。

模式C1 output tok/sC1 acceptanceC8 output tok/sC8 acceptance
Clean vLLM baseline45.175.1595.77~2.6
Native-LoRA server,base45.415.1393.162.69
Native-LoRA server,adapter31.923.74100.523.05
旧 always-on hook,base30.323.46

这里有三条必须和数字一起发布的解释:

  1. +49.8% 比较的是 native-LoRA base request 与旧 always-on hook,不代表比 vanilla vLLM、SGLang、所有 Spark recipe 或更新 checkpoint 快 49.8%。
  2. C1 是单请求 output throughput;C8 是八个并发请求的 aggregate output throughput。两者不能混用。
  3. native-LoRA 的 C8 数字是单轮 qualification,不是置信区间。adapter 的 C8 本轮高于 base,不应在没有更多重复和 workload 变化时外推。

Prefix cache:隔离也是正确性

共享 prefix cache 只有在不同模型身份绝不复用彼此 KV block 时才安全。验证器让两个 alias 依次请求相同长前缀,并读取 vLLM cache-hit counter:

Base 与 adapter 的 prefix-cache 隔离

请求Cache-hit 增量
base 第一次0 tokens
base 同前缀第二次3,296 tokens
base 后第一次 adapter0 tokens
adapter 同前缀第二次3,296 tokens

adapter 第一次请求紧跟在已缓存的 base 之后,仍然得到 0 hit;adapter 自己重复后才命中 3,296 tokens。这正是需要的语义:同一身份内复用,不同身份间隔离。

API 与真实 Agent 门禁

本地 benchmark 很快,但客户端不兼容,不能算部署。公开 validator 对两个 alias 验证:

  • OpenAI Chat Completions;
  • OpenAI Responses;
  • Anthropic Messages;
  • 三套 API 的 forced tool call。

内部 qualification 还跑了真实 CLI agent 的工具闭环。协议 shim 的失败往往很隐蔽:普通文本可以返回正常,但 forced tool call、reasoning block、streaming field 或 Anthropic compatibility 可能已经丢失。

私有 adapter 门禁

可选 adapter 通过了本实验定义的内部兼容与回归门禁,绑定轮次中没有空回答或请求错误。公开材料只保留粗粒度 pass 信号,不描述私有套件构成、类别拆分、规则或原始输出。

这个结果不是通用能力证书。任何经过变换的模型在部署前,仍需完成任务、工具使用、具体业务、隐私、可靠性与应用级评测。

真正决定设计的几次失败

最终 clean 结果来自一连串值得保留的失败:

  • 编译缓存身份不完整。 上游 DFlash/DSpark cache key 没有包含 speculative depth,不同 K 值可能复用不兼容 artifact。每种实验形态必须使用独立 cache root,公开镜像则应用 fail-closed overlay。
  • DFlash2 constructor 需要固定补丁。 当时 nightly 的 shared constructor 选择了普通 DFlash layer,遗漏 attention_conv。固定容器 digest 和 patch anchor,才能让上游漂移显式失败。
  • autotune 会吃掉最后的安全余量。 自动分配大 KV cache 可以加载权重,但 FlashInfer autotune 随后触发 driver OOM。稳定方案使用显式 16 GiB KV,并关闭 FlashInfer autotune。
  • 旧服务是真实的显存泄漏。 一个遗留 llama-server 仍占约 21.7 GiB;之后 SGLang 和 vLLM 同时存在,使分配接近 100 GiB并触发 NVRM OOM。进程清单本身就是模型部署的一部分。
  • 参数为零不等于 bypass。 这是最重要的架构教训。若 clean-path 性能重要,就必须证明 clean request 没有进入修改后的图。

当天出现的新 BF16 lm_head checkpoint

就在参考结果发布当天,RadixArk 又发布了 Qwen3.8-27B-NVFP4-BF16-LMHead。它只把量化 lm_head 换回原始 BF16 权重,其余 tensor 据 model card 与来源 NVFP4 checkpoint 相同。

这已经是一个新的实验身份。当前公开 model card 在相同 GSM8K protocol 下给 BF16-head 版本 96.13%,来源 checkpoint 96.36%,并明确称差异处于单次 sampling noise 内;其他 workload 可能不同。因此本文不对新 checkpoint 的准确率或速度作任何外推。

更新 recipe 前,需要重新完成:

  1. converter compatibility 与 hash;
  2. 不同 K 下的 DFlash2 acceptance;
  3. C1/C8 throughput 与 TTFT;
  4. capability 和 tool-use 评测;
  5. 私有 adapter 门禁;
  6. cache isolation 与 soak。

这也是为什么文章与仓库都固定 revision,而不是把 Hugging Face repo 名当作不可变对象。

如何复现

公开仓库包含固定的 ARM64 Docker build、混合量化转换器、启动脚本、API/cache validator、benchmark harness 和脱敏 JSON 结果:

git clone https://github.com/liuzl/qwen38-dgx-spark-lab
cd qwen38-dgx-spark-lab

docker build -t qwen38-vllm-dflash2:lab docker/

# 单独取得并审核 target、drafter 与兼容的源 artifact,
# 然后把源变换转换成原生 PEFT adapter。

cp configs/qwen38-spark.env.example .env
$EDITOR .env
set -a; source .env; set +a
scripts/serve-native-lora.sh

python3 scripts/validate-apis.py \
  --base-url http://127.0.0.1:18102 \
  --base-model qwen3.8-27b \
  --adapter-model <adapter-model-id>

python3 scripts/validate-cache-isolation.py \
  --base-url http://127.0.0.1:18102 \
  --base-model qwen3.8-27b \
  --adapter-model <adapter-model-id>

仓库不再分发权重、draft checkpoint、源变换 artifact 或生成后的 adapter。仓库自有代码采用 Apache-2.0;第三方模型与 artifact 保留各自许可。生成的 adapter 同时衍生自 base checkpoint 与变换输入,任何再分发者都需要独立确认自己拥有相应权利。

可以带到下一次部署的结论

这项实验表面上关于 Qwen3.8、DFlash2 和 DGX Spark,但可复用的结论更一般:

  • fast path 是一种架构属性。 必须测量,不能从一个值为零的参数推断。
  • speculative decoding 受 acceptance 支配,而不是只由 draft depth 决定。 吞吐必须和 acceptance length 一起报告。
  • 模型身份应包含 adapter、量化、drafter、sampler、软件 commit 和 cache state。 只有模型名远远不够。
  • cache isolation 属于正确性,而不只是优化。 要验证 first-cross-arm miss 和 same-arm reuse。
  • 一个 8 MiB adapter 可以避免复制数十 GiB 服务状态。 调度抽象比 adapter 计算本身更重要。
  • 限制条件也是 benchmark 的一部分。 一台机器、一套 pinned stack、没有 24 小时 soak、C8 单轮,都必须随结果公开。

真正有价值的结果不只是“45 tokens per second”,而是一个 clean path 确实保持干净、第二种模型模式拥有原生身份、性能与隔离结论都能够复现的服务。

链接