【Bug已解决】Partial LoRA on Qwen3.5/Qwen3.6 GatedDeltaNet (in_proj_qkv without in_proj_z) crashes in expand_packed_lora 解决方案一、现象长什么样在 Qwen3.5 / Qwen3.6 的GatedDeltaNet门控线性注意力/线性 RNN 类模型上加载一个部分 LoRAPartial LoRA——即只训练了部分投影、没有覆盖全部 packed 投影——时加载阶段在expand_packed_lora这一步崩溃。典型日志KeyError / IndexError in expand_packed_lora: in_proj_z not found while expanding in_proj_qkv RuntimeError: packed lora expects 4 components (q,k,v,z) but only got 3 (q,k,v)或者更笼统Partial LoRA on Qwen3.5/Qwen3.6 GatedDeltaNet (in_proj_qkv without in_proj_z) crashes in expand_packed_lora几个特征帮你判断是不是同一个坑报错发生在expand_packed_lora把「打包的 LoRA」展开成各个子投影这一步不是权重加载、也不是 forward。错误明确提到in_proj_qkv合并的 q/k/v 投影和in_proj_zGatedDeltaNet 特有的「门控/循环」投影且说「有 qkv 但没 z」。全量 LoRA同时训了 qkv 和 z能正常加载只有**部分 LoRA只训了 qkv、没训 z**崩——说明是「打包展开假设所有分量都存在」出了问题。换成普通 Transformer没有in_proj_z的模型做部分 LoRA 没事说明问题在 GatedDeltaNet 的「额外投影分量 z」与打包逻辑的耦合。二、背景GatedDeltaNet 这类线性注意力/门控循环模型其「混合投影」写法常把多个子投影**打包pack**成一个大矩阵以减少 kernel 调用、提升效率。以in_proj为例普通 Transformer 的in_proj通常是q_projk_projv_proj打包后叫in_proj_qkv张量形状[3*hidden, ...]。GatedDeltaNet 多了一个in_proj_z门控/循环状态投影于是完整的打包是in_projq,k,v,z张量形状[4*hidden, ...]。LoRA 在「打包投影」上做训练时会把 LoRA 的 A/B 矩阵也按打包维度组织。expand_packed_lora的作用是把「打包的 LoRA 权重」按固定分量顺序q → k → v → z切回各个子投影挂到对应子模块上。问题出在部分 LoRA用户只对in_proj_qkvq,k,v训了 LoRA没有对in_proj_z训。于是打包 LoRA 里只有 q,k,v 三个分量但expand_packed_lora的代码写死「按 q,k,v,z 四个分量顺序切分」并且假设第 4 段一定是 z。当它在切分后的列表里去找in_proj_z时要么下标越界只有 3 段却取第 4 段→IndexError要么切完后用固定 keyin_proj_z去取取不到 →KeyError于是崩溃。换句话说打包展开逻辑假设「所有分量都被 LoRA 覆盖」但部分 LoRA 只覆盖了子集假设被打破。三、根因根因一句话GatedDeltaNet 的expand_packed_lora把打包 LoRA 按「固定的全部分量顺序q,k,v,z」切分并假设每段都存在但部分 LoRA 只训了 qkv、没有 z 分量导致切分后取in_proj_z时越界/缺失触发崩溃。具体成因分量顺序写死expand_packed_lora用for comp in (q,k,v,z)遍历并按下标从打包张量切段未判断该分量是否在 LoRA 里实际存在。缺失分量无默认当in_proj_z不在部分 LoRA 中时代码没有「跳过该分量、或填充零 LoRA」的处理直接按固定 key 取值 → KeyError/IndexError。packed 维与实际训练维不一致打包张量的第 0 维是3*hidden只有 qkv还是4*hidden含 z取决于训练时覆盖了哪些展开逻辑没读取这个实际维仍按 4 切。缺少部分 LoRA 的白名单加载器没声明「GatedDeltaNet 支持部分 LoRA子集分量」于是把所有 LoRA 当全量处理。错误未优雅提示崩溃信息没告诉用户「你的 LoRA 缺了 in_proj_z应跳过或补零」而是底层索引错误难以定位。核心矛盾打包展开逻辑把「打包里有哪些分量」当成了「固定全集」但部分 LoRA 的打包里只有子集二者对不齐时就越界。四、最小可运行复现下面用纯 Python 模拟「打包 LoRA 只有 qkv 三段展开按 qkvz 四段切分导致越界」# reproduce_packed_lora.py # 复现部分 LoRA 打包只有 qkvexpand 按 qkvz 四段切 - 越界 def expand_packed_lora_buggy(packed, components(q, k, v, z)): # 假设 packed 按 components 顺序切成等长段 seg len(packed) // len(components) out {} for i, comp in enumerate(components): out[comp] packed[i * seg:(i 1) * seg] # 只有 3 段时 i3 越界/错位 return out def expand_packed_lora_fixed(packed, present): 只展开实际存在的分量。 seg len(packed) // len(present) out {} for i, comp in enumerate(present): out[comp] packed[i * seg:(i 1) * seg] return out if __name__ __main__: qkv [1, 2, 3, 4, 5, 6] # 只有 q,k,v 三段 (每段2) try: expand_packed_lora_buggy(qkv) # 按4段切 - 错位 except (ZeroDivisionError, IndexError) as e: print(复现成功(切分错位):, e) # 正确: 只展开存在的 q,k,v print(修复:, expand_packed_lora_fixed(qkv, present(q, k, v)))运行python reproduce_packed_lora.py能看到「按固定 4 段切 3 段数据」会导致错位/越界正是崩溃成因。五、解决方案第一层最小直接修复最小修复expand_packed_lora不再按「固定全集顺序」切分而是先读取打包 LoRA 实际包含哪些分量只展开存在的分量缺失的如in_proj_z直接跳过等价于该分量无 LoRA不影响原权重。# fix_layer1_expand.py def component_layout(model_has_z: bool): GatedDeltaNet 的打包分量顺序按模型是否有 z 投影决定。 return (q, k, v, z) if model_has_z else (q, k, v) def expand_packed_lora_safe(packed_bytes, present_components): 只切分实际存在的分量缺失分量跳过。 if len(present_components) 0: return {} seg len(packed_bytes) // len(present_components) assert seg * len(present_components) len(packed_bytes), 打包维与分量数不匹配 out {} for i, comp in enumerate(present_components): out[fin_proj_{comp}] packed_bytes[i * seg:(i 1) * seg] return out if __name__ __main__: qkv list(range(6)) # 仅 q,k,v # 部分 LoRA: 已知只有 qkv print(expand_packed_lora_safe(qkv, present_components(q, k, v)))这一层把「假设全集」改成「按实际 present 分量展开」缺失的 z 直接跳过部分 LoRA 即可正常加载。六、解决方案第二层结构性改进把「打包分量探测 展开」做成独立模块从 LoRA 权重名或元数据推断实际分量集合而非写死# fix_layer2_packed.py from dataclasses import dataclass, field dataclass class PackedLoraSpec: # 该模型打包投影的完整候选分量 full_components: tuple (q, k, v, z) # 实际 LoRA 覆盖的分量从权重名推断 present: tuple field(default_factorytuple) classmethod def infer_from_keys(cls, lora_keys: set, base(q, k, v, z)): present tuple(c for c in base if any(fin_proj_{c} in k for k in lora_keys)) return cls(full_componentsbase, presentpresent) def expand(self, packed, proj_name: str): if len(self.present) 0: return {} seg len(packed) // len(self.present) out {} for i, comp in enumerate(self.present): out[f{proj_name}_{comp}] packed[i * seg:(i 1) * seg] return out def missing(self): return tuple(c for c in self.full_components if c not in self.present) if __name__ __main__: keys {in_proj_qkv.lora_A, in_proj_qkv.lora_B} # 仅 qkv spec PackedLoraSpec.infer_from_keys(keys) print(实际分量:, spec.present) # (q,k,v) print(缺失分量(跳过):, spec.missing()) # (z,) print(展开:, spec.expand(list(range(6)), in_proj))这样换模型有无 z、换 LoRA部分/全量时展开逻辑都按「推断出的实际分量」走缺失分量自动跳过不再越界。七、解决方案第三层断言 / CI 守护把「打包分量子集展开安全」钉进断言和 CI# fix_layer3_guard.py # ---- pytest 用例进 CI ---- def test_partial_lora_skips_z(): from fix_layer2_packed import PackedLoraSpec keys {in_proj_qkv.lora_A} spec PackedLoraSpec.infer_from_keys(keys) assert spec.present (q, k, v) assert z in spec.missing() out spec.expand(list(range(6)), in_proj) assert in_proj_z not in out assert set(out) {in_proj_q, in_proj_k, in_proj_v} def test_full_lora_includes_z(): from fix_layer2_packed import PackedLoraSpec keys {in_proj_qkv.lora_A, in_proj_z.lora_A} spec PackedLoraSpec.infer_from_keys(keys) assert spec.present (q, k, v, z) def test_packed_dim_matches_components(): from fix_layer1_expand import expand_packed_lora_safe try: expand_packed_lora_safe([1, 2, 3], (q, k, v, z)) # 维不对 assert False except AssertionError: pass再加加载断言def assert_packed_lora_ok(spec: PackedLoraSpec, packed, proj_name): spec.expand(packed, proj_name) # 内部已校验维与分量数对齐八、排查清单GatedDeltaNet 部分 LoRA 在expand_packed_lora崩溃按序查先试全量 LoRA同时训 qkvz 能加载说明问题在「部分覆盖」。确认缺哪个分量报错说in_proj_z缺失说明你的 LoRA 只训了 qkv。看打包维in_proj张量第 0 维是3*hidden仅 qkv还是4*hidden含 z应与 LoRA 覆盖一致。改展开逻辑expand_packed_lora按「实际 present 分量」切分别写死 qkvz 全集。缺失分量跳过没有 z 就跳过等价于该分量无 LoRA不影响原权重。从权重名推断分量用in_proj_q/in_proj_z等键名推断 present而非假设全集。确认模型是否有 zGatedDeltaNet 才有in_proj_z普通 Transformer 没有别在后者硬套 4 分量。错误要可读缺失分量时报「LoRA 未覆盖 in_proj_z已跳过」而非底层 IndexError。升级 PEFT/vLLM新版对部分打包 LoRA 支持更好可能已修复。最后才动 kernel优先在加载/展开层修分量推断不要为兼容去改打包注意力核。九、小结GatedDeltaNet 上「部分 LoRA有 in_proj_qkv 无 in_proj_z」在expand_packed_lora崩溃根子是打包展开逻辑按固定的「q,k,v,z」全集顺序切分假设每个分量都被 LoRA 覆盖但部分 LoRA 只有 qkv 子集切分后取 in_proj_z 越界/缺失而崩。修复三层第一层展开时只切实际 present 分量、缺失 z 直接跳过第二层抽PackedLoraSpec从权重名推断实际分量集合、自动跳过缺失第三层用 pytest 把「部分 LoRA 跳过 z」「全量含 z」「打包维对齐」钉进 CI。核心认识——打包投影的展开必须「以实际打包内容为唯一事实来源」绝不能用固定全集顺序去切一个可能只有子集的打包张量任何缺失分量都应等价「该分量无 LoRA」被安全跳过。
【Bug已解决】Partial LoRA on Qwen3.5/Qwen3.6 GatedDeltaNet (in_proj_qkv without in_proj_z) crashes in exp
【Bug已解决】Partial LoRA on Qwen3.5/Qwen3.6 GatedDeltaNet (in_proj_qkv without in_proj_z) crashes in expand_packed_lora 解决方案一、现象长什么样在 Qwen3.5 / Qwen3.6 的GatedDeltaNet门控线性注意力/线性 RNN 类模型上加载一个部分 LoRAPartial LoRA——即只训练了部分投影、没有覆盖全部 packed 投影——时加载阶段在expand_packed_lora这一步崩溃。典型日志KeyError / IndexError in expand_packed_lora: in_proj_z not found while expanding in_proj_qkv RuntimeError: packed lora expects 4 components (q,k,v,z) but only got 3 (q,k,v)或者更笼统Partial LoRA on Qwen3.5/Qwen3.6 GatedDeltaNet (in_proj_qkv without in_proj_z) crashes in expand_packed_lora几个特征帮你判断是不是同一个坑报错发生在expand_packed_lora把「打包的 LoRA」展开成各个子投影这一步不是权重加载、也不是 forward。错误明确提到in_proj_qkv合并的 q/k/v 投影和in_proj_zGatedDeltaNet 特有的「门控/循环」投影且说「有 qkv 但没 z」。全量 LoRA同时训了 qkv 和 z能正常加载只有**部分 LoRA只训了 qkv、没训 z**崩——说明是「打包展开假设所有分量都存在」出了问题。换成普通 Transformer没有in_proj_z的模型做部分 LoRA 没事说明问题在 GatedDeltaNet 的「额外投影分量 z」与打包逻辑的耦合。二、背景GatedDeltaNet 这类线性注意力/门控循环模型其「混合投影」写法常把多个子投影**打包pack**成一个大矩阵以减少 kernel 调用、提升效率。以in_proj为例普通 Transformer 的in_proj通常是q_projk_projv_proj打包后叫in_proj_qkv张量形状[3*hidden, ...]。GatedDeltaNet 多了一个in_proj_z门控/循环状态投影于是完整的打包是in_projq,k,v,z张量形状[4*hidden, ...]。LoRA 在「打包投影」上做训练时会把 LoRA 的 A/B 矩阵也按打包维度组织。expand_packed_lora的作用是把「打包的 LoRA 权重」按固定分量顺序q → k → v → z切回各个子投影挂到对应子模块上。问题出在部分 LoRA用户只对in_proj_qkvq,k,v训了 LoRA没有对in_proj_z训。于是打包 LoRA 里只有 q,k,v 三个分量但expand_packed_lora的代码写死「按 q,k,v,z 四个分量顺序切分」并且假设第 4 段一定是 z。当它在切分后的列表里去找in_proj_z时要么下标越界只有 3 段却取第 4 段→IndexError要么切完后用固定 keyin_proj_z去取取不到 →KeyError于是崩溃。换句话说打包展开逻辑假设「所有分量都被 LoRA 覆盖」但部分 LoRA 只覆盖了子集假设被打破。三、根因根因一句话GatedDeltaNet 的expand_packed_lora把打包 LoRA 按「固定的全部分量顺序q,k,v,z」切分并假设每段都存在但部分 LoRA 只训了 qkv、没有 z 分量导致切分后取in_proj_z时越界/缺失触发崩溃。具体成因分量顺序写死expand_packed_lora用for comp in (q,k,v,z)遍历并按下标从打包张量切段未判断该分量是否在 LoRA 里实际存在。缺失分量无默认当in_proj_z不在部分 LoRA 中时代码没有「跳过该分量、或填充零 LoRA」的处理直接按固定 key 取值 → KeyError/IndexError。packed 维与实际训练维不一致打包张量的第 0 维是3*hidden只有 qkv还是4*hidden含 z取决于训练时覆盖了哪些展开逻辑没读取这个实际维仍按 4 切。缺少部分 LoRA 的白名单加载器没声明「GatedDeltaNet 支持部分 LoRA子集分量」于是把所有 LoRA 当全量处理。错误未优雅提示崩溃信息没告诉用户「你的 LoRA 缺了 in_proj_z应跳过或补零」而是底层索引错误难以定位。核心矛盾打包展开逻辑把「打包里有哪些分量」当成了「固定全集」但部分 LoRA 的打包里只有子集二者对不齐时就越界。四、最小可运行复现下面用纯 Python 模拟「打包 LoRA 只有 qkv 三段展开按 qkvz 四段切分导致越界」# reproduce_packed_lora.py # 复现部分 LoRA 打包只有 qkvexpand 按 qkvz 四段切 - 越界 def expand_packed_lora_buggy(packed, components(q, k, v, z)): # 假设 packed 按 components 顺序切成等长段 seg len(packed) // len(components) out {} for i, comp in enumerate(components): out[comp] packed[i * seg:(i 1) * seg] # 只有 3 段时 i3 越界/错位 return out def expand_packed_lora_fixed(packed, present): 只展开实际存在的分量。 seg len(packed) // len(present) out {} for i, comp in enumerate(present): out[comp] packed[i * seg:(i 1) * seg] return out if __name__ __main__: qkv [1, 2, 3, 4, 5, 6] # 只有 q,k,v 三段 (每段2) try: expand_packed_lora_buggy(qkv) # 按4段切 - 错位 except (ZeroDivisionError, IndexError) as e: print(复现成功(切分错位):, e) # 正确: 只展开存在的 q,k,v print(修复:, expand_packed_lora_fixed(qkv, present(q, k, v)))运行python reproduce_packed_lora.py能看到「按固定 4 段切 3 段数据」会导致错位/越界正是崩溃成因。五、解决方案第一层最小直接修复最小修复expand_packed_lora不再按「固定全集顺序」切分而是先读取打包 LoRA 实际包含哪些分量只展开存在的分量缺失的如in_proj_z直接跳过等价于该分量无 LoRA不影响原权重。# fix_layer1_expand.py def component_layout(model_has_z: bool): GatedDeltaNet 的打包分量顺序按模型是否有 z 投影决定。 return (q, k, v, z) if model_has_z else (q, k, v) def expand_packed_lora_safe(packed_bytes, present_components): 只切分实际存在的分量缺失分量跳过。 if len(present_components) 0: return {} seg len(packed_bytes) // len(present_components) assert seg * len(present_components) len(packed_bytes), 打包维与分量数不匹配 out {} for i, comp in enumerate(present_components): out[fin_proj_{comp}] packed_bytes[i * seg:(i 1) * seg] return out if __name__ __main__: qkv list(range(6)) # 仅 q,k,v # 部分 LoRA: 已知只有 qkv print(expand_packed_lora_safe(qkv, present_components(q, k, v)))这一层把「假设全集」改成「按实际 present 分量展开」缺失的 z 直接跳过部分 LoRA 即可正常加载。六、解决方案第二层结构性改进把「打包分量探测 展开」做成独立模块从 LoRA 权重名或元数据推断实际分量集合而非写死# fix_layer2_packed.py from dataclasses import dataclass, field dataclass class PackedLoraSpec: # 该模型打包投影的完整候选分量 full_components: tuple (q, k, v, z) # 实际 LoRA 覆盖的分量从权重名推断 present: tuple field(default_factorytuple) classmethod def infer_from_keys(cls, lora_keys: set, base(q, k, v, z)): present tuple(c for c in base if any(fin_proj_{c} in k for k in lora_keys)) return cls(full_componentsbase, presentpresent) def expand(self, packed, proj_name: str): if len(self.present) 0: return {} seg len(packed) // len(self.present) out {} for i, comp in enumerate(self.present): out[f{proj_name}_{comp}] packed[i * seg:(i 1) * seg] return out def missing(self): return tuple(c for c in self.full_components if c not in self.present) if __name__ __main__: keys {in_proj_qkv.lora_A, in_proj_qkv.lora_B} # 仅 qkv spec PackedLoraSpec.infer_from_keys(keys) print(实际分量:, spec.present) # (q,k,v) print(缺失分量(跳过):, spec.missing()) # (z,) print(展开:, spec.expand(list(range(6)), in_proj))这样换模型有无 z、换 LoRA部分/全量时展开逻辑都按「推断出的实际分量」走缺失分量自动跳过不再越界。七、解决方案第三层断言 / CI 守护把「打包分量子集展开安全」钉进断言和 CI# fix_layer3_guard.py # ---- pytest 用例进 CI ---- def test_partial_lora_skips_z(): from fix_layer2_packed import PackedLoraSpec keys {in_proj_qkv.lora_A} spec PackedLoraSpec.infer_from_keys(keys) assert spec.present (q, k, v) assert z in spec.missing() out spec.expand(list(range(6)), in_proj) assert in_proj_z not in out assert set(out) {in_proj_q, in_proj_k, in_proj_v} def test_full_lora_includes_z(): from fix_layer2_packed import PackedLoraSpec keys {in_proj_qkv.lora_A, in_proj_z.lora_A} spec PackedLoraSpec.infer_from_keys(keys) assert spec.present (q, k, v, z) def test_packed_dim_matches_components(): from fix_layer1_expand import expand_packed_lora_safe try: expand_packed_lora_safe([1, 2, 3], (q, k, v, z)) # 维不对 assert False except AssertionError: pass再加加载断言def assert_packed_lora_ok(spec: PackedLoraSpec, packed, proj_name): spec.expand(packed, proj_name) # 内部已校验维与分量数对齐八、排查清单GatedDeltaNet 部分 LoRA 在expand_packed_lora崩溃按序查先试全量 LoRA同时训 qkvz 能加载说明问题在「部分覆盖」。确认缺哪个分量报错说in_proj_z缺失说明你的 LoRA 只训了 qkv。看打包维in_proj张量第 0 维是3*hidden仅 qkv还是4*hidden含 z应与 LoRA 覆盖一致。改展开逻辑expand_packed_lora按「实际 present 分量」切分别写死 qkvz 全集。缺失分量跳过没有 z 就跳过等价于该分量无 LoRA不影响原权重。从权重名推断分量用in_proj_q/in_proj_z等键名推断 present而非假设全集。确认模型是否有 zGatedDeltaNet 才有in_proj_z普通 Transformer 没有别在后者硬套 4 分量。错误要可读缺失分量时报「LoRA 未覆盖 in_proj_z已跳过」而非底层 IndexError。升级 PEFT/vLLM新版对部分打包 LoRA 支持更好可能已修复。最后才动 kernel优先在加载/展开层修分量推断不要为兼容去改打包注意力核。九、小结GatedDeltaNet 上「部分 LoRA有 in_proj_qkv 无 in_proj_z」在expand_packed_lora崩溃根子是打包展开逻辑按固定的「q,k,v,z」全集顺序切分假设每个分量都被 LoRA 覆盖但部分 LoRA 只有 qkv 子集切分后取 in_proj_z 越界/缺失而崩。修复三层第一层展开时只切实际 present 分量、缺失 z 直接跳过第二层抽PackedLoraSpec从权重名推断实际分量集合、自动跳过缺失第三层用 pytest 把「部分 LoRA 跳过 z」「全量含 z」「打包维对齐」钉进 CI。核心认识——打包投影的展开必须「以实际打包内容为唯一事实来源」绝不能用固定全集顺序去切一个可能只有子集的打包张量任何缺失分量都应等价「该分量无 LoRA」被安全跳过。