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 |
三条结论
- 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 秒。
- decode 全程平 39~43 tok/s,没有"长上下文悬崖"。KV 从 2K 涨到 244K(122 倍),decode 只波动 ±5% ⇒ 瓶颈是权重搬运,不是 KV 长度 —— 这个模型的注意力是压缩+稀疏的(QSA + indexer)。
- 无周期性卡顿: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 逐字节一致 |
今天真正贵的三个教训
- 新容器首轮数据一律不作数(JIT 冷启):c8 167.4 → 190.8,c1 41.7 → 49.7,差 12~15%。今天被这个坑咬了三次。
- 改多行续行命令后必须逐行核
\结尾:给docker run插入一行时留下一个空行 → bash 把命令在空行处结束 →docker: 'docker run' requires at least 1 argument→ 容器被rm掉且没重建。救命流程:bash -n→ 干跑(sed 's/^docker run -d/echo DRY -d/')数 argv →awk查块内不以\结尾的行 → 编辑前留一份逐字节可用的副本。 - 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(实测净亏)。