Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/peft/utils/transformers_weight_conversion.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
# NOTE: don't import from this module unless transformers v5+ is used
import copy
import re
import warnings
from typing import Any

import torch
Expand Down Expand Up @@ -368,6 +369,23 @@ def _convert_peft_config_moe(peft_config, model_type: str) -> None:
if not fused_targets:
return

# `target_modules` may be a regex string (e.g. when the user passes a pattern produced by an
# upstream framework like ms-swift's multimodal helper). In that case, iterating it would
# split the regex into individual characters, leading to a nonsense set like
# {'^', '(', 'q', '_', 'p', 'r', 'o', 'j', ...} and a downstream "Target modules not found"
# error. The MoE conversion below assumes module-name granularity, so it can't sensibly
# rewrite a regex; skip the conversion and leave the regex untouched.
if isinstance(peft_config.target_modules, str):
warnings.warn(
f"Skipping MoE target-module conversion for model_type={model_type!r}: "
"`target_modules` is a regex string and cannot be remapped automatically. "
"If this is a transformers v4 checkpoint loaded against a v5 architecture, "
"please update the regex to reference the v5 module names (e.g. `gate_up_proj`, "
"`down_proj`).",
stacklevel=2,
)
return

peft_config.target_parameters = set(peft_config.target_parameters or [])
peft_config.target_modules = set(peft_config.target_modules or [])
if not hasattr(peft_config, "rank_pattern") or peft_config.rank_pattern is None:
Expand Down
46 changes: 46 additions & 0 deletions tests/test_integrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -391,3 +391,49 @@ def test_qwen3_moe_works(self):
# target up_proj and gate_proj
config = LoraConfig(target_modules=["gate_proj", "up_proj", "down_proj", "score"])
get_peft_model(model, config) # does not raise

@pytest.mark.parametrize(
"model_type, regex",
[
# The kind of pattern ms-swift's `get_multimodal_target_regex` helper produces for Qwen3-Omni; see
# https://github.com/huggingface/peft/issues/3229 and
# https://github.com/modelscope/ms-swift/issues/9321.
(
"qwen3_omni_moe_thinker",
r"^(thinker\.model(?=\.).*\.(q_proj|k_proj|v_proj|gate_proj|up_proj|down_proj))$",
),
# A simpler "ends with q_proj" pattern that still happens to contain MoE module names as substrings.
("mixtral", r".*\.q_proj"),
],
)
def test_moe_conversion_preserves_regex_target_modules(self, model_type, regex):
# Regression test for https://github.com/huggingface/peft/issues/3229. The MoE config conversion previously did
# `set(peft_config.target_modules or [])`, which when `target_modules` is a regex string splits the regex into
# its individual characters (yielding `{'^', '(', 'q', '_', ...}`). The resulting per-character "targets" then
# caused "Target modules ... not found in the base model" downstream.
from peft.utils.transformers_weight_conversion import _convert_peft_config_moe

config = LoraConfig(target_modules=regex, target_parameters=["foo.weight"])
assert isinstance(config.target_modules, str) # sanity check on LoraConfig

with pytest.warns(UserWarning, match="`target_modules` is a regex string"):
_convert_peft_config_moe(config, model_type)

# The regex must be preserved verbatim — no `set(...)`-induced character splitting.
assert isinstance(config.target_modules, str)
assert config.target_modules == regex
# `target_parameters` is left untouched as well; the conversion is fully skipped.
assert config.target_parameters == ["foo.weight"]

def test_moe_conversion_remaps_list_target_modules(self):
# Defensive companion to test_moe_conversion_preserves_regex_target_modules: a list of module names should
# still go through the normal MoE conversion path and remap `gate_proj` + `up_proj` to the fused
# `gate_up_proj`.
from peft.utils.transformers_weight_conversion import _convert_peft_config_moe

config = LoraConfig(target_modules=["gate_proj", "up_proj"])
# list -> set in LoraConfig.__post_init__
assert isinstance(config.target_modules, set)
_convert_peft_config_moe(config, "qwen3_omni_moe_thinker")
# After conversion the original module names are remapped onto `gate_up_proj` via target_parameters.
assert "gate_up_proj" in config.target_parameters