模型结构深度解读 · 混合注意力 · 含 modeling 源码 + 技术报告消融 · Qwen4 预览架构

Qwen3.8-Flash-Next 逐层拆解 3× Gated DeltaNet + 1× Qwen Sparse Attention 的混合骨干

这是 Qwen 团队作为「未来 Qwen4 架构预览」放出的首个开源权重模型。它把此前 Qwen3-Next 的「Gated DeltaNet + 门控全注意力」骨干,替换成了「Gated DeltaNet + Qwen Sparse Attention(QSA)」,并配上 Gated Residual、N-gram Embedding、MTP 与 Muon/AdamW 混合优化器。本文按官方模型卡与 config.json 逐字段拆开,重点把注意力那一层——尤其是 QSA 的 indexer 与 micro-block 稀疏机制——讲到每个符号。

Qwen Team · Alibaba Group · 2026-08 · 125B 总参 / 6B 激活 · +51B N-gram Embedding · +4B MTP · 原生 262K,可扩到 1M
五句话讲完这个架构
125B
总参数
(不含 n-gram / MTP)
6B
每 token
激活参数
3 : 1
GDN 层 : QSA 层
混合比例
2048
QSA 每 query
注意力预算(token)
⚠ 关于这个模型的一句说明
Qwen3.8-Flash-Next 被官方定位为「支撑 Qwen4 的实验性架构预览」。本文所有结构、维度、超参数均严格来自其 HuggingFace 模型卡与 config.json,我把它当作权威一手规格来解读;文中凡属我基于公开机制(DeltaNet / Mamba2 / DeepSeek Sparse Attention 等技术线)做的推断与延伸,都会明确标注为「解读」而非原文断言。

01 全局总览:一张图看懂它长什么样

先把官方架构图放上来。这张图的信息密度很高,右侧是放大的「一个 Hybrid Block」内部,左侧是整体堆叠。

Qwen3.8-Flash-Next 架构图
图 1 · 官方架构图。输入 token → 词表 embedding,第 2 层额外叠加 N-gram Embedding;主干由 Hybrid Block × (L/4) 堆成,每个 block = 3× GDN Layer + 1× QSA Layer;每层内部是「(GDN 或 Qwen Sparse Attn) → MoE」,并用 Gated Residual(GR Write / GR Read)在加宽的残差流(Expanded Residual)上读写;顶部经 MTP 模块接 Prediction Head。来源:Qwen 官方模型卡。

config.json 里的关键数字对齐到这张图,就是下面这张「配置总表」。后面每一节会把其中一块拎出来单独拆。

