Qwen3.8-Flash-Next 单机部署:向上游 MiaAI 对齐 + 长上下文/真实负载全量实测

日期:2026-09-20 · 机器:懒猫 AIPod X5-T5000(Jetson AGX Thor T5000 · sm_110 · 统一内存 122.86 GiB) 模型: myllmbox/Qwen3.8-Flash-Next-hibrid48-uncensored 血统 + 自改 NVFP4 lm_head(98.7 GiB) 镜像: vllm-thor-plemmap:latest(vLLM 0.28.1rc1 + 自研 PLE mmap 补丁 · 镜像未改动)

本文是 blog_flashnext_uncensored_2026-09-18.md(部署/优化/GPQA)与 blog_flashnext_gpqa_2026-09-19.md(评测终局)的续篇。 本篇主题:上游仓库 MiaAI-Lab 于 9/19 更新后,它的做法能不能套到我们这份权重上 —— 逐条移植 + 逐条实测 + 逐条给结论。


一、上游这次更新了什么

MiaAI-Lab/Qwen3.8-Flash-Next-Single-DGX-Spark 于 2026-09-19 连推 3 个提交,仓库从 30 个文件涨到 55 个:

提交 内容
11:33 7×24 无人值守套件:supervise.sh 状态机 + 断路器 + 每周维护重启 + 心跳 + 告警 webhook + 健康探测
12:55 download.sh 校验重做(原实现会记住"曾通过"的 blob → 磁盘后来损坏查不出来)
17:00 新文档:怎么跑 NVIDIA 官方 checkpoint(ModelOpt v0.46.0 混合精度版)及其代价

它的 README 列出四个"真旋钮":① PLE mmap 的 MADV_RANDOM ② MAMBA_SSM_CACHE_DTYPE=bfloat16 ③ 减少词表草稿(47k/65k) ④ KV_CACHE_DTYPE=fp8。


二、前提问题:我们和他的模型是同一份吗?

血统:Qwen/Qwen3.8-Flash-Next → OrcaRouter(abliteration + NVFP4/FP8 重量化)
      ├→ MiaAI 直接打包(99 GiB · 39 文件)        ← 他
      └→ myllmbox 改造(PLE 分片 + lm_head 单独量化)← 我们
项目 他 我们 结论
model_type qwen4_exp qwen4_exp 同
层结构 48 层 = 36 GDN + 12 标准注意力 同 同
hidden / vocab 2560 / 248,320 同 同
PLE 表 320,001,536 行 × dim160 同 同
mamba_ssm_dtype float32 float32 同
lm_head BF16(1212 MB) NVFP4 4bit(341 MB) ★ 关键差异
vLLM 0.29 0.28.1rc1 差异

⇒ 结构级优化 100% 可套用;与"头部尺寸/量化格式"挂钩的收益不能照搬。 本文后面会反复用到这条。


三、① PLE mmap 加 MADV_RANDOM —— 有效,但上游文档漏了一个坑

做法:在我们的 PLE mmap 适配器 vllm_ple_mmap_thor.py 的 _mm() 里,对每个 np.memmap 调 madvise(MADV_RANDOM)。

上游没写的坑:madvise 要求起始地址页对齐,而 np.memmap(offset=...) 给不出对齐指针 →

[ple-mmap-thor] madvise(MADV_RANDOM) 失败 errno=22(EINVAL)→ 继续用默认策略

第一次移植直接吃 EINVAL,每片都失败(因为包在 try/except 里,是静默降级,服务不受影响,很容易看不出来)。修法:

aligned = raw - (raw % PAGE)            # 向下取整到页边界
length  = int(mm.nbytes) + (raw - aligned)
libc.madvise(ctypes.c_void_p(aligned), ctypes.c_size_t(length), MADV_RANDOM)

本地先复现再验证:未对齐 → ret=-1 / EINVAL ✗;对齐 → ret=0 ✓。上线后日志:

[ple-mmap-thor] mmap 已设 MADV_RANDOM ✓(shard=5 leaf=weight 2.98 GiB · 对齐修正 3304 B)
  MADV 成功片数: 16(8 shard × weight/weight_scale)· EINVAL 次数: 0

效果(受控微基准:8 发 × 400 token · 每发全新随机提示词,排除 prefix cache 造假)

指标 ① 前 ① 后 变化
每 token 磁盘读 1109.0 KiB 305.2 KiB −72.5%
单轮读盘总量 3.14 GiB 0.86 GiB −72.6%
KV 池 442,417 461,310 +4.3%
decode 速度 — 持平 中性

