2. 上游接口 QwenLinearDecoderLayer和QwenDecoderLayer的区别在于:将传统注意力模块改为基于GatedDeltaNet的注意力模块;classQwenLinearDecoderLayer(nn.Module):"""Qwen3.5 Linear Attention (@) + MLP (^) 混合层"""def__init__(self,config:QwenNemotronConfig,linear_attn_idx:int,mlp_layer_idx:int):super().__init__()self.hidden_size=config.hidden_size self.linear_attn_idx=linear_attn_idx self.mlp_layer_idx=mlp_layer_idx self.linear_attn=Qwen3_5GatedDeltaNet(config,layer_idx=linear_attn_idx)self.mlp=QwenMLP(config)self.input_layernorm=QwenRMSNorm(config.hidden_size,eps=config.rms_norm_eps)self.post_attention_layernorm=QwenRMSNorm(config.hidden_size,eps=config.rms_norm_eps)defforward(self,hidden_states,attention_mask=None,cache_params=None,cache_position=None,**kwargs):# 1. Linear Attention (无 RoPE,左Padding或全连接Mask)residual=hidden_states hidden_states=self.input_layernorm(hidden_states)hidden_states=self.linear_attn(hidden_states,cache_params=cache_params,attention_mask=attention_mask,cache_position=cache_position,)hidden_states=residual+hidden_states# 2. MLPresidual=hidden_states hidden_states=self.post_attention_layernorm(hidden_states)hidden_states=self.mlp(hidden_states)returnresidual+hidden_states3. GatedDeltaNet 模块的实现GatedDeltaNet 的数据流动分为了以下几个关键步骤:init 部分看看 GatedDeltaNet 的数据流动需要什么区分边界条件:根据cache_params、cache_position、seqlen区分是否是训练 / 推理;cache_params:有无缓存对象;cache_position:过去生成的文字;cache_params和cache_position、seqlen都是在main model传入到当前layer(GatedDeltaNet)的,我们回顾 main model 对 cache_position 处理的代码:ifcache_positionisNone:past_seen_tokens=cache_params.get_seq_length()ifcache_paramsisnotNoneelse0cache_position=torch.arange(past_seen_tokens,# 从这里开始past_seen_tokens+hidden_states.shape[1],# 到这里结束device=hidden_states.device,)训练时:past_seen_tokens为 0,并行处理seqlen个 token,则 torch.arrange(0, seqlen),因此训练条件下 -cache_position = [0,1,2,3,4];推理时:past_seen_tokens为 4,seqlen= 1,则 torch.arrange(4, seqlen),那么推理条件下 -cache_position = [(4)];结论:因此我们可以根据cache_position的第一个元素cache_position[0]是否为 0 区分当前状态是否训练;3.1 推理 / 训练缓存判断如果是推理则需要从cache_params抽取出缓存,代码如下:cache_params is not None:传入了缓存作为参数,才是推理;cache_position[0] 0:过去生成了不止一个词,才是推理;seq_len == 1:生成一个词,才是推理;# ======================# 推理缓存判断# ======================use_precomputed_states=(cache_paramsisnotNoneandcache_position[0]0andseq_len==1)# 如果是推理增量生成 → 读取缓存ifuse_precomputed_states:conv_state=cache_params.linear_conv_states[self.layer_idx]recurrent_state=cache_params.linear_recurrent_states[self.layer_idx]如果是推理,则use_precomputed_states = True, 从缓存cache_params中读取 “因果卷积” 和过去的 “记忆状态”;3.1init: QKV 投影到卷积空间首先我们来阐述变量的定义:self.conv_dim:将 Q、K、V 三个向量,做一次 因果卷积!,因此它的 shape 为 Q、K、V 三个向量拼起来的维度;(目的:使得 QKV 能够共享局部特征,因为Q、K、V 必须看到完全一样的局部上下文)self.in_proj_qkv:投影到卷积空间;conv_kernel_size:卷积核大小;conv_dim 就是打包袋!把 Q、K、V 装进去一起过卷积,再拆开;卷积的窗口大小为 conv_kernel_size;代码如下:# --------------------------# 线性注意力核心投影层# --------------------------self.key_dim=self.head_k_dim*self.num_k_heads self.value_dim=self.head_v_dim*self.num_v_heads# QKV 投影# 因果卷积(用于局部上下文建模)self.conv_kernel_size=config.linear_conv_kernel_dim self.conv_dim=self.key_dim*2+self.value_dim# Q,K,V 拼起来的维度self.in_proj_qkv=nn.Linear(self.hidden_size,self.conv_dim,bias=False)# 将 QKV 投影到同一个卷积空间,共享局部特征3.2init:GatedDeltaNet 最灵魂的 “门控 + 时序控制”我们需要三个变量z /b/a 作为 “控制信号”:(1)控制输出;(2)控制写入;(3)控制衰减,具体如下:代码如下:# 门控、参数投影self.in_proj_z=nn.Linear(self.hidden_size,self.value_dim,bias=False)self.in_proj_b=nn.Linear(self.hidden_size,self.num_v_heads,bias=False)self.in_proj_a=nn.Linear(self.hidden_size,self.num_v_heads,bias=False)为什么这么设计?self.in_proj_z:作为输出门控(Output Gate),生成一个门控向量z,最后和注意输出逐元素相乘,控制最终输出哪些信息、抑制哪些信息(所以要和V(value)的输出维度完全一样);self.in_proj_b:控制当前 token 对历史状态的 “写入强度”,每个头一个独立的记忆强度有的头记得多,有的头记得少。self.in_proj_a:控制过去的信息多快被遗忘(生成时间衰减系数g),每个头一个独立的遗忘速度有的头忘得快,有的头忘得慢。3.3init: 设置卷积和遗忘系数 A_log(1) 设置卷积参考3.1和3.2:输入:Q、K、V的拼凑大小;输出:Q、K、V的拼凑大小(共享局部空间后再进行切分为 QKV);# 因果卷积(局部建模)self.conv1d=nn.Conv1d(in_channels=self.conv_dim,out_channels=self.conv_dim,kernel_size=self.conv_kernel_size,groups=self.conv_dim,padding=self.conv_kernel_size-1,)(2)设置遗忘系数:随着时间步长,每个head控制过去信息的遗忘程度都不一样,代码如下:# 时间步参数(Delta 门控专用)self.dt_bias=nn.Parameter(torch.ones(self.num_v_heads))A=torch.empty(self.num_v_heads).uniform_(0,16)# 取随机向量(0,16)self.A_log=nn.Parameter(torch.log(A))# 去对数 log(A),变成可微分(可学习)的参数self.dt_bias:每个头拥有一个偏置记忆遗忘的偏置(可学习);A:A 越大 → 忘得越快;A 越小 → 忘得越慢(A 必须 0,遗忘速度不能为负数);3.4init: 输出投影self.out_proj=nn.Linear(self.value_dim,self.hidden_size,bias=False)3.5init:卷积(推理 / 训练)的函数self.causal_conv1d_fn=causal_conv1d_fn# 训练:整句卷积self.causal_conv1d_update=causal_conv1d_update# 推理:增量卷积self.chunk_gated_delta_rule=chunk_gated_delta_rule# 训练:分块并行self.recurrent_gated_delta_rule=recurrent_gated_delta_rule# 推理:逐词递归训练 = 快路径 = 并行 = chunk + full conv推理 = 慢路径 = 流式 = recurrent + update0. 卷积操作 GatedDeltaNet 操作self.causal_conv1d_fn=causal_conv1d_fn# 训练:整句卷积self.causal_conv1d_update=causal_conv1d_update# 推理:增量卷积self.chunk_gated_delta_rule=chunk_gated_delta_rule# 训练:分块并行self.recurrent_gated_delta_rule=recurrent_gated_delta_rule# 推理:逐词递归它们的分工:训练 = 快路径 = 并行 = chunk + full conv推理 = 慢路径 = 流式 = recurrent + update1. torch_causal_conv1d_update(推理时的因果卷积)作用:增量式更新卷积窗口, 只输入1 个 token,不重新算整句!输入:hidden_states:[B, C, 1]→ 当前新词;conv_state:[B, C, kernel_size]→ 过去窗口;输出:当前新输入 1 个 token 经过【因果卷积】后的特征[batch, hidden_size, 1];流程:# 1. 获取输入维度:批次、特征维度、当前输入长度_,hidden_size,seq_len=hidden_states