模块关键配置数值(来自 config.json)
整体总参 / 激活 / 层数 / hidden125B / 6B / 48 层 / hidden_size=2560
词表vocab / n-gram / 输出248320 · n-gram 2000 万(ngram_size=3)· LM 输出 248320
层布局full_attention_interval=4每 4 层出现 1 层 full(=QSA),其余 3 层 linear(=GDN)
QSA(全注意力层)Q/KV 头 · head_dim · RoPE24 Q 头 / 2 KV 头(GQA)· head_dim=256 · 旋转维 64(partial_rotary_factor=0.25
QSA · Indexer结构 · 预算MQA 4 Q 头 / 1 KV 头 · indexer_head_dim=128 · 预算 512 块 / 2048 tokenindexer_budget=2048, 压缩比 4)
GDN(线性注意力层)V/QK 头 · head_dim · conv48 V 头 / 16 QK 头 · head_dim=128 · 短卷积核 linear_conv_kernel_dim=4
MoE专家数 · 激活 · 中间维512 专家,选 10 路由 + 1 共享 · moe_intermediate_size=640
Gated Residual分支 · 瓶颈秩4 分支 · hc_lowrank=320(bottleneck rank)
MTP层数 · 训练1 层,multi-step 训练(mtp_num_hidden_layers=1
上下文原生 · 可扩262144 原生,YaRN 扩到 1,000,000
视觉塔ViTdepth=27, hidden 1152, patch 16, 输出对齐 2560
💡 解读:为什么是「3:1」而不是「全稀疏」或「全线性」
线性注意力(GDN)把状态压成固定大小,擅长顺序建模但天然记不准细节;一旦任务需要「回头精确检索上文某个具体 token」,纯线性会掉链子。所以 Qwen 从 Qwen3-Next 起就用混合骨干:多数层用便宜的 GDN 扛住 O(N) 的长序列,少数层(这里是每 4 层 1 层)保留能精确寻址的全注意力做兜底。Qwen3.8-Flash-Next 的新意在于——把那 1/4 的「全注意力」进一步换成稀疏的 QSA,让兜底层本身也不再是 O(N²)。

02 层布局:把 48 层摊开看

config.json 里的 layer_types 数组是这个模型最诚实的「设计蓝图」——它逐层列出了每一层是 linear_attention(GDN)还是 full_attention(QSA)。规律非常干净:linear, linear, linear, full 每 4 层一循环,共 12 个循环 = 48 层。这正对应模型卡里写的 12 × (3 × (Gated DeltaNet → MoE) → 1 × (Qwen Sparse Attention → MoE))

L0
GDN Gated DeltaNet → MoE
L1
GDN Gated DeltaNet → MoE
L2
GDN Gated DeltaNet → MoE + N-gram Embedding 在此注入
L3
QSA Qwen Sparse Attention → MoE ← 第 1 个全注意力层
L4–6
GDN ×3
L7
QSA 第 2 个全注意力层
循环 12 次:L11, L15, L19, L23, L27, L31, L35, L39, L43 都是 QSA
L47
QSA 第 12 个(最后一个)全注意力层
MTP
MTP 1 层 full_attention,multi-step 训练(额外 ~4B 参数)
full_attention
_interval = 4
每隔 4 层放一个全注意力层。数组里 index 3, 7, 11, …, 47 是 full,其余是 linear。48 / 4 = 12 个 QSA 层。
ple_layer_ids
= [2]
N-gram Embedding(PLE, Parameter-Lookup Embedding)只在第 2 层注入。早注入,让局部 n-gram 语义尽早融进残差流。
mtp.hybrid
= true
MTP 头本身也是一个 full_attention 小层,独立 rope_theta=1e7,用于多 token 预测的训练时监督。
💡 解读:把「精确检索」层放在序列后段更划算吗?
QSA 层均匀分布(每 4 层一个),并没有像某些设计那样「全堆在顶部」。均匀交错的好处是:每经过 3 层便宜的 GDN 做完局部/顺序聚合后,就有一个能全局精确寻址的层来「校准」一次表示,信息不会在纯线性层里漂太久才被纠正。这与混合架构里「让全注意力层周期性充当锚点」的经验一致。

03 ★ 重点:Qwen Sparse Attention(QSA)—— 对照 modeling 源码逐行拆

这是全文的核心,也是这次基于官方 modeling_qwen4_exp.py 源码重写的部分。QSA 是 Qwen3.8-Flash-Next 相对 Qwen3-Next 最大的改动——把那 1/4 的「门控全注意力」换成了「块级稀疏注意力」。它落到代码里是两个类协作:Qwen4ExpTextQSAIndexer(选块、产出稀疏 mask)+ Qwen4ExpTextAttention(拿 mask 做门控注意力)。下面先立靶子,再照着 Qwen4ExpTextQSAIndexer.forward() 一行一行走

🔑 读源码得到的 3 个「与初版推断不同 / 更精确」的结论(先摆出来)
  • 打分不是普通点积,而是 relu(·)按 head 求和再 /√d。——初版我猜的是「max / 均值点积」,源码给的是 ReLU + sum-over-heads,这是一个刻意的非负、多头投票式打分。
  • 块的代表向量 = 该块 4 个 token key 的均值池化(mean-pool),再过 k_layernorm、再按块首 token位置打 RoPE。不是简单取压缩表示。
  • indexer 不 gather KV,而是产出一张布尔/加性 mask,叠加到主因果 mask 上,主注意力照常 dense 跑但被 mask 掐成稀疏。这解释了它为什么能直接复用标准 attention kernel。

3.1 先立靶子:标准因果全注意力,贵在哪

对每个 query 位置 t、每个 head h,标准注意力计算:

对第 t 个 query,要和它之前所有 i≤t 个 key 算内积、做 softmax、再对 value 加权求和。

逐符号拆开看它到底贵在哪:

qt, ki, vi
query / key / value 向量,维度 d_h。在 QSA 里 head_dim=256
⟨qt, ki
内积打分。对长度 N 的序列,一共要算 O(N^2) 个内积——这就是二次方成本的来源。
/ √dh
缩放,防止内积随维度增大而把 softmax 推向饱和(梯度消失)。
αt,i
归一化后的注意力权重,\sum_i \alpha_{t,i}=1。这是一个严格的概率分布——约束 Σα=1 不是「显然」,它来自 softmax 分母的归一化,正是它让输出是 value 的凸组合。
Σi≤t
因果掩码:只看过去。推理时要缓存所有历史 k,v(KV-Cache),显存随 N 线性增长、算力随 N² 增长。
✗ 关键痛点(为什么要稀疏)
当上下文拉到 26 万甚至 100 万 token,每生成一个新 token 都要对全部历史做一遍内积——prefill 的 O(N²) 和 decode 每步的 O(N) 都变得难以承受。agent 场景(多轮工具调用、长文档、代码库)恰恰把 N 推到极大。QSA 的目标:让每个 query 实际参与注意力的 token 数从「全部历史」降到一个固定的小预算。

3.2 数据流总览:indexer 产 mask,attention 吃 mask

先看 Qwen4ExpTextAttention.forward() 的头两行——它把整个 QSA 的调用关系摊开了:

selected_token_mask = self.indexer(hidden_states, position_embeddings, attention_mask, past_key_values) # 把稀疏 mask 叠加到主因果 mask 上(eager 用加法、sdpa 用按位与) if attention_mask.is_floating_point(): attention_mask = attention_mask + selected_token_mask else: attention_mask = attention_mask & selected_token_mask

所以 QSA = 「indexer 决定每个 query 能看见哪些 KV」→「把不该看的位置在 mask 里屏蔽掉」→「主注意力照常算,但只有选中的位置参与 softmax」。它没有 gather、没有搬运 KV——纯靠一张 mask 把稠密注意力掐成稀疏。下面把 indexer 逐段走完。

Step 1
投影 q / k
index_qk_proj 把 hidden 投成 4 个 indexer-query 头 + 1 个 key 头(各 128 维),q 过 RMSNorm + RoPE。
Step 2
分块 + 池化
可见 key 每 4 个(compress_ratio)切一块,块内 key 均值池化→k_layernorm→按块首位置打 RoPE,得块代表向量。
Step 3
打分 + Top-k
relu(q·kᵀ) 按 head 求和 /√d 得块分数,取 Top-512 块。
Step 4
块→token→mask
选中块展开回 token 下标,散射(scatter)成布尔 mask 返回。

Step 1 · 投影:一个 Linear 同时出 indexer 的 q 和 k

# __init__:注意 out 维度 = (4 + 1) × 128 self.index_qk_proj = nn.Linear(hidden_size, (index_n_heads + index_kv_heads) * index_head_dim, bias=False) self.q_layernorm = RMSNorm(index_head_dim); self.k_layernorm = RMSNorm(index_head_dim) self.block_topk = token_budget // compress_ratio # = 2048 // 4 = 512 # forward:一次投影,再劈成 q(4头) 和 token_k(1头) qk = self.index_qk_proj(hidden_states) q, token_k = torch.split(qk, [index_n_heads*index_head_dim, index_kv_heads*index_head_dim], dim=-1) q = self.q_layernorm(q.reshape(B, S, 4, 128)) raw_keys = token_k.reshape(B, S, 1, 128).squeeze(2) # 每个 token 一个 128 维 key q = apply_rotary_pos_emb(q, cos=current_cos, sin=current_sin) # indexer 自己的 RoPE
index_qk_proj
indexer 有自己独立的 q/k 投影参数,与主分支的 q_proj/k_proj 完全无关——它是一个「专门用来选块的迷你注意力」。out 维 = (4+1)×128 = 640。
q_layernorm
k_layernorm
q 和 key 各自过一个 RMSNorm(index_head_dim=128 上归一),稳定打分尺度。
block_topk
= 512
源码原话 token_budget // compress_ratio = 2048 // 4 = 512。这行代码就是「预算 2048 token = 512 块」换算的出处——每个 query 最多选 512 个块
4 Q / 1 K 头
indexer_n_heads=4, indexer_kv_heads=1:4 个 query 头共享 1 个 key 头(MQA 极致),打分成本极低。

Step 2 · 分块与池化:块代表 = 4 个 token key 的均值

对每个 (batch, query) 位置,取它可见的 key 下标,每 compress_ratio=4 个连续可见 token 组成一个「完整块」;不足 4 个的尾巴单独处理(见 Step 4)。块代表向量这样算:

block_token_indices = local_visible_indices[:n_blocks*4].view(n_blocks, 4) # 每行 4 个 token 下标 key_groups = raw_keys[b].index_select(0, block_token_indices.flatten()).view(n_blocks, 4, 128) pooled_keys = key_groups.float().mean(dim=1).to(dtype) # ← 块内 4 个 key 均值池化 pooled_keys = self.k_layernorm(pooled_keys) group_starts = block_token_indices[:, 0] # 块的“首 token”位置 block_key_states = apply_rotary_pos_emb(pooled_keys.unsqueeze(1), cos=full_cos[b].index_select(0, group_starts), sin=full_sin[b].index_select(0, group_starts)).squeeze(1)
B:块 B 的代表向量 = 块内 4 个 token 的 key 均值 → RMSNorm → 按块首 token 位置 p₀(B) 打 RoPE。用块首位置而非块中心,是个务实的近似(整块共享一个位置编码)。

Step 3 · 打分:ReLU 后按 head 求和 —— 这里和初版推断不同

scores = torch.matmul(q[b, qi].float(), block_key_states.float().transpose(-1, -2)).transpose(-1, -2) scores = torch.relu(scores).sum(dim=-1) / math.sqrt(self.index_head_dim) selected_block_indices = scores.topk(min(self.block_topk, n_blocks), dim=0).indices
逐符号:i(h) 是第 i 个 query 的第 h 个 indexer 头(共 4 个);和块代表 B 做内积,先 ReLU 把负相关截成 0,再对 4 个 head 求和,最后 /√d 缩放。得到每个块一个标量分数 si,B,取分数最高的 512 个块。
relu(·)
非负打分。只有「正相关」的头才为这个块投票,负相关被截断为 0——避免负项互相抵消,让打分更像「有多少个头觉得这块相关」的投票强度。这是源码的真实设计,初版我误写成 max/均值点积,此处已按代码更正。
sum over 4 heads
4 个 indexer 头的分数直接相加(不是取 max、不是平均)。多头在此充当「多个独立评委」,票数累加。
/ √d (d=128)
用 indexer 的 head_dim 而非主分支的 256 做缩放。
topk(min(512,
n_blocks))
块数不足 512 时全选。预算是「上限」,短序列自然不受限。

Step 4 · 块→token→mask:把选中块散射成布尔掩码

selected_tokens = block_token_indices.index_select(0, selected_block_indices).flatten() # 块展开回 token tail = local_visible_indices[n_blocks*4:] # ← 不足一块的尾巴:无条件保留 selected_tokens = torch.cat([selected_tokens, tail]) ... scatter_indices = torch.where(selected_token_indices >= 0, selected_token_indices, kv_length) # -1 占位丢到末列 selected_token_mask = zeros(..., kv_length+1, bool).scatter(-1, scatter_indices, True)[..., :kv_length] if attention_mask.is_floating_point(): # eager:布尔→加性 float mask selected_token_mask = torch.where(selected_token_mask, 0.0, min_dtype) # 未选中 = -inf
块→token 展开
选中的是「块」,但主注意力按 token 算,所以把每个选中块的 4 个 token 下标全部展开,得到实际可见 token 集合。
tail 尾块
无条件保留
不足 compress_ratio(4) 的最后几个可见 token 永远保留,不参与打分竞争。通常是最近的、离 query 最近的 token——保证「近处上文」不会被稀疏掉。
scatter 成 mask
-1 做占位符,散射时把 -1 全丢到「第 kv_length 列」再切掉,干净地得到布尔可见矩阵;eager 模式再转成 0 / −inf 的加性 mask
叠加主 mask
返回后与主因果 mask 相加(或按位与):既满足因果、又只保留选中块——主注意力 softmax 时,未选中位置权重被 −inf 压成 0。
💡 为什么「产 mask」而不是「gather KV」是聪明的工程选择
产 mask 的最大好处:主注意力 eager_attention_forward 完全不用改——它就是标准的 softmax(QKᵀ·scaling + mask)·V,mask 里已经包含了稀疏信息。这样 QSA 能直接复用现成的 attention kernel(含 SDPA),不需要为「选中块」写专门的 gather-attention kernel。代价是:训练/短序列时它逻辑上仍算了全部 QKᵀ(只是 mask 掉),真正的省算要靠推理引擎(vLLM/SGLang)识别 mask 结构做块稀疏 kernel。换句话说:这份 modeling 是「参考实现」,重在语义正确与可复用;生产提速靠 serving 引擎的稀疏 kernel。

3.3 主分支:Gated Attention(源码里的输出门)

indexer 给完 mask,主注意力还有一个初版没细讲的细节——输出门控。看 Qwen4ExpTextAttentionq_proj 的输出宽度是 ,劈成「真正的 query」和「门 gate」:

self.q_proj = nn.Linear(hidden_size, num_attention_heads * head_dim * 2, bias=False) ... query_states, gate = torch.chunk(self.q_proj(hidden_states).view(*shape, -1, head_dim*2), 2, dim=-1) query_states = self.q_norm(query_states...) # QK-Norm:q/k 各过 RMSNorm key_states = self.k_norm(self.k_proj(...)...) query_states, key_states = apply_rotary_pos_emb(query_states, key_states, cos, sin) # partial RoPE 64 维 attn_output = attention_interface(self, q, k, v, attention_mask, scaling=head_dim**-0.5) # 带稀疏 mask attn_output = attn_output * torch.sigmoid(gate) # ← 逐元素输出门控 attn_output = self.o_proj(attn_output)
q_proj × 2
Gated Attention:query 投影多出一半宽度当门。注意力输出 o 再乘 sigmoid(gate) ——逐元素、数据相关地缩放每个通道,等价于给注意力输出加了个 SiLU/GLU 式的自门控。
q_norm / k_norm
(QK-Norm)
q、k 在 head_dim=256 上各过 RMSNorm 再算注意力——稳定长上下文下的注意力分数尺度(QK-Norm,近年大模型标配)。
num_key_value
_heads = 2
激进 GQA:24 Q 头分 2 组,每 12 Q 头共享 1 组 KV。KV-Cache 缩到 2/24 ≈ 1/12repeat_kv 在算之前把 KV 复制回 24 头)。
scaling
= 256^-0.5
主分支缩放用 head_dim=256(区别于 indexer 的 128)。