上游报的是 1366 → 57 KiB/token(−96%),我们只到 −72%(残留 305 KiB/token 来自其它路径,未归因)。但"①前的 1109 KiB/token"与他的 1366 同量级,说明"每 token 换入 64 KiB 读窗口"这个机制在我们这台机器上真实存在。

读盘计数要从容器内取(宿主用户读不到引擎进程的 /proc/<pid>/io): docker exec vllm-flashnext bash -c 'grep ^read_bytes /proc/<pid>/io'


四、② MAMBA_SSM_CACHE_DTYPE=bfloat16 —— 有效,复现了他的 +8.5%

做法:--mamba-ssm-cache-dtype bfloat16(0.28.1rc1 里是实名 CLI 参数,不是环境变量)。 生效铁证(启动日志会警告但我们传的值胜出):

WARNING [config.py:819] Qwen3.5 model specifies mamba_ssm_dtype='float32' in its config,
        but --mamba-ssm-cache-dtype='bfloat16' was passed. Using the user-specified value.

效果(thorbench · prose · 384 token · 2 预热 + 2 轮 · 同口径)

并发 基线(裸) ② 生效 变化
c1 46.2 48.6 +5.2%
c2 76.6 79.2 +3.4%
c4 118.1 124.2 +5.2%
c8 179.9 190.8 +6.1%

机制核验:ms/step @c8 由 93.37/90.19 → 86.36/84.49(−7.5% / −6.3%,上游报 −7.8% ✓),而 tokens/step 几乎不变(2.142 → 2.151)⇒ 纯步时收益,不是靠接受率凑的 ⇒ 与"recurrent state 由 fp32 减半 → 每步 state 流量减半"完全一致。

⚠️ 必须排除的假象:新容器首轮跑必然被 Triton JIT 冷启拖低。我们第一轮测得 c8 = 167.4,复测 190.8,差 14% —— 首轮数据一律不作数。 同一个坑今天踩了三次(见 §9)。


五、①+② 合并(公平轮,内核已热)

并发 基线 ①+② 变化
c1 46.2 49.6 +7.4%
c2 76.6 76.6 0%(噪声内)
c4 118.1 123.2 +4.3%
c8 179.9 191.9(单轮最高 198.2) +6.7%

⇒ 速度收益几乎全部来自 ②;① 在速度上中性,但它换来 −72% 读盘 + 4% KV 池,两者不冲突。


六、③ 减少词表草稿(47k/65k draft vocab)—— 我们这台拿不到,原因是格式不是难度

上游的收益来自哪里(他 patch 的文档串原文):

The MTP drafter carries its own BF16 ParallelLMHead over the full 248,320-token vocabulary: 1.27 GB, read once per draft step. At MTP 3 that is three of the four lm_head reads in an engine step, 3.8 GB of the ~15.6 GB a single-stream step moves.

即:他的草稿头是 bf16,切到 47k 后每步省 ~20% 搬运 ⇒ 实测 +13.1%。

我们的情况完全不同:

我们的草稿头 = nvfp4-pack-quantized(config 的 group_lm_head ✓ 实测确认)= 341 MB,只有他的 28%
每步头部搬运 = 341 MB × 3(目标验证 1 + 草稿 K=2 两读)= 1.0 GB
   c1:43.5 ms/step × 194 GB/s ≈ 8.44 GB/步 → 头部占 11.8%
   c8:86.0 ms/step × 194 GB/s ≈ 16.68 GB/步 → 头部占  6.0%
切到 47k 的理论上限:c1 +6.4% · c8 +3.2%(还没扣接受率损失;47k 覆盖 99.58%,损失很小)

而且实现路径也不干净:vLLM 的 NVFP4 线性内核在 post-load 阶段对 weight_scale 做了 swizzle_blockscale 重排 → 按行切 packed 权重是对的,但切出来的 scale 行不是合法布局;要自己把切片重做一遍 swizzle,而原始未 swizzle 的 scale 在 post-load 后已被覆盖。

另外,这条我们在这台机器上已经直接 A/B 过一次(技能库留档,当时头还是 bf16):47k 切片 vs 全词表 —— c1 42.11 vs 43.03(−2%)· c2 75.00 vs 64.82(+16%)· 接受率完全相同(设计如此,不可能改输出)。结论是"不确定/无稳定收益",机制是这里的草稿前向是延迟受限而非字节受限**。

⇒ 结论:不做。这是量化格式差异导致的,不是实现难度导致。 哪天换成 bf16 头的权重,这条立刻值得做。


七、④ KV_CACHE_DTYPE=fp8 —— 本镜像不可用

我们镜像 qsa.py 写死:supported_kv_cache_dtypes = ["auto","bfloat16"](第 67 行)
                     第 109 行显式 raise(非 bf16 一律拒绝)
