
Qwen3.5 这个模型、在"事后改门"这个操作方式下:头级一个共享门已经够用,甚至是被模型主动选中的。证据就是权重统计里同头 128 个通道数值全等------模型拿着 128 份自由度却只用出 1 份,说明主干的表征结构本来就按"整头一起动"组织。
所以通道级改造的收益 ≈ 0 还要倒贴过拟合风险,"多门收益不大"在这条线上是被三个证据共同判死的。
2026-10 | 硬件:AMD Radeon AI PRO R9700(gfx1201,32GB)+ WSL2
一句话:利用"GDN-2 的两个门收拢成同一个 β 时数学上精确退化为 GDN-1 "这一性质,
我们对 Qwen3.5-2B 做了零损失换核手术 ,然后用真语料把"事后解耦到底有多少收益"
这个问题一路测到了底。结论是一个有价值的负结果,全部过程可复现。
文章目录
-
- [0. 这篇文章讲什么](#0. 这篇文章讲什么)
- [1. 理论基础:为什么能"零损失"换核](#1. 理论基础:为什么能"零损失"换核)
- [2. 环境:让 AMD 卡在 WSL2 里跑起 FLA 的 Triton 内核](#2. 环境:让 AMD 卡在 WSL2 里跑起 FLA 的 Triton 内核)
- [3. 实验一:零损失换核(等价性验证)](#3. 实验一:零损失换核(等价性验证))
- [4. 实验二:真语料上的头级解耦 ------ 域外 -4.2%](#4. 实验二:真语料上的头级解耦 —— 域外 -4.2%)
- [5. 实验三:逐通道门 ------ 一发判停](#5. 实验三:逐通道门 —— 一发判停)
- [6. 踩坑合集(AMD + WSL2 + 长训,每条都是真实事故)](#6. 踩坑合集(AMD + WSL2 + 长训,每条都是真实事故))
- [7. 保留了什么、下一步](#7. 保留了什么、下一步)
0. 这篇文章讲什么
Qwen3.5 是混合架构:24 层里 18 层 Gated DeltaNet(GDN,线性注意力)+ 6 层全注意力 ,
3:1 交替。另一边,NVIDIA 的 GDN-2 (Decoupling Erase and Write in Linear Attention )
把 GDN-1 里"擦除和写入共用一个标量门 β"改成了独立的擦除门 b 和写入门 w ,从头预训练
的收益已经得到验证。
那么一个自然的问题:能不能不动预训练权重,把一个量产模型里的 GDN-1 直接"换芯"成
GDN-2? 如果换完的第 0 步和原模型功能完全一致,之后再只训新加的自由度,就能得到一个
绝对干净的消融:事后解耦到底值多少?
答案分三段:换芯可以,而且是逐位等价的(ppl 差 0.02%);头级解耦能拿到域外 -4.2%;
把门开到逐通道(参数放大 250 倍),域外一分不涨 ------瓶颈不在门的开数,在主干是
"耦合门"下预训练的。按我们训练前定下的判停标准,这条线关闭。
1. 理论基础:为什么能"零损失"换核
两个记忆核的更新式(S ∈ R^{K×V} 为循环记忆状态):
S t G D N 1 = ( I − β t k t k t ⊤ ) d i a g ( e g t ) S t − 1 + k t ( β t v t ) ⊤ S_t^{GDN1} = (I - \beta_t k_t k_t^\top)\, \mathrm{diag}(e^{g_t})\, S_{t-1} + k_t (\beta_t v_t)^\top StGDN1=(I−βtktkt⊤)diag(egt)St−1+kt(βtvt)⊤
S t G D N 2 = ( I − k t ( b t ⊙ k t ) ⊤ ) d i a g ( e g t ) S t − 1 + k t ( w t ⊙ v t ) ⊤ S_t^{GDN2} = (I - k_t (b_t \odot k_t)^\top)\, \mathrm{diag}(e^{g_t})\, S_{t-1} + k_t (w_t \odot v_t)^\top StGDN2=(I−kt(bt⊙kt)⊤)diag(egt)St−1+kt(wt⊙vt)⊤
GDN-2 的 b(键轴擦除门)、w(值轴写入门)都是逐通道向量。关键性质:
当 b t = w t = β t b_t = w_t = \beta_t bt=wt=βt(同一个标量广播到所有通道)时,GDN-2 更新式逐项等于 GDN-1。
这意味着手术可以这样做:
- 保留原模型全部权重(q/k/v 投影、因果卷积、衰减参数 A_log/dt_bias、out_proj...);
- 新增写入门投影
in_proj_w,逐字复制原in_proj_b的权重 → 初始w ≡ β; - 擦除门 b 直接复用原 β,衰减 g 复用原标量广播到通道。
换核第 0 步与原模型功能等价 ;训练时两份参数的梯度自然分化,才实现真正的解耦。
这套"复制初始化" later 还能推广到逐通道(见 §4),等价性同样严格成立。
2. 环境:让 AMD 卡在 WSL2 里跑起 FLA 的 Triton 内核
我们的 GDN-2 快核来自 flash-linear-attention (fla),
但 fla 的高速路径全靠 Triton------而 Windows 原生没有 Triton(更没有 ROCm 版) 。
路线是 WSL2 + AMD 新的 ROCDXG 方案(不需要装 Linux 版驱动):
bash
# WSL2 Ubuntu-24.04 内,GPU 现形的关键三步
wget https://github.com/ROCm/librocdxg/releases/download/v1.2.0/rocdxg-roct_1.2.0_amd64.deb
sudo dpkg -i rocdxg-roct_1.2.0_amd64.deb # 配 ROCm 7.2.x
export HSA_ENABLE_DXG_DETECTION=1 # ROCm < 7.13 必需
export ROCPROFILER_REGISTER_ENABLED=0 # 不设 → import torch 直接 core dump!
rocminfo | grep gfx # → gfx1201, AMD Radeon AI PRO R9700
bash
pip install torch --index-url https://download.pytorch.org/whl/rocm7.2 # 2.14.1+rocm7.2
pip install flash-linear-attention # 0.5.2, triton-rocm 3.8.0
pip install "git+https://github.com/huggingface/transformers.git" # PyPI 最新 5.19 还没有 qwen3_5
三个非显而易见的坑,都是实测撞出来的:
| 坑 | 症状 | 解法 |
|---|---|---|
| rocprofiler 注册 | import torch 即 core dump (Found 0 rocprofiler agents and 2 HSA agents) |
ROCPROFILER_REGISTER_ENABLED=0 |
| fla 反向核 dtype | 训练时 Both operands must be same dtype. Got bf16 and fp32 |
门张量 .contiguous().to(q.dtype),核内部自升 fp32 |
| fla 0.5.2 的 naive 参考 | gated_delta_rule 的 naive 在 K==V 时 β 静默错位(对账时别信它) | 用 fla.ops.gdn2 自带的 naive(正常),或旧版 vendored 参考 |
换上的 chunk_gdn2 快核先和 naive 逐位对账:max|diff| ≈ 4e-6,然后才敢用它做手术引擎。
3. 实验一:零损失换核(等价性验证)
手术层约 100 行,核心就是换掉 Qwen3_5GatedDeltaNet.forward 里的记忆核调用:
python
# 初始化:新写入门逐字复制 in_proj_b(bias=False)
self.in_proj_w = nn.Linear(hidden, num_v_heads, bias=False)
self.in_proj_w.weight.data.copy_(gdn1.in_proj_b.weight)
# forward 里,只换记忆核这一个调用:
beta = torch.sigmoid(gdn1.in_proj_b(h)) # 擦除门(继承原参数)
w = torch.sigmoid(self.in_proj_w(h)) # 写入门(新,初始 ≡ β)
g = -gdn1.A_log.exp() * F.softplus(gdn1.in_proj_a(h) + gdn1.dt_bias)
o, S = chunk_gdn2(q, k, v, g=g[:, :, None].expand(B,T,H,K),
b=beta[:, :, None].expand(B,T,H,K),
w=w[:, :, None].expand(B,T,H,V),
use_qk_l2norm_in_kernel=True) # 与原版同一 l2norm 约定
评测(本仓库中文研究文档 2048 token,同一份权重只切 use_gdn2 标志):
| 配置 | ppl | 相对差 |
|---|---|---|
| 原核 GDN-1 | 19.4661 | --- |
| GDN-2 核(零训练) | 19.4985 | 0.177% |
| 复测(逐通道初始化) | 26.3977 vs 26.4097 | 0.05% |
换芯第 0 步等价性成立。 这个 0.0x% 就是"bf16 + 不同 kernel 实现的数值噪声"本底。
顺手把部署路径也打通了:包装层接上 transformers 的 cache(预填走 chunk 核、解码走
fused_recurrent_gdn2 单步核),贪婪解码 48/48 token 与全量重算逐位一致 。
(一个诚实的吐槽:WSL2 的 GPU-PV 内核启动延迟 ~600µs,T=1 小核吃满开销,带 cache
反而比全量重算慢------原生 Linux 上这条快路径才有意义。)
4. 实验二:真语料上的头级解耦 ------ 域外 -4.2%
协议(刻意做严):
- 语料:SkyPile-150B(ModelScope)两个不同分片 ,分片0 → train.bin 40.9M token,
分片1 → eval.bin 4.3M token,分片级隔离,零泄漏; - 域外对照:一份与语料无关的中文文档集;
- 可训练参数:只有新写入门 + 衰减参数,共 59 万(占 2.21B 的 0.027%);
- 三方对比:GDN-1 原核 / GDN-2 零训练 / GDN-2 训后,AdamW lr 1e-4,2500 步。
| 同域 eval.bin | 域外 docs | |
|---|---|---|
| GDN-1 原核 | 26.41 | 20.53 |
| GDN-2 零训练 | 26.40(等价复验 ✅) | 20.55 |
| GDN-2 + 门训练 | 20.56(-22.1%) | 19.66(-4.2%) |
域外也降,说明解耦门的收益不是领域适配假象 。门可视化(18 层 × 16 头热图)显示
训练后 mean|b−w| 从精确的 0 涨到 0.276,并且出现了功能分化:
- 覆写/复位专家:某头 b≈0.99(猛擦)、w≈0.92(猛写)、强衰减------负责"清旧写新";
- 保守/累积专家:某头 w≈0.09------几乎只读不写,专注保留旧关联。
到这里一切都很美好。但我们训练前就立了判停规则 :这只是稀释版 GDN-2(每头一对门),
GDN-2 的本体是逐通道 bK、wV。所以下一发才是定生死的。
5. 实验三:逐通道门 ------ 一发判停
把 b/w 投影从"每头 1 个标量"加宽到逐通道 (输出 H·K / H·V 维),零损失初始化用
权重复制 :新投影的第 h·K+k 行 = 原 in_proj_b 第 h 行 → sigmoid 后逐通道 ≡ β 广播,
等价性依旧严格 (实测零训练 26.3977 vs 26.4097)。可训练参数变成 151.6M。
| 同域 | 域外 | 域外过拟合 | |
|---|---|---|---|
| 原核 | 26.41 | 20.53 | --- |
| 每头级(59 万参数) | 20.56 | 19.66(-4.2%) | 无 |
| 逐通道(151.6M 参数) | 19.50 | 19.81(-3.5%) | 有(@1250 步见底 19.49 后回升) |
三个证据叠成一个结论:
- 域外不涨反略降:参数 ×250,泛化收益一分没有;后半程同域降、域外升 = 纯领域记忆化;
- 权重结构最有说服力 :直接从训后权重算"每通道偏离复制初始化的距离",发现同一头内
128 个通道的偏离度几乎完全相等 (如某头三个通道 1.18 / 1.18 / 1.18)------通道是
整头一起动的,模型自己根本没用通道自由度; - 域外收益天花板始终是 -45%,与门粒度无关。
结构性结论:事后门解耦的瓶颈不在门的开数,而在主干是"β 耦合"下预训练的。
所有下游层都学会了配合耦合门工作,事后开门只能吃到残差。要吃满 GDN-2 的收益,
只有从头预训练(NVlabs 的做法)------那不是一张 32GB 消费卡的量级。
我们在训练前就预注册了判据("域外两位数改进→继续;个位数→收手"),结果个位数,
这条线正式关闭 。负结果也是结果:任何想在 Qwen3.5/GDN 系上做"事后门改造"的同学,
头级就饱和了,别再往通道级加码。
6. 踩坑合集(AMD + WSL2 + 长训,每条都是真实事故)
- fp32 logits 是显存巨无霸 :2B 模型 × 24.8万词表,batch 8 × 2048 的 cross_entropy
单笔分配 16.3GB → OOM。解法:batch 4 + 梯度检查点(峰值 ~19GB);根治要 chunked CE。 PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True在 ROCm 上直接
hipErrorInvalidValue------CUDA 生态的习惯配置别无脑搬。- 会话断线会杀死 wsl.exe 子进程 → 长训练必须
setsid nohup python -u ... > train.log 2>&1 &完全脱钩 +python -u免缓冲;
另外pkill -f train_gates会匹配到启动命令自身把自己杀了,要么分两次调用,要么
pkill -f '[t]rain_gates'。 - ModelScope 下载的 safetensors 文件名别改名 ------以
model.safetensors.index.json
的 weight_map 为准(它可能就是带前缀的长文件名)。 - 等价守卫值得写进脚本 :训练前先评"零训练 GDN-2 vs 原核",相对差 >1% 直接中止,
避免带着坏初始化烧两小时卡。 - 训练/验证分片级隔离 (不同分片、不同来源域外集)是这类"适配收益"实验的生命线,
否则 -22% 会骗你。
7. 保留了什么、下一步
保留的资产:
- 零损失架构手术方法论 :复制初始化 + 等价守卫 + 严格 A/B,适用于任何"门扩展/算子
替换"型改造(不止 GDN); - 三阶段完整数据:0.02%(换核)→ -4.2%(头级解耦)→ 通道冗余(负结果);
- 全套脚本:swap 层 / 训练 / 可视化 / 权重统计,以及增量解码一致性 48/48 的实现。
如果这条线还有救,唯一值得的是 :0.1B 级从头预训练 GDN-2 vs GDN-1 对照------
才能真正回答"解耦门在预训练中值多少",这是另一量级的立项。而"事后改造量产模型"这个
方向,我们建议就到这里。
欢迎在评论区指出我们协议里可能的漏洞(比如:域外集只有一份是否足够?lr 是否该扫?)------
判停标准我们提前写死了,但批评永远欢迎。
实验环境:AMD Radeon AI PRO R9700 32GB / WSL2 Ubuntu-24.04 / torch 2.14.1+rocm7.2 /
triton-rocm 3.8.0 / fla 0.5.2 / transformers main(qwen3_5)。模型 Qwen3.5-2B(ModelScope)。