partial RoPE:只旋转前 64 维

apply_rotary_pos_embrotary_dim = cos.shape[-1],只对 q/k 的前 rotary_dim 维施加旋转、后面维度原样拼回:

q_rope, q_nope = q[..., :rotary_dim], q[..., rotary_dim:] # rotary_dim = 256×0.25 = 64 q_rope = (q_rope * cos) + (rotate_half(q_rope) * sin) q_rotated = torch.cat([q_rope, q_nope], dim=-1) # 前 64 维带位置,后 192 维“无位置内容通道”
partial_rotary
_factor = 0.25
256 × 0.25 = 64 维旋转(对应模型卡「RoPE Dimension: 64」),其余 192 维不带位置——一部分容量专注内容匹配、不被位置扰动。
rope_theta = 1e7
大基频拉长旋转波长,原生支持 26 万上下文的前提。
mrope_section
=[11,11,10]
M-RoPE(多模态 RoPE,interleaved):32 个旋转对分给 时间/高/宽(11+11+10=32=64/2),同时表达文本序号与图像/视频坐标。apply_interleaved_mrope 负责交织。
YaRN factor=4
扩 100 万上下文时把 rope_typeyarnfactor=4。官方提醒 YaRN 是静态缩放,略伤短文本,只在需要时开。
⚠ 关于「稀疏选择不可导」——技术报告给出了明确答案
Top-k 选块确实不可导,但技术报告披露了完整训练方案(见下 3.4):indexer 不靠反传注意力梯度,而是用一个KL 蒸馏损失,让 indexer 的块打分去逼近主注意力的真实分布。上一版这里我只能"按技术线推断",现在按报告原文更正为确切做法。