上游 README 原话:  "The stock QSA backend refuses FP8 KV"
他为此写了 patch_qsa_fp8_kv.py(改 QSA Triton 核 + 重排 scale 乘法),是给 0.29 源码写的 → 不portable
收益(KV 池 ×1.85)对我们也不是刚需:现池 45.4~46.1 万 token 已够 8×16K / 4×70K / 1×256K

⇒ 结论:不做。 但这是那份 NVIDIA 配方里唯一"白给 KV 池"的一项,值得留档:哪天镜像能换,优先评估。


八、附带实验:MTP K=3 —— 账面很美,实测净亏

③ 被格式差异挡掉之后,做这个零代码的等价尝试。账面上很诱人:K=2 时 per_pos 第 3 位结构性恒为 0.00(只提 2 个草稿),若提到 K=3 命中 ~0.3 ⇒ tokens/step +14% 上限,代价只有一次额外的头部读(341 MB ≈ 1.8 ms)。

并发 K=2(①+②) K=3 变化
c1 49.6 45.7 −7.9%
c2 76.6 69.4 −9.4%
c4 123.2 116.7 −5.3%
c8 191.9 167.6 −12.7%

机制(这才是重点):K=3 确实拿到了更多 token/步 —— tokens/step 由 2.13 → 2.31~2.44(+9~14%),per_pos 第 3 位实测 0.22~0.29(与预测一致)—— 但 ms/step 涨 18~22%(c1 43.7 → 51.5 · c8 88.2 → 107.9)⇒ 净亏。

⇒ 这个模型的草稿前向是"延迟受限"而不是"字节受限":一次额外草稿要 8~15 ms,远超它 341 MB 权重在 194 GB/s 下的 1.8 ms 理论值。所以任何"减少头部字节"的招(③ 词表切片、FP8 头、NVFP4 头)都不会按带宽账兑现 —— 而 K=2 的边际收益高于 K=3 的边际成本。


九、不同上下文长度的 prefill / decode 曲线

口径:单流 · 每档全新随机提示词(prefix cache 不可能命中)· temp 0 · 关思考 · gen 512 · 每档 2 次(第一次当预热丢弃) · 直连 8001 绕过网关

真实 ctx TTFT prefill tok/s decode tok/s 间隔中位 p90 可用内存
1,936 1.34 s 1448 39.98 46.4 ms 52.6 20.8 GiB
2,086 1.52 s 1373 38.89 49.0 ms 52.8 20.8
15,651 8.84 s 1770 40.65 47.5 ms 52.0 20.8
16,079 8.90 s 1807 39.33 48.3 ms 52.5 20.8
62,470 34.84 s 1793 41.07 46.6 ms 51.9 20.8
62,588 34.71 s 1803 43.21 45.4 ms 51.4 20.8
124,307 69.61 s 1786 41.86 45.7 ms 52.8 20.8
124,704 70.34 s 1773 40.65 46.3 ms 53.0 20.8
243,361 140.88 s 1727 41.91 46.6 ms 52.3 20.8
244,369 141.04 s 1733 41.31 48.1 ms 53.5 20.8

三条结论

  1. prefill 几乎不随 ctx 衰减:16K→124K 稳定 1770~1807 tok/s,到 243K 才 −3%(1727~1733)。TTFT 完全线性:≈ ctx ÷ 1750 秒(16K→8.9 s · 64K→35 s · 128K→70 s · 244K→141 s)。小 ctx(2K)tok/s 偏低(1373~1448)是单个 chunk 太小、SM 填不满,但绝对时间只有 1.4 秒。
  2. decode 全程平 39~43 tok/s,没有"长上下文悬崖"。KV 从 2K 涨到 244K(122 倍),decode 只波动 ±5% ⇒ 瓶颈是权重搬运,不是 KV 长度 —— 这个模型的注意力是压缩+稀疏的(QSA + indexer)。
  3. 无周期性卡顿:p90/中位 = 1.11~1.15,全档一致(若 PLE 抓行或调度抖动,这里会翘起来)。

三个坑(今天都踩了)

  • 冷启动会把小 ctx 数据打假:第一轮 2K/8K/16K 只测出 613~631 tok/s,一度让我怀疑"16384 是个阈值"(--max-num-batched-tokens)。后来在热机上复测同一量级的 14,722 token → 1755 tok/s,16,000 → 1721 tok/s ⇒ 不是长度阈值,是冷启动。
  • 档内前快后稳:每档 512 token 生成内部,前 1/3 都偏快(46~49 tok/s),中/后 1/3 落到 38~44(2K 档同样如此)⇒ 这不是"长上下文导致的衰减",而是每个请求开头几步偏快再落稳态。判长上下文影响要用中/后段比。
  • 不要在测量跑着的时候插别的请求:我用一次"边界探针"污染了自己的复测(有一发测出 4.65 tok/s 的离谱值)。

