DGX Spark + TensorFold/SparkFold 跑 Qwen3.8-Flash-Next 完整实战:EXL3 4.05bpw 由 18 个补丁到中文 draft 词表,decode 64.7 tok/s(附全套 benchmark)
-
DGX Spark + TensorFold/SparkFold 跑 Qwen3.8-Flash-Next 完整实战:EXL3 4.05bpw 由 18 个补丁到中文 draft 词表,decode 64.7 tok/s(附全套 benchmark)
沿着
dgx-spark-llm-serving-main仓库lanes/exl3-sparkfold这条路线,我在 DGX Spark 上从零跑了一遍完整 setup(2026-10-29 部署完成,服务在跑),顺便做了全套 benchmark 和中文 draft 词表的 A/B 实测。有几个发现(尤其中文词表那 part)想还回来给社群。一、硬件
- DGX Spark:GB10(sm_121a)、128 GB 统一内存、aarch64,Docker 带 NVIDIA runtime
- 根分区 3.7 T,这条 lane 共占 173 G:
~/models/qwen3.8-flash-next-exl3-4.05bpw 101 G 权重 ~/.cache/tensorfold-sparkfold 967 M 扩展缓存 + 视觉权重 ~/tensorfold-src 25 M 打补丁的源码树 ~/src ~70 M 上游配方 + TensorFold tarball /var/lib/docker ~70 G 镜像- 前置条件(
01-healthcheck.sh只读体检,不要跳过):可用统一内存 ≥ 121 GiB、磁盘可用 ≥ 200 GiB(权重 100 + 缓存 + spill 最多 48)、docker info能看到 NVIDIA runtime。
二、软件栈和安装
- 引擎:TensorFold 0.6.5 + SparkFold
806b345+ 仓库 18 个补丁。这条 lane 的全部价值来自那 18 个补丁——仓库记录过未打补丁时英文 decode 42.6 tok/s vs 打完 62.9,不要用未经补丁的 SparkFold 跑。 - 镜像:
tensorfold-qwen38:v0.3.6.3(MiaAI-Lab 封装)再叠一层xgrammar(response_format/ 结构化输出)→tensorfold-qwen38:v0.3.6.3-xgrammar(36.9 GB,tf.patches=c1f5d72f8d16)。 - 部署脚本全部做成幂等,失败重跑即可;都支持
DRY_RUN=1打印将执行的命令。
cd ~/dgx-spark-deploy && chmod +x *.sh bash 01-healthcheck.sh # 只读体检,先做 bash 02-build-tree.sh # SparkFold 806b345 + 18 补丁 → ~/tensorfold-src bash 03-build-image.sh # 两层镜像(v0.3.6.3 → v0.3.6.3-xgrammar) bash 04-download-pack.sh # 权重 99.94 GiB:钉 commit sha + 逐文件 size + SHA-256 校验 bash 05-vision.sh # 视觉权重转换(可选,VISION=0 就跳过) bash 05b-draft-vocab-zh.sh # 构造中文 draft 词表(中文场景建议做,见第七节) bash 06-start.sh # 启动(封装仓库的 start.sh,不重新实现启动逻辑) bash 07-smoke.sh # 冒烟 + 中英文基准(RUN_BENCH=1 附带仓库的 bench_en.py)全新机器先跑
bash 00-bootstrap.sh:检查并补齐 Docker / NVIDIA Container Toolkit /hfCLI,已装的一律跳过,最后一步跑 GPU 容器冒烟,确认容器里真能看到 GB10。
02-build-tree.sh末尾应显示spill.py 在位(补丁 0005)。
MiaAI-Lab 的prepare.sh第 3、4 步会下载该封装自己的 MLX checkpoint,本 lane 用不到;照跑是为了与上游保持一致。三、两个必改的关键配置(今次分享重点之一)
3.1 内存地板 6 GiB —— 不是随便定的
TensorFold 0.6.5 的 stream cache 规则:只有当宿主机
MemAvailable高于max(4 GiB, RAM/10)(本机 = 12.2 GiB)再加 2 GiB 时才允许增长,否则直接丢掉一条空闲对话的整个状态。而 Spark 服务这个模型时空载只有约 15 GiB 可用——于是一条长对话会把另一条挤掉,每一轮都变成冷 prefill。仓库的对照实验(三条对话轮流,16 token 回复):
内存地板 后面几轮从零重新 prefill 的次数 12.2 GiB(引擎默认) 8 轮里 7 轮(丢了 409 s 的 prefill) 6 GiB( TENSORFOLD_MEMORY_RESERVE_GIB=6)9 轮里 0 轮 代价:到 earlyoom SIGTERM 线(RAM 的 3% = 3.6 GiB)的余量从约 11 GiB 降到约 5 GiB。所以不要把 n-gram 表全锁住——仓库记录过
TENSORFOLD_PIN_EXTRA_GIB=12(锁 36 GiB)会让 earlyoom 直接 SIGKILL 掉服务。当前配置PIN_EXTRA_GIB=-6.2,让锁页量维持在 20-25 GiB。3.2 NVMe spill —— 救 evicted 的对话,不是加速
TF_SPILL_DIR=/cache/spill,只写 ≥ 16384 token 的状态,目录上限 48 GiB,LRU 淘汰。仓库实测:100k token 的状态约 1.5-1.8 GiB,写 0.30-0.40 s,读 0.26-0.45 s。它只在对话被挤掉时救命。内存够时它什么都不做,也不带来加速。仓库明确写:只在受控重放里验证过,没有在真实流量上验证过——当成便宜的保险,不要当成实测收益。
四、模型
turboderp/Qwen3.8-Flash-Next-exl3,分支4.05bpw_h6_ng6,EXL3 4.05 bpw,125B 总参 / 6B 激活,99.94 GiB。本机校验结果:26 个文件的 size + SHA-256 与 Hub 逐个一致,实下 100.08 GiB(trunk 62.9 GiB / n-gram 表 36.36 GiB / 视觉 0.52 GiB,与仓库记录逐项吻合)。
为什么必须逐文件校验:仓库的 vLLM lane 明确记录过——大小对但内容截断的 shard 能正常加载、报告出合理的 shape、产出量级正确的激活值,然后生成通顺但完全错误的输出。不崩溃,不报错,只是不对。有人为这个丢了一天。
另两个硬坑(与网络无关,但必须钉住版本):
- 上游封装仓库(MiaAI-Lab)的
main已换成 Zig 原生引擎,与本 lane 的 Python 引擎不兼容。必须用 commit38da375e5f4a0be0ed4c065d1014f61088e0a9e4(2026-09-29,最后一个 Python 引擎 + TensorFold v0.3.6.3 版本)。这个 commit 是对的,有硬证据:它的补丁集 hash 是c1f5d72f8d16,与仓库 README 记录的镜像标签tf.patches=c1f5d72f8d16一字不差。 04-download-pack.sh会先把分支名解析成 commit sha 再下载,钉住版本、可复现,并逐文件比对 size + SHA-256——照做就行。
五、生产启动配置(全套)
06-start.sh透传给仓库start.sh:变量 默认 说明 PROFILEsparksafe= 旧解码策略(A/B 对照用);bare= 引擎默认值SPILL1NVMe spill。 SPILL=0关掉VISION1多模态。 VISION=0关掉PARALLEL5并发流数 CONTEXT262144上下文长度 KV_DTYPEint8KV 精度 PORT8888端口 BIND127.0.0.1监听地址。 BIND=(空)= 所有网卡KNOBS— 追加任意 TF_*,如KNOBS="TF_KEEP=32"KNOBS="TF_DRAFT_VOCAB=draft_vocab_zh.txt" bash 06-start.sh # 中文词表(推荐) PROFILE=safe bash 06-start.sh # 对照组 VISION=0 SPILL=0 bash 06-start.sh # 纯文本、不落 NVMe BIND= bash 06-start.sh # 对外提供服务(服务没有 API key,不要暴露到不可信网络)启动日志里应该看到这四行:
lone-stream graph slot: 65536 rows spill: N stored states adopted ... GiB of pages locked <- 本机实测 "22.0 GiB of it locked"(仓库记录 20-25 GiB) serving qwen3.8-flash-next ... loaded in ~90s首次启动要编译 CUDA/Triton 扩展(本机 260 s),之后重启 73.8 s。脚本会等到
/health有响应(最多 15 分钟)。服务在127.0.0.1:8888。常用操作:
bash 07-smoke.sh # 冒烟 + 中英文基准 docker logs -f llm # 看日志 docker exec llm env | grep TF_DRAFT_VOCAB # 确认实际生效的词表日志目录
~/.cache/tensorfold-sparkfold/logs/。仓库的start.sh会docker rm -f旧容器、日志随之消失,所以06-start.sh现在重启前会先把旧日志备份进去。六、Benchmark(本机 2026-10-29 vs 仓库基线 2026-10-21)
Decode 单流: 英文 64.7 tok/s(69.12 / 68.72 / 55.50 / 65.54 四条提示均值)vs 仓库 62.9——同一套构建,吻合。中文装词表 53.4、不装 38.9(见第七节);仓库韩文基线 54.1。
并发: 仓库 sgbench 端到端 1 / 2 / 4 流 = 66.9 / 92.1 / 131.1 tok/s;llama-benchy decode(深度 0)1 / 2 / 4 流 = 61.7 / 89.5 / 112.2 tok/s。本机只测了单流,多流并发没有测。
Prefill(冷启动): 1.9k-49k token 真实文本 1,830-2,067 tok/s(仓库)。
Spill 还原: 100k 对话被挤掉后,冷启 40-50 s vs 从 NVMe 还原 0.8-2.3 s(仓库)。
工具调用: tool-eval-bench 90/100(装 xgrammar 后,仓库)。
关键校验(本机):
- 权重:26 个文件 size + SHA-256 逐个一致,100.08 GiB
- 源码树:18 个补丁提交全在,
draft_vocab_ko.txt86,232 行(与仓库记录一字不差) - 镜像:
tf.patches=c1f5d72f8d16(与仓库 README 记录一致) - 引擎启动:22.0 GiB locked、84 个图、spill 目录 48 GiB、视觉塔 0.84 GiB
没测的(诚实清单): 中文质量没有基线(仓库对韩文质量和长工具上下文质量也明确没测过,速度追平 ≠ 质量追平,需要自己评估);多流并发;视觉输入(权重转换好了、引擎起来了,但没实际发过图片)。
七、中文 draft 词表 A/B 实测(新发现)
背景:MTP 投机解码的 drafter 只能从一张候选 token 列表里挑下一个 token。引擎自带的那张列表来自 CPython 标准库和 id < 65,536:
覆盖 比例 代码 / Markdown token 99.3-99.5% 英文散文 token 93.9% 韩文 token 47.5% 中文比韩文更糟。 这张表基本全是 ASCII,而 Qwen 分词器里绝大多数汉字 token 的 id 都 ≥ 65,536——它们根本不在候选表里。drafter 看不见中文 token,中文的投机接受率会很低,而 decode 速率基本就是"不投机"的速度。
上游其实已经做了中文版(MiaAI-Lab 的语言补丁带
draft_vocab_zh.txt,22,088 个纯汉字 token id),但那是替换语义——上游 README 明写 "English and code get a little slower with it"。本 lane 的TF_DRAFT_VOCAB是并集语义(仓库补丁 0009 的韩文表就是这么做的,补齐后英文贪心回复逐字节不变)。所以05b-draft-vocab-zh.sh取的是 并集:引擎默认表 ∪ 上游汉字 id = 101,679 个 id(默认 79,591 + 汉字 22,088)。中文拿到全部候选,英文和代码不受影响。A/B(同一套构建、同一批中文提示、同一份 server 配置,只换
TF_DRAFT_VOCAB):词表 ZH-HIST ZH-SCI ZH-ADV 均值 draft_vocab_ko.txt(仓库默认,不含汉字)34.38 42.80 39.52 38.90 draft_vocab_zh.txt(默认表 ∪ 汉字)50.40 55.96 53.68 53.35 +37.1%。 仓库测韩文时是 42 → 49(+17%),中文的增幅更大——因为默认表里汉字的覆盖率比谚文还低。旁证:每 chunk 的 token 数从 1.20-1.59 升到 1.88-2.11,也就是 MTP 接受率上去了,这正是 draft 词表该起的作用。对照仓库韩文基线 54.1:中文词表装上之后,中文追平韩文。
基准方法(
tools/bench_zh.py,与仓库bench_ko.py同构):走/v1/chat/completions,不覆盖采样参数(用服务端默认),thinking 打开,三条中文长句提示(历史散文 / 科学解释 / 现实建议),600 token 是测量窗口不是服务上限,流式测量、用首末 chunk 之间的时间把 decode 从 prefill 里分出来。复现:
cd ~/dgx-spark-deploy # A:仓库默认(不含汉字) KNOBS="TF_DRAFT_VOCAB=draft_vocab_ko.txt" bash 06-start.sh BENCH_MODEL=qwen3.8-flash-next BENCH_URL=http://127.0.0.1:8888/v1/chat/completions \ python3 tools/bench_zh.py # B:中文并集表 KNOBS="TF_DRAFT_VOCAB=draft_vocab_zh.txt" bash 06-start.sh BENCH_MODEL=qwen3.8-flash-next BENCH_URL=http://127.0.0.1:8888/v1/chat/completions \ python3 tools/bench_zh.py要注意的取舍:
- 只影响速度,不影响质量。 draft 出来的 token 要经过验证才被采纳,换词表不会改变回复内容(仓库对韩文验证过"英文贪心回复逐字节相同")。
- 一次只认一个词表。
TF_DRAFT_VOCAB是一个文件名,韩文表和中文表不要同时用;中文表取的是并集,换过去不会丢掉英文/代码的覆盖。 06-start.sh会把仓库默认的韩文表和 KNOBS 一起传给 docker,同名变量以最后一个为准;用docker exec llm env | grep TF_DRAFT_VOCAB确认实际生效值。
八、陷阱清单
- 未打补丁的 SparkFold 千万别跑:英文 decode 42.6 vs 打完补丁 62.9,差一半。
- 权重必须逐文件 SHA-256 校验:截断的 shard 不崩溃、不报错,只是生成"通顺但完全错误"的输出。
- n-gram 表千万别全锁:
TENSORFOLD_PIN_EXTRA_GIB=12(锁 36 GiB)→ earlyoom 直接 SIGKILL 服务。 set -e+pipefail会让脚本静默死掉:grep没匹配 → 退出 1 → 管道非 0 → 脚本被杀,什么错误信息都没有。07-smoke.sh就这么死过一次(引擎的锁页日志是 "22.0 GiB of it locked",不是仓库旧版的 "pages locked")。凡是grep ... | ...形式的输出行,后面都要|| true。- 上游封装仓库的
main已换成 Zig 引擎:必须钉 commit38da375e…(补丁集 hashc1f5d72f8d16对得上镜像标签,是硬证据)。 - 仓库
start.sh会docker rm -f旧容器,日志随之消失——06-start.sh已改成先把日志备份到$CACHE/logs/再重启。 - 第一条回复特别慢是正常的:另一个 graph 槽位首次解码要多花 0.6-1.6 s,第二次就正常。
- 本 lane 不主打 prefix caching:重复前缀不保证命中,与 vLLM lane 不同。
- 服务没有 API key:
BIND=对外监听只可以在可信局域网里用。 - 对话每轮都重新 prefill 多半是内存地板问题:看日志里
memory gate evicted ...,确认TENSORFOLD_MEMORY_RESERVE_GIB=6。
致谢
dgx-spark-llm-serving仓库(lanes/exl3-sparkfold)的前辈记录、turboderp 的 EXL3 权重、MiaAI-Lab 的封装与上游语言补丁、TensorFold / SparkFold。中文质量评估如果有人焗到更深,欢迎交流。 -
-
-
-