3.4 ★ Indexer 到底什么时候、怎么训练(技术报告 §2.1.2)

这是你特别关心的问题。技术报告给的答案很清楚:QSA 不是从头预训练的——它在「继续预训练(CPT, Continued Pre-Training)」阶段引入,把一个已经用全注意力训好的骨干,改造成稀疏注意力。整个过程分两大阶段(论文正文)/ 落地时细分为三步(含 warm-up),序列长度全程 256K

起点
全注意力骨干
先有一个用标准 full attention 训好的模型(backbone + MTP 里的全注意力层)。
Stage 1
Dense Distillation
(warm-up)
只训 indexer,冻结骨干。用全注意力的真实分布蒸馏 indexer 打分。1,000 步,lr=1e-3,≈2B token。
Stage 2
Sparse Training
(联合)
indexer + 整个骨干联合训,让骨干适应稀疏模式。8,000 步,lr=2.5e-5,≈200B token。

Stage 1 · Dense Distillation:把「全注意力该看哪」教给 indexer

核心思路:老师是全注意力自己算出的注意力分布,学生是 indexer 的块打分。三步构造老师信号:

① 汇总多头
把某个 query i 对所有 token j 的注意力权重,在所有 teacher head 上求和再做 L1 归一化,得到 token 级老师分布 a_i ∈ ℝⁿ
② MaxPool 到块级
(注意:不是均值!)
关键细节:老师分布用 MaxPool 池化到块级——ā_{i,b}=\text{MaxPool}(a_i, p_b:p_b+r-1),再 L1 归一。用 max 而非 mean 是为了保住块内最显著 token 的信号,防止一个强相关 token 被同块的 3 个弱 token 平均稀释掉。(对比:indexer 的块 key 表示用的是 AvgPool——两处池化目的不同,别混。)
③ KL 蒸馏
最小化 L_{KL}=\frac1N\sum_i D_{KL}\big(\hat a_{i,:}\,\|\,\text{Softmax}(I_{i,:})\big):让 indexer 打分经 softmax 后逼近老师的块分布。只在完整块上算。
为什么先只训 indexer?因为 indexer 是新加的、随机初始化的模块,若一上来就让骨干在它(乱选的块)上做稀疏注意力,会严重掉点。先花 1000 步、2B token 把 indexer「教会看哪」,是廉价的初始化。