与改动前(9/19)的对比 —— 一处自我纠正

第一轮单次采样让我以为"长 ctx 步时慢了 6~10%",怀疑是 ① MADV_RANDOM。干净复测(每档 2 次、冷暖并列)推翻了这个判断:

9/19(①+② 之前) 今天(①+②)
prefill @120K 1765 1773~1786(±1%)
prefill @240K 1735 1727~1733(±0.5%)
decode @124K 41.56 40.65~41.86(±1.5%)

⇒ ① 和 ② 对长 ctx 的 prefill/decode 都没有实质影响,收益只在短 ctx 聚合(c8 +6.7%)。所以 ① 可以放心留着(它换来 −72% 读盘 + 4% KV 池,不欠速度债)。 严格说:跨天对比只能说"没有 ≥5% 的差异",要判 3% 级的差别需要同一天做"① 开/关"的 A/B。


十、真实负载(代码 / JSON)实测 —— 以及和上游的步级拆解

前面用散文测的 49.6 tok/s 只是"通用负载"数字。换成我们日常真正在跑的负载类型(代码生成、JSON 结构化输出):

负载 c1(稳态) c8 聚合 tokens/step per_pos
散文 49.6 191.9 2.13 0.70 / 0.44
代码(写 LRU cache + 单测) 57.1 247.0 2.49 0.84 / 0.66
JSON(20 条结构化输出) 62.6 251.7 2.77~2.80 0.90~0.93 / 0.84~0.88

和上游那边的步级拆解

MiaAI(DGX Spark · bf16 头 · MTP3) 我们(Thor · NVFP4 头 · MTP2)
散文单流 48.7 tok/s 49.6
散文 8 并发 162.9 191.9(+18%)
每步 token(K) 2.80(K=3) 散文 2.13 / 代码 2.49 / JSON 2.77
★ 步时(反推) 57.5 ms 43.0 ms
头部每步搬运 bf16 1212 MB × 4 = 4.85 GB NVFP4 341 MB × 3 = 1.02 GB
每位置接受率 0.80 / 0.59 / 0.41 散文 0.70/0.44 · 代码 0.84/0.66 · JSON 0.90/0.84
我们步时快 34%(少读 3.8 GB 头);他每步多拿 31% token(草稿头是 bf16,无量化误差)
⇒ 散文负载上正好打平
⇒ 代码/JSON 这类有强结构重复的负载上,我们第 2 位接受率能到 0.66~0.88,每步 2.5~2.8 token
   ⇒ 变成全面领先(8 并发 247~252 vs 162.9)

这也解释了"我们的 MTP 是不是不行"这个疑问:不成立。 我们的 MTP 头与他是同一血缘;散文上接受率略低(0.70 vs 0.80)是因为我们主动把草稿头量化成 4bit(换步时 −34%,当年实测净 +15%/+25%);而在代码/JSON 上我们反而更高(0.84/0.90)。真正"上不去"的只有 K —— 根因是我们 0.28.1rc1 镜像缺 fused GDN-MTP 解码核(他 0.29 有),每次草稿比他贵。


十一、量化配方对照:我们这份 vs NVIDIA 官方那份

上游文档里介绍的 nvidia/Qwen3.8-Flash-Next-NVFP4 是 NVIDIA 自家 ModelOpt v0.46.0 的混合精度版。把两份配方并排:

部件 NVIDIA 官方 我们这份(config 实测) 含义
路由 MoE 专家 NVFP4 W4A4(E2M1 + 每 16 元素一组 FP8 E4M3 缩放,激活也 4bit) NVFP4 W4A16(group_size 16、E4M3 缩放、无 input_activations) 他激活也量化 → 计算受限时更快;decode 是带宽受限 ⇒ 对我们没好处
注意力层 / 共享专家 / 主干 BF16 FP8(naive-quantized 8bit · per-channel · 对称) 他更保精度,我们每步少搬一半字节
MTP 路由专家 FP8(block-scaled) NVFP4 4bit(mtp 下 4608 个专家张量全是 weight_packed/scale/global_scale) 他草稿更准,我们更小 ← 这正是散文接受率 0.80 vs 0.70 的根源
PLE n-gram 嵌入 FP8(47.68 GiB) NVFP4 4bit(26.88 GiB = 他的 56%) ★ 我们省 20.8 GiB,这是能塞下并且 KV 还剩 45 万 token 的原因之一
KV Cache FP8(262K 上下文下 ~1M 池) bf16(kv_cache_scheme: None) 他池子 ×1.85,我们镜像 qsa 拒绝 fp8
lm_head 未在配方里单列 NVFP4 4bit 自改,实测 +15%(c1)/ +25%(c2)

