 解决方案)
【Bug已解决】layers_to_transform incorrectly matches MoE expert indices (should match layer index) 解决方案一、现象长什么样PEFT 的LoraConfig里有个参数layers_to_transform用来指定“只在第几层 transformer 上挂 LoRA”。比如layers_to_transform[0, 1, 2]表示只对第 0/1/2 层做适配。但在MoE混合专家模型上用这个参数时会出现诡异现象你只想给第 0、1、2 层挂 LoRA结果第 5、6、7 层的专家也被挂了或者反过来你指定[0,1,2]但第 0 层的专家一个都没挂反而挂到了其它层print_trainable_parameters()显示挂上去的模块名形如model.layers.5.mlp.experts.0.dense_h_to_4h.lora_A.default——这里的5是专家索引不是层索引排查后发现layers_to_transform匹配逻辑是从模块名里取第一个出现的整数来做比较而 MoE 的模块名是model.layers.0.mlp.experts.0.dense...里面有两个0——第一个0是层索引对的但当模块路径里专家索引排在更靠前、或匹配逻辑用正则layers.(\d)之外的方式提取时就会把专家索引误当成层索引结果是LoRA 挂到了“专家索引在列表里”的模块而不是“层索引在列表里”的模块适配目标完全错位。一句话layers_to_transform在 MoE 上把专家索引当成了层索引来匹配。二、背景先看 MoE 模型的模块命名。以典型 MoE transformer 为例model.layers.0.self_attn.q_proj model.layers.0.mlp.gate_proj model.layers.0.mlp.experts.0.dense_h_to_4h # 专家 0 model.layers.0.mlp.experts.1.dense_h_to_4h # 专家 1 model.layers.1.self_attn.q_proj ...“层索引”指的是model.layers.i里的i“专家索引”指的是experts.j里的j。两者是完全不同的轴一层有n_experts个专家。layers_to_transform的设计意图是“按层索引筛选”。但实现上如果它用了一个粗粒度的整数提取比如re.findall(r\d, name)取所有数字、或取模块名里某个位置的层号就会把专家索引j也算进来。当layers_to_transform[0,1,2]时专家索引为 0/1/2 的模块它们可能属于第 5、6、7 层也会被命中——因为匹配逻辑只看“名字里有没有 0/1/2”而不管这个数字是层还是专家。下面用最小可运行代码复现“错误匹配”和“按层索引正确匹配”。三、根因根因一句话layers_to_transform的匹配逻辑在 MoE 模块名含model.layers.层...experts.专家两段数字上没有锚定到“层索引”那段而是把专家索引也当成了匹配对象导致按专家索引而非层索引筛选。展开数字提取不锚定若用re.findall(r\d, name)或类似“取所有数字”的方式层索引和专家索引混在一起。语义错位layers_to_transform的语义是“层”但实现匹配了“任意数字”专家索引恰好也是数字于是误命中。MoE 特有非 MoE 模型模块名里通常只有一个层索引数字如layers.3所以旧逻辑“碰巧”正确MoE 出现两个数字才暴露 bug。修复方向匹配时显式锚定model.layers.i或对应前缀这一段只取层索引数字与layers_to_transform比较专家索引一律忽略。四、最小可运行复现下面构造一批 MoE 风格的模块名演示“按所有数字匹配错误”与“按层索引锚定匹配正确”。import re # 模拟 MoE 模块名列表 module_names [ model.layers.0.self_attn.q_proj, model.layers.0.mlp.experts.0.dense_h_to_4h, model.layers.0.mlp.experts.1.dense_h_to_4h, model.layers.5.mlp.experts.0.dense_h_to_4h, model.layers.5.mlp.experts.2.dense_h_to_4h, model.layers.7.self_attn.q_proj, ] def match_by_any_digit(name, layers_to_transform): 错误实现取名字里所有数字任一命中即挂。 digits [int(d) for d in re.findall(r\d, name)] return any(d in layers_to_transform for d in digits) def match_by_layer_index(name, layers_to_transform): 正确实现只锚定 model.layers.i 的层索引。 m re.search(rlayers\.(\d), name) if not m: return False layer_idx int(m.group(1)) return layer_idx in layers_to_transform target [0, 5] # 只想给第 0 层和第 5 层挂 LoRA print( 错误匹配按任意数字) for n in module_names: if match_by_any_digit(n, target): print( 命中:, n) print( 正确匹配按层索引锚定) for n in module_names: if match_by_layer_index(n, target): print( 命中:, n)运行后错误匹配会把model.layers.0.mlp.experts.1...专家索引 1 不在 target但层 0 在——这个其实命中对了和model.layers.5.mlp.experts.0...层 5 命中、但也会因为“名字里含 0/5”而把model.layers.7? 不会。真正出错的是专家索引2若恰好落在 target 里则model.layers.5.mlp.experts.2会被错误命中即使第 5 层本就在 target 看起来“对”但反过来若 target[2]想挂第 2 层则model.layers.0.mlp.experts.2和model.layers.5.mlp.experts.2会被错误命中因为专家索引 2 在 target而真正的第 2 层却没挂。下面的修复演示正是要解决这个问题。五、解决方案第一层最小直接修复修复 1匹配时锚定层索引前缀如match_by_layer_index用re.search(rlayers\.(\d), name)只取层索引忽略专家索引。这是最小修复。修复 2在注入前打印“将被挂的模块”肉眼核对def preview_targets(model, layers_to_transform): hits [] for name, m in model.named_modules(): if isinstance(m, nn.Linear): if match_by_layer_index(name, layers_to_transform): hits.append(name) return hits # 确认命中都是 model.layers.{target 里的数字} print(preview_targets(model, [0, 5]))修复 3若只想挂专家用独立的experts_to_transform不要把专家索引塞进layers_to_transform如需按专家筛选另设参数避免语义混淆cfg LoraConfig( r8, lora_alpha16, target_modules[dense_h_to_4h, dense_4h_to_h], layers_to_transform[0, 5], # 层索引不再是专家索引 # experts_to_transform[0, 1], # 若需按专家筛选单独参数示意 )六、解决方案第二层结构性改进改进 1封装一个 MoE 安全的resolve_layers工具import re def resolve_lora_targets(model, target_modules, layers_to_transformNone): 返回真正该挂 LoRA 的模块名按层索引锚定。 hits [] for name, m in model.named_modules(): if not isinstance(m, nn.Linear): continue if not any(tm in name for tm in target_modules): continue if layers_to_transform is not None: lm re.search(rlayers\.(\d), name) if not lm or int(lm.group(1)) not in layers_to_transform: continue hits.append(name) return hits # 用法 targets resolve_lora_targets(model, [dense_h_to_4h], [0, 5])改进 2统一前缀约定避免不同模型命名差异不同 MoE 模型前缀可能不同model.layersvstransformer.hvsdecoder.layers。用可配置前缀LAYER_PREFIX layers. # 或 h., decoder.layers. 视模型而定 def layer_index_of(name, prefixLAYER_PREFIX): m re.search(re.escape(prefix) r(\d), name) return int(m.group(1)) if m else None改进 3在配置加载时校验def validate_layers_to_transform(layers_to_transform, num_layers): for i in layers_to_transform: if i 0 or i num_layers: raise ValueError(flayers_to_transform 含越界层 {i}模型共 {num_layers} 层)七、解决方案第三层断言 / CI 守护import re import pytest def match_by_any_digit(name, target): digits [int(d) for d in re.findall(r\d, name)] return any(d in target for d in digits) def match_by_layer_index(name, target): m re.search(rlayers\.(\d), name) if not m: return False return int(m.group(1)) in target MODULES [ model.layers.0.self_attn.q_proj, model.layers.0.mlp.experts.0.dense_h_to_4h, model.layers.0.mlp.experts.1.dense_h_to_4h, model.layers.5.mlp.experts.2.dense_h_to_4h, model.layers.7.self_attn.q_proj, ] def test_wrong_match_hits_expert_index(): # 错误实现target[2] 会把专家索引 2 的模块误命中 hits [n for n in MODULES if match_by_any_digit(n, [2])] assert any(experts.2 in n for n in hits) # 确认专家索引被误命中 def test_correct_match_uses_layer_index(): # 正确实现target[2] 不应命中没有第2层的模块 hits [n for n in MODULES if match_by_layer_index(n, [2])] assert all(layers.2. in n for n in hits) assert len(hits) 0 # 这批样本里没有第2层 def test_correct_match_hits_exact_layers(): hits [n for n in MODULES if match_by_layer_index(n, [0, 5])] # 第0层和第5层全部命中且不含第7层 assert all((layers.0. in n) or (layers.5. in n) for n in hits) assert all(layers.7. not in n for n in hits) def test_no_expert_index_leak(): # 关键词专家索引绝不参与 layers_to_transform 匹配 hits [n for n in MODULES if match_by_layer_index(n, [0])] for n in hits: # 命中模块必属第0层专家索引随意不影响 assert layers.0. in n这四个测试守护“错误实现会命中专家索引、正确实现只用层索引、精确命中指定层、专家索引不泄漏”。八、排查清单layers_to_transform在 MoE 上挂错时按序查打印实际命中的模块名确认命中的是不是model.layers.{目标层}。检查模块名里的数字段MoE 有layers.层和experts.专家两段旧逻辑可能把专家当层。锚定层索引前缀用re.search(rlayers\.(\d), name)只取层索引。不要混用专家索引按专家筛选用独立参数别塞进layers_to_transform。前缀可配置不同模型前缀不同layers./h./decoder.layers.配置化。加载时校验越界layers_to_transform里所有层号必须在[0, num_layers)。对比非 MoE 行为非 MoE 模型只有一个层数字旧逻辑“碰巧对”MoE 才暴露。测试守护用本节的测试固化“只用层索引、专家索引不泄漏”。九、小结layers_to_transform incorrectly matches MoE expert indices (should match layer index)的根因是MoE 模块名里同时含“层索引”model.layers.i和“专家索引”experts.j两段数字而layers_to_transform的匹配逻辑没有锚定到层索引那段把专家索引也当成匹配对象导致按专家索引而非层索引筛选LoRA 挂错目标。最小修复是匹配时显式锚定layers.(\d)取层索引、注入前打印命中对核对、按专家筛选用独立参数结构性改进是封装 MoE 安全的resolve_lora_targets、前缀可配置、配置加载时校验越界最后用测试守护“错误实现命中专家索引、正确实现只用层索引、精确命中、专家索引不泄漏”。把匹配锚定到层轴LoRA 在 MoE 上才能挂对地方。