Stage 2 · Sparse Training:骨干去适应稀疏

真·稀疏前向
这一阶段 indexer 真的去选 Top-K_B 块,展开成 token 集合 S_i(并入尾块 token),主注意力只在 S_i 上算——和推理时完全一致。
KL 只在选中块上
此阶段 KL 损失只在 indexer 选中的 Top-K_B 块上计算,且老师概率在这些块内重新归一到 1(Eq. 20)。既继续校准 indexer,又不再管没选中的块。
联合训练规模
8,000 步、lr 2.5e-5、每步 96 条 256K 序列 ≈ 200B token。(Stage 1 是 8 条/步、lr 1e-3、2B token。)
融合 kernel
实现了 fused QSA kernel:稀疏注意力输出和 KL 损失一次算完、不落中间结果,大幅省显存。
📊 报告实测:稀疏几乎不掉点,长上下文反而更强
  • 训练 loss 几乎重合:Stage 2 里 QSA 与全注意力基线的 LM-loss 差异仅 ~10⁻⁴ 量级(报告 Fig.4)。
  • 短上下文 8 项评测:QSA 在 8 个里赢 7 个,平均分 75.9 → 76.8(不降反升)。
  • 长上下文更亮眼:RULER 512K–1M 段 90.08 → 93.00;MRCR 8-needle 在 512K 30.66 → 40.53、1M 20.71 → 26.44。稀疏在超长上下文上反超全注意力(稀疏起了降噪/聚焦作用)。
  • kernel 级加速(1M 上下文):注意力模块 prefill 7.6×、decode 4.9×;单看 indexer(压缩比 4)prefill 3.8×、decode 4.4×

消融:为什么是「压缩比 4、4 个 indexer 头」

压缩比 r=4
报告在 35B-A3B 上扫了 block size 2/4/8/16:压缩比 4 时在「相对 indexer 延迟 0.25」处就追平全注意力 RULER;对比跨层共享索引的 IndexShare 在 0.5 延迟都还没追平——层内压缩比跨层共享更适合混合架构(GDN+QSA 交错,层间相似度低,跨层共享吃亏)。
4 个 query 头
扫了 1/2/4/16 头:4 头就已接近全注意力,再多收益甚微。报告明说「直接用 Stage 1 后的 indexer 做稀疏会明显掉点,短暂 Stage 2 联合训后即可恢复到全注意力水平」——印证了两阶段的必要性。最终选 4 头平衡速度与精度。
MTP 复用索引
MTP 的注意力层也换成 QSA,并跨投机解码步复用同一份 top-k 索引(借鉴 GLM)。4 步投机解码实测:mean accepted length 基本不变(4.06 → 4.07),说明复用索引不伤 MTP。
💡 一句话把 QSA + 训练串起来
QSA = 一个自带迷你 MQA 打分器(indexer)的门控 GQA,且这个打分器是"蒸馏"出来的。先用全注意力训好骨干;再花 1000 步、冻结骨干、用「全注意力分布 MaxPool 到块级」当老师,KL 蒸馏教会 indexer「该看哪些块」;最后 8000 步让骨干+indexer 联合适应稀疏。indexer 用「4 头 ReLU 投票」给每 4 个 token 一块打分、选 Top-512 块、散射成稀疏 mask;主分支吃 mask 只在选中块上做精确 softmax。工程精髓仍是——用 mask 而非 gather 实现稀疏、免写专用 kernel;而"能不掉点"靠的是 KL 蒸馏 + 短暂联合训这套 CPT 配方。