结论:他这份是精度优先(主干保 BF16、激活也量化);我们这份是足迹优先(全 4bit/8bit,98.7 GiB = 71.8 权重 + 26.9 PLE,他则是 124 + 48 GiB)。

在统一内存、带宽受限、还要留 KV 的 Thor 上,我们的取舍更贴硬件 —— 而且没有换来质量损失:GPQA Diamond 91.4%(181/198 · 95%CI ±3.9)vs 官方自报 91.7%,差 0.3 点落在噪声内。

有趣的是,他文档自己的结论也是"一台 Spark 上跑默认那份更合适",并且换成 NVIDIA 那份之后 KV 池反而从 975K 掉到 545~570K(PLE 47.68 + reserve 30)—— 两份配方的取舍是一体的:精度换尺寸、尺寸换 KV。


十二、定稿配置 + 回滚资产 + 今天的教训

定稿配置(正在跑 · 真请求已验)

PORT=8001 SEQS=8 GPU_MEM=0.73 MTP=2 BATCHED=16384 SSM_DTYPE=bfloat16 bash ~/mtpvocab/start_seqs.sh
# ① MADV_RANDOM 16/16 片成功 · EINVAL 0
# ② SSM bf16 有日志铁证
# KV 池 454,225~461,310 token(基线 435,941 → +4.2~5.8%)
# 可用内存 25 GiB(基线 21.2)

start_seqs.sh 新增三个默认关闭的旋钮(SSM_DTYPE / KV_DTYPE / DRAFT_VOCAB),默认空 = 与旧现场逐字一致。

回滚资产(一条命令回到"历史最好")

资产 说明
~/mtpvocab/restore_baseline_8001.sh 还原脚本 + 拉起 + 轮询到 HTTP 200 才停
start_seqs.sh.bak-preMia 出成绩那版脚本(与现行仅差新增 6 行)
ple_v2/vllm_ple_mmap_thor.py.preMADV-restored 与 backup/vllm_ple_mmap_thor.py.orig 逐字节一致

今天真正贵的三个教训

  1. 新容器首轮数据一律不作数(JIT 冷启):c8 167.4 → 190.8,c1 41.7 → 49.7,差 12~15%。今天被这个坑咬了三次。
  2. 改多行续行命令后必须逐行核 \ 结尾:给 docker run 插入一行时留下一个空行 → bash 把命令在空行处结束 → docker: 'docker run' requires at least 1 argument → 容器被 rm 掉且没重建。救命流程:bash -n → 干跑(sed 's/^docker run -d/echo DRY -d/')数 argv → awk 查块内不以 \ 结尾的行 → 编辑前留一份逐字节可用的副本。
  3. madvise 要页对齐(见 §3):上游文档没写,静默降级,只有日志里那行 errno=22 能看出来。

附:本篇用到的脚本与数据

文件 用途
~/mtpvocab/ple_readbench.py 受控读盘基准(8 发 × 400 token · 全新随机提示词 · 量 /proc/<pid>/io 的 read_bytes 增量)
~/mtpvocab/ctx_curve_clean.py prefill/decode vs ctx 曲线(含逐桶明细 · 每档重复 · 全新提示词)
~/mtpvocab/ctx_compare.py 多轮曲线按 log 刻度并排对比
~/mtpvocab/mia12_report.py 多轮 thorbench JSONL 拉平成对照表
~/mtpvocab/show_quant_recipe.py 打印本 checkpoint 的量化配方与 MTP 张量形态
logs/ctx_curve_clean_20260920.jsonl · logs/mia12-*.jsonl · logs/mia-k3.jsonl · logs/mia-code.jsonl · logs/ple_readbench-*.json 全部原始数据

最终账(相对今天之前的基线):短 ctx 聚合 +4~7%(c1 +7.4 / c4 +4.3 / c8 +6.7)· 每 token 磁盘读 −72.5% · KV 池 +4.2~5.8% · 可用内存 +3.8 GiB · 长 ctx 与改动前持平(±3%)· 代码/JSON 负载单流 57~63 tok/s、8 并发 247~252 tok/s。 三项未做:③(格式差异,客观拿不到)、④(镜像拒绝 fp8 KV)、MTP K=3(实测净亏)。

← 返回文章列表