04 另外 3/4 层:Gated DeltaNet(线性注意力骨干)

QSA 讲透了,再看承担 3/4 层的 GDN。它是 Qwen3-Next 就在用的线性注意力,核心是「用一个固定大小的状态矩阵 S_t 概括全部历史」,从而把复杂度从 O(N²) 降到 O(N)。它的递推式把「自适应遗忘」和「定点改写」合二为一:

St:t 时刻的记忆状态矩阵(固定大小,不随 N 增长)。整条式子=先「淡忘 + 擦掉旧的、写入新的」,再产出。
αt ∈ (0,1)
标量门控(来自 Mamba2 思路):自适应遗忘。α→1 记得牢,α→0 快速淡忘整块旧记忆。数据相关,逐 token 决定。
I − βtktkt
Householder 型的 delta 规则(来自 DeltaNet):定点擦改。这个投影只在 kt 方向上修正记忆——「把旧的关于 kt 的内容擦掉」,实现误差修正式的精确改写,而非线性注意力常见的「只会累加」。
βt
写入强度 / 学习率,控制这次改写的力度。
vtkt
外积,把新的 (key→value) 关联写进状态。
linear_num
_value_heads=48
48 个 value 头
linear_num
_key_heads=16
16 个 QK 头(V 头比 QK 头多,48 vs 16,是一种头维度上的不对称设计)。
linear_*_head_dim
=128
K/V head 维度均 128。
linear_conv
_kernel_dim=4
GDN 前有个 核长 4 的短因果卷积,先做局部混合(类 Mamba 的 short conv),再进线性注意力递推。
🔗 已有专文:这部分我们之前拆得更细
Gated DeltaNet 的完整数学(从 softmax→线性注意力→DeltaNet→Mamba2→GDN 的技术线、WY 表示与 chunk 并行训练)在知识库另有一篇专文:《Gated DeltaNet:会精确改写记忆的线性注意力》。本文只把它作为「3/4 层的骨干」定位,不重复展开。
💡 GDN 与 QSA 的分工,一句话
GDN 是「压缩的、顺序的、O(N) 的工作记忆」——擅长顺着序列往下捋、维护一个不断被改写的状态;QSA 是「稀疏的、可寻址的、精确检索」——需要回头精确翻某段上文时靠它。两者相乘式互补,正是混合架构长上下文既快又准的原因。

05 Gated Residual:让残差流「可读可写」

残差连接(residual + norm)是深层 LLM 能训起来的基础。Qwen3.8-Flash-Next 在此之上做了 Gated Residual:在一条加宽的(expanded)残差流上,用数据相关的门控来调制信息进出。架构图里的 GR Write / GR Read 就是它。

📖
读门(GR Read)
逐元素、数据相关的 read gate:决定当前层从加宽残差流里「读多少、读哪些通道」。element-wise 意味着每个维度独立开关,粒度很细。
✍️
写门(GR Write)
每分支一个标量的 write gate:控制这一层的计算结果以多大权重写回残差流。per-branch scalar 保持稳定、开销极小。
hc_count = 4
4 条分支(branches):加宽的残差流被组织成 4 路,Gated Residual 分别在这 4 路上读写。
hc_lowrank
= 320
瓶颈秩 320:门控不是在全维 2560 上直接算,而是先降到 320 维的低秩瓶颈再算——省参省算,同时够表达。
output_gate
_type=sigmoid
输出门用 sigmoid(0~1 的软开关)。
💡 解读:为什么「读门逐元素、写门标量」这样不对称?
读的时候需要细粒度地挑通道(哪些特征此刻有用是高度数据相关的),所以用 element-wise 向量门;写的时候若也逐元素,会引入大量参数并可能破坏残差流的尺度稳定性——所以写门退化成每分支一个标量,只调「整体写入强度」,既保住深层训练的稳定性,又几乎不加推理开销。这就是模型卡说的「finer-grained expressiveness … while preserving training stability and keeping inference overhead low」。

06 N-gram Embedding:用「查表」换「算力」的第三条扩展轴

这是很有意思的一块:51B 参数几乎全砸在一张巨大的 n-gram 查找表上,但它几乎不增加计算量。

2000万
n-gram 词表大小
(bigram/trigram)
51B
n-gram embedding
参数量
3
ngram_size
(最长 trigram)
L2
注入层
(ple_layer_ids)

机制:把输入里连续的 2~3 个 token 拼成一个 n-gram,用它去索引一张 2000 万行的 embedding 表,取出的向量加进第 2 层的表示。

索引,不是矩阵乘
MoE 扩容量要「路由 + 算专家」,n-gram 扩容量只需一次查表(gather)——计算量几乎为零,参数却能堆到 51B。报告用「300 tokens-per-active-param(TPP)」的口径做所有 n-gram 实验。
易 offload
(可异步预取)
这张大表确定性寻址(给定 n-gram 就知道取哪一行)、访问稀疏,所以可以放到 host 内存/慢速存储里,用异步预取提前把要用的行搬上加速卡。放在 Layer 2 正是为了让预取和第 1 层的计算重叠,把访存延迟藏掉。
heads_per_ngram
=8, split=128
工程细节:每个 n-gram 表项切成 8 个头、词表按 128 分片,兼顾查表效率与分布式存储。

6.1 ★ N-gram 到底有没有用?——技术报告的三组消融(这是关键)

你特别问了「n-gram 的整体效用」。技术报告在这块做了很扎实的消融,结论比"有用/没用"微妙得多,可以拆成三个层次:

① 加不加?——加了明显有用

固定 n-gram 总参数、只看「有 vs 无」,加一层 n-gram embedding 把 loss 从 1.585 降到 1.541,下游平均分 45.44 → 47.94(约 +2.5)。GSM8K 从 59.21 涨到 64.00、MATH 从 32.52 涨到 37.32——加 n-gram 是净收益,没有争议。

② 放哪层 / 放几层?——一层就够,浅层略优

放置位置Loss下游 Avg.结论
不加1.58545.44基线
第 1 层1.54147.30浅层强
第 2 层 ★采用1.54147.94最佳,且能与 L1 计算重叠预取
第 15 层1.54347.37中层也可
第 2 + 25 层1.54047.75loss 微降但下游没更好

报告结论:「没有哪个深度一致胜出;把同样的参数摊到多层没有稳定收益;一层 n-gram embedding 就够了。」而且不同放置在全注意力和 GDN 下表现相似,说明放置对注意力机制不敏感。最终放 Layer 2,主要是为了预取能和第 1 层计算重叠这个工程理由。

③ 词表越大越好吗?——loss 单调降,但下游会饱和(最重要的洞察)

N-gram 词表规模LossC-Eval(中文)CMMLU(中文)下游总体
不加1.58566.9168.10基线
20× 词表1.55371.7572.29大涨
50× 词表1.54172.1272.48继续涨
100× 词表1.53473.7572.73loss 还在降
200× 词表1.52674.9473.24loss 最低,中文最强
📊 报告最核心的结论:loss 与下游准确率「不同步」
把 n-gram 词表从 20× 放大到 200×,训练 loss 单调下降(1.553 → 1.526),但下游 benchmark 却会饱和或波动,并不同步变好。这被报告反复强调(连摘要都写了):
  • 中文任务是例外——持续受益:C-Eval / CMMLU 随词表增大一致上升(C-Eval 66.9→74.9)。这符合直觉:中文里高频固定词组(成语、专名、搭配)极多,正是 n-gram「背下来」最划算的地方。
  • 固定总参预算下,n-gram 和 MoE 角色不同:另一组实验把 n-gram 词表做大、同时砍 MoE 专家来保持总参不变——结果 loss 在 10×(25%) 处最低(非单调),但 out-of-domain 的 uncheatable PPL 几乎不变、下游相比纯 MoE 没明显提升。报告据此判断 「n-gram embedding 和 MoE 专家在扩容量上扮演不同角色」,不能简单互相替代。
  • 省参尝试基本无效:报告还试了 token 归一化压词表、按 n-gram 阶非均匀分配、按频率分桶等提效手段,在他们的配方里都没带来稳定收益——如实记录了负结果。
💡 综合解读:N-gram 的「效用」到底是什么
把三组消融合起来看,n-gram embedding 的真实定位是:一条用「记忆」换「计算」的正交扩展轴,尤其擅长记住高频/固定搭配(对中文格外划算),且几乎零算力、可 offload。但它不是万能容量——(1) 它降 loss 的能力强于降下游误差(说明它主要吃下了「可背诵」的那部分不确定性,而非提升真正的推理/泛化);(2) 它与 MoE 互补而非替代,各扩一类容量;(3) 一层、放浅层即可,堆多层/压词表都没用。所以生产配置就落在 2000 万词表(约 50× 量级)、放 Layer 2、51B 参数全 offload 这个「够用又省」的点上,而不是一味把词表往大堆。

07 MoE、MTP 与训练配方

7.1 MoE:512 专家,选 10 路由 + 1 共享

num_experts
=512
每个 MoE 层有 512 个专家(细粒度专家,intermediate 维只有 640,很「瘦」)。
experts_per_tok
=10
每个 token 路由到 10 个专家
+1 shared
外加 1 个共享专家(所有 token 都过),承接通用能力,让路由专家专注差异化。10+1 是激活的专家数。
aux_loss_coef
=0.001
负载均衡辅助损失系数 0.001,防止专家塌缩(少数专家被抢光)。→ MoE 负载均衡的数学专文

「512 个瘦专家、每 token 选 10+1」是典型的细粒度 MoE:专家越多越小,组合数越大、专精度越高,是 6B 激活量却有 125B 总参的直接原因。

7.2 MTP:多 token 预测

顶部挂一个 1 层的 MTP(Multi-Token Prediction)模块(约 4B 参数,multi-step 训练)。训练时不止预测「下一个 token」,还预测再往后几个,给模型更密的监督信号、提升样本效率;推理时可用作投机解码的草稿头,加速生成。

7.3 训练配方:Muon + AdamW 分工,砍掉 batch warmup(技术报告 §3)

⚙️
Muon 只给「真·线性映射」用
Muon 用在真正当线性算子的二维权重:attention 的 q/k/v/o、GDN 输入输出投影、MoE 路由/共享专家的 fc1/fc2、n-gram 的 k/v 投影。输入 embedding、输出头、MoE 路由器、GR 的两个低秩投影、GDN 的 decay/beta、输出门都留给 AdamW(路由器各维独立、无共享线性结构可正交化;低秩投影太"扁")。→ Muon vs AdamW 专文
✂️
融合矩阵先拆再正交化
Megatron 里 qkv、SwiGLU 的 fc1、GDN 输入投影是融合成一个大矩阵存的,但语义上是多个独立算子拼接。直接对融合矩阵做 NS 正交化会混淆不相关子块的奇异方向。所以按 per-head / gate-up 粒度拆开、各自正交化再拼回——loss 和下游都更好。用自研 Canzona 解决 Muon 与 TP/DP 分片冲突。
📈
重拟合 scaling law → 更大 batch/lr
新架构 + Muon 训得更稳,于是重拟合 scaling law,预测更大的 batch 和学习率(且 lr 随模型增大衰减更慢)。10.8B-A0.89B / 4T token 上验证:从旧 B=12.6M 到预测最优 B=25.2M,loss 改善 7.2×10⁻³;再大到 37.7M 反而微降。
🚀
Batch-size warmup 不再需要
传统大模型早期会 ramp batch。但 Muon 在大 batch 下保数据效率、且 MoE 大 batch 让每个专家拿到更足信号。实测两种 ramp 方案都没变好(差 2.5~3.5×10⁻⁴),还多花 18.8% 优化步数。所以直接从目标大 batch 起步,不做 warmup。

08 评测:6B 激活能打到什么位置

模型卡给出的语言能力横评(对比 Qwen3.8-27B 稠密、Qwen3.7-Plus 397B MoE、DeepSeek-V4-Flash、Claude-Opus-4.6)。Qwen3.8-Flash-Next 只激活 6B,却在多数项领先,尤其 agentic 类。

能力 / BenchmarkFlash-Next
(6B act)
Qwen3.8-27B3.7-Plus
(397B)
DS-V4-FlashOpus-4.6
Agentic coding · DeepSWE 1.158.742.216.554.4
SWE-bench Pro62.561.755.856.053.4
SWE-bench Multilingual81.073.875.877.5
Long-horizon office · CoWorkBench73.970.765.145.168.2
JobBench(职业任务)55.733.427.641.336.6
Real-world tool use · Toolathlon73.567.150.670.3
指令遵循 · IFBench81.379.579.179.262.5
科学推理 · GPQA Diamond91.789.290.390.891.3
竞赛编程 · LiveCodeBench v691.990.389.690.688.8
多学科推理 · HLE35.930.834.733.840.0
JobBench(职业任务)· Flash-Next 领先第二名 +14.455.7
Agentic coding DeepSWE · vs 同族 27B 稠密(42.2)58.7
指令遵循 IFBench · 大幅甩开 Opus-4.6(62.5)81.3
⚠ 看这张表要抓的两个「呈现细节」
(1) 激活量不对等。Flash-Next 只激活 6B,对手多是 13B~27B 甚至 397B 总参——「小激活打平大模型」是它的卖点,但也意味着单看某个绝对分数不能直接等同「更强」,要连着激活成本一起看。(2) 评测口径有脚注。SWE-bench 系用 temp=1.0/top_p=0.95/256K harness、HLE 由 GPT-4o 评判、MathVision 修正过标注——横评公平性依赖这些口径一致,脱离脚注比大小会被误导。

09 落地要点与真实反馈

🧠
默认开思考 + 保留思考
默认 thinking mode,且 preserve_thinking=true——保留全对话历史的思考块,利于 agent 决策一致性,还能提升 KV-Cache 复用率。可用 reasoning_effort(xhigh/medium/low)调。
📏
输出长度要给足
官方建议 1M 上下文内:推理内容上限 262K token、最终回答上限 131K token。给足空间才不会在复杂 agent 任务里被截断。
🔧
推理框架
SGLang / vLLM / TokenSpeed 均有官方 recipe。生产/高吞吐强烈建议用专用引擎,而非裸 Transformers。
🎛️
采样参数分两套
思考模式 temp=1.0/top_p=0.95;非思考 temp=0.7/top_p=0.80/presence_penalty=1.5。用错会明显掉效果或语言混杂。
💡 结合我们自己的痛点:QSA 对「可预测 prefill 上界」的意义
QSA 把每 query 的注意力预算钉死在 2048 token(512 块)——无论上下文 10K 还是 1M,主分支精算的 KV 量恒定。这带来一个工程上极有价值的性质:prefill 的注意力算力有一个可预测的上界,不再随上下文平方爆炸。对多轮 agent 请求里 KV-Cache 跨机命中难、请求漂移导致 cache miss 后重算 prefill、TTFT 飙升的问题,这类「预算恒定」的稀疏注意力比全注意力更容易做前缀缓存与路由的容量规划。(此为结合实践的延伸解读,非官方结论。)
⚠ 真实反馈现状:一手评测尚少
截至整理时,Qwen3.8-Flash-Next 作为「Qwen4 预览架构」刚放出,社区(小红书/知乎/Reddit)尚无大量深度实测沉淀,官方也把不少 QSA/训练细节留给了尚未完全公开的技术报告。因此本文以模型卡 + config.json 的一手规格为准,对训练细节(如 indexer 如何反传)明确标注为技术线推断。等技术报告与社区实测出来后,本页会补一节「真实反馈」。