Defuser 0.0.4__tar.gz → 0.0.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {defuser-0.0.4 → defuser-0.0.6}/Defuser.egg-info/PKG-INFO +1 -1
- {defuser-0.0.4 → defuser-0.0.6}/Defuser.egg-info/SOURCES.txt +3 -7
- {defuser-0.0.4 → defuser-0.0.6}/PKG-INFO +1 -1
- {defuser-0.0.4 → defuser-0.0.6}/defuser/__init__.py +4 -4
- defuser-0.0.6/defuser/defuser.py +124 -0
- defuser-0.0.6/defuser/model_registry.py +16 -0
- {defuser-0.0.4/defuser/modeling/fused_moe → defuser-0.0.6/defuser/modeling}/moe_experts_interface.py +0 -8
- {defuser-0.0.4/defuser/modeling/fused_moe → defuser-0.0.6/defuser/modeling}/replace_modules.py +4 -32
- {defuser-0.0.4/defuser/modeling/fused_moe → defuser-0.0.6/defuser/modeling}/update_module.py +1 -1
- {defuser-0.0.4 → defuser-0.0.6}/defuser/utils/hf.py +1 -33
- {defuser-0.0.4 → defuser-0.0.6}/pyproject.toml +1 -1
- {defuser-0.0.4 → defuser-0.0.6}/tests/test_convert_model.py +24 -14
- {defuser-0.0.4 → defuser-0.0.6}/tests/test_replace_modules_tracker.py +1 -1
- defuser-0.0.4/defuser/defuser.py +0 -100
- defuser-0.0.4/defuser/model_registry.py +0 -35
- defuser-0.0.4/defuser/modeling/fused_moe/qwen3_5_moe.py +0 -116
- defuser-0.0.4/defuser/modeling/unfused_moe/__init__.py +0 -0
- defuser-0.0.4/defuser/modeling/unfused_moe/qwen3_moe.py +0 -70
- defuser-0.0.4/defuser/utils/__init__.py +0 -0
- {defuser-0.0.4 → defuser-0.0.6}/Defuser.egg-info/dependency_links.txt +0 -0
- {defuser-0.0.4 → defuser-0.0.6}/Defuser.egg-info/requires.txt +0 -0
- {defuser-0.0.4 → defuser-0.0.6}/Defuser.egg-info/top_level.txt +0 -0
- {defuser-0.0.4 → defuser-0.0.6}/LICENSE +0 -0
- {defuser-0.0.4 → defuser-0.0.6}/README.md +0 -0
- {defuser-0.0.4 → defuser-0.0.6}/defuser/modeling/__init__.py +0 -0
- {defuser-0.0.4/defuser/modeling/fused_moe → defuser-0.0.6/defuser/utils}/__init__.py +0 -0
- {defuser-0.0.4 → defuser-0.0.6}/defuser/utils/common.py +0 -0
- {defuser-0.0.4 → defuser-0.0.6}/defuser/utils/device.py +0 -0
- {defuser-0.0.4 → defuser-0.0.6}/defuser/utils/model.py +0 -0
- {defuser-0.0.4 → defuser-0.0.6}/setup.cfg +0 -0
- {defuser-0.0.4 → defuser-0.0.6}/setup.py +0 -0
- {defuser-0.0.4 → defuser-0.0.6}/tests/test_device_utils.py +0 -0
|
@@ -11,13 +11,9 @@ defuser/__init__.py
|
|
|
11
11
|
defuser/defuser.py
|
|
12
12
|
defuser/model_registry.py
|
|
13
13
|
defuser/modeling/__init__.py
|
|
14
|
-
defuser/modeling/
|
|
15
|
-
defuser/modeling/
|
|
16
|
-
defuser/modeling/
|
|
17
|
-
defuser/modeling/fused_moe/replace_modules.py
|
|
18
|
-
defuser/modeling/fused_moe/update_module.py
|
|
19
|
-
defuser/modeling/unfused_moe/__init__.py
|
|
20
|
-
defuser/modeling/unfused_moe/qwen3_moe.py
|
|
14
|
+
defuser/modeling/moe_experts_interface.py
|
|
15
|
+
defuser/modeling/replace_modules.py
|
|
16
|
+
defuser/modeling/update_module.py
|
|
21
17
|
defuser/utils/__init__.py
|
|
22
18
|
defuser/utils/common.py
|
|
23
19
|
defuser/utils/device.py
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
# SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
# Contact: qubitium@modelcloud.ai, x.com/qubitium
|
|
5
5
|
|
|
6
|
-
def
|
|
6
|
+
def convert_model(*args, **kwargs):
|
|
7
7
|
"""Lazily import conversion entrypoint to avoid import-time cycles."""
|
|
8
|
-
from .defuser import
|
|
8
|
+
from .defuser import convert_model as _convert_model
|
|
9
9
|
|
|
10
|
-
return
|
|
10
|
+
return _convert_model(*args, **kwargs)
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
__all__ = ["
|
|
13
|
+
__all__ = ["convert_model"]
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 ModelCloud.ai
|
|
2
|
+
# SPDX-FileCopyrightText: 2026 qubitium@modelcloud.ai
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
# Contact: qubitium@modelcloud.ai, x.com/qubitium
|
|
5
|
+
|
|
6
|
+
from torch import nn
|
|
7
|
+
|
|
8
|
+
from defuser.model_registry import MODEL_CONFIG
|
|
9
|
+
from defuser.modeling.update_module import update_module
|
|
10
|
+
from packaging import version
|
|
11
|
+
import transformers
|
|
12
|
+
from logbar import LogBar
|
|
13
|
+
|
|
14
|
+
logger = LogBar(__name__)
|
|
15
|
+
|
|
16
|
+
def check_model_compatibility(model: nn.Module) -> bool:
|
|
17
|
+
"""Validate model type and transformers version compatibility."""
|
|
18
|
+
config = getattr(model, "config", None)
|
|
19
|
+
model_type = getattr(config, "model_type", None)
|
|
20
|
+
if model_type not in MODEL_CONFIG:
|
|
21
|
+
return False
|
|
22
|
+
|
|
23
|
+
min_ver = MODEL_CONFIG[model_type].get("min_transformers_version")
|
|
24
|
+
current_ver = version.parse(transformers.__version__)
|
|
25
|
+
if min_ver and current_ver < version.parse(min_ver):
|
|
26
|
+
logger.warn(
|
|
27
|
+
f"Skip conversion for model_type={model_type}: "
|
|
28
|
+
f"requires transformers>={min_ver}, current version is {transformers.__version__}."
|
|
29
|
+
)
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
return True
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def convert_model(
|
|
36
|
+
model: nn.Module,
|
|
37
|
+
cleanup_original: bool = False,
|
|
38
|
+
max_layers: int | None = None,
|
|
39
|
+
) -> nn.Module:
|
|
40
|
+
if max_layers is not None and max_layers < 1:
|
|
41
|
+
raise ValueError("max_layers must be >= 1 when provided")
|
|
42
|
+
|
|
43
|
+
# Patch modeling structure for legacy Qwen3 MoE
|
|
44
|
+
#
|
|
45
|
+
# There are two slightlyfis_within_max_layers different checkpoint formats we need to support:
|
|
46
|
+
# 1) Qwen3 MoE
|
|
47
|
+
# 2) Qwen3.5 MoE
|
|
48
|
+
#
|
|
49
|
+
# The key difference is how the expert MLP weights are stored in the original
|
|
50
|
+
# checkpoint (fused vs. defused). Because of that, the amount of work needed
|
|
51
|
+
# after replacing the modeling structure is different.
|
|
52
|
+
#
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
# Step 1: Try applying a lightweight modeling patch
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
# `apply_modeling_patch(model)` only replaces the *modeling structure*
|
|
57
|
+
# (module definitions / forward logic) to match our runtime implementation.
|
|
58
|
+
#
|
|
59
|
+
# For **Qwen3 MoE**, this is sufficient because:
|
|
60
|
+
# - The original checkpoint already stores `mlp.experts` weights in a
|
|
61
|
+
# **defused format**.
|
|
62
|
+
# - In other words, the tensors are already separated as:
|
|
63
|
+
#
|
|
64
|
+
# gate_proj
|
|
65
|
+
# up_proj
|
|
66
|
+
# down_proj
|
|
67
|
+
#
|
|
68
|
+
# - Therefore we only need to swap the modeling implementation so that the
|
|
69
|
+
# module structure matches the expected layout, without touching the
|
|
70
|
+
# underlying tensors.
|
|
71
|
+
#
|
|
72
|
+
# If this patch succeeds, it means the model is in the Qwen3 MoE format and
|
|
73
|
+
# no further tensor transformation is required.
|
|
74
|
+
|
|
75
|
+
# -----------------------------------------------------------------------
|
|
76
|
+
# Step 2: Handle Qwen3.5 MoE checkpoints
|
|
77
|
+
# -----------------------------------------------------------------------
|
|
78
|
+
#
|
|
79
|
+
# If `apply_modeling_patch` fails, we assume the checkpoint corresponds to
|
|
80
|
+
# **Qwen3.5 MoE**.
|
|
81
|
+
#
|
|
82
|
+
# In Qwen3.5 MoE, the expert MLP weights are stored in a **fused format**.
|
|
83
|
+
# Specifically, the checkpoint keeps tensors such as:
|
|
84
|
+
#
|
|
85
|
+
# gate_up_proj
|
|
86
|
+
# down_proj
|
|
87
|
+
#
|
|
88
|
+
# where `gate_proj` and `up_proj` are fused together.
|
|
89
|
+
#
|
|
90
|
+
# Because our runtime modeling expects **defused tensors**, simply replacing
|
|
91
|
+
# the module structure is not enough. We must also convert the stored
|
|
92
|
+
# parameters.
|
|
93
|
+
#
|
|
94
|
+
# `update_module()` performs two tasks:
|
|
95
|
+
#
|
|
96
|
+
# 1) Replace the modeling structure so that it matches the expected
|
|
97
|
+
# defused MoE implementation.
|
|
98
|
+
#
|
|
99
|
+
# 2) Prepare the module for **tensor defusion** of the expert weights.
|
|
100
|
+
#
|
|
101
|
+
# After the structure update, `materialize_model_()` will be invoked to
|
|
102
|
+
# actually split the fused tensors:
|
|
103
|
+
#
|
|
104
|
+
# gate_up_proj --> gate_proj + up_proj
|
|
105
|
+
#
|
|
106
|
+
# and ensure the module finally contains the expected parameters:
|
|
107
|
+
#
|
|
108
|
+
# gate_proj
|
|
109
|
+
# up_proj
|
|
110
|
+
# down_proj
|
|
111
|
+
#
|
|
112
|
+
# This ensures compatibility between the Qwen3.5 fused checkpoint format
|
|
113
|
+
# and the runtime model implementation that operates on defused weights.
|
|
114
|
+
|
|
115
|
+
if not check_model_compatibility(model):
|
|
116
|
+
return model
|
|
117
|
+
|
|
118
|
+
return update_module(
|
|
119
|
+
model,
|
|
120
|
+
cleanup_original=cleanup_original,
|
|
121
|
+
max_layers=max_layers,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
__all__ = ["convert_model"]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 ModelCloud.ai
|
|
2
|
+
# SPDX-FileCopyrightText: 2026 qubitium@modelcloud.ai
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
# Contact: qubitium@modelcloud.ai, x.com/qubitium
|
|
5
|
+
|
|
6
|
+
MODEL_CONFIG = {
|
|
7
|
+
"qwen3_moe": {
|
|
8
|
+
"min_transformers_version": "5.0.0",
|
|
9
|
+
},
|
|
10
|
+
"qwen3_5_moe": {
|
|
11
|
+
"min_transformers_version": "5.2.0",
|
|
12
|
+
},
|
|
13
|
+
"qwen3_5_moe_text": {
|
|
14
|
+
"min_transformers_version": "5.2.0",
|
|
15
|
+
},
|
|
16
|
+
}
|
{defuser-0.0.4/defuser/modeling/fused_moe → defuser-0.0.6/defuser/modeling}/moe_experts_interface.py
RENAMED
|
@@ -56,14 +56,6 @@ KNOWN_PROJECTION_PATTERNS = {
|
|
|
56
56
|
"gate_proj": {"is_input_proj": True, "output_multiplier": 1}, # hidden -> intermediate (gate)
|
|
57
57
|
"up_proj": {"is_input_proj": True, "output_multiplier": 1}, # hidden -> intermediate (up)
|
|
58
58
|
"down_proj": {"is_input_proj": False, "output_multiplier": 1}, # intermediate -> hidden
|
|
59
|
-
# Mixtral-style
|
|
60
|
-
"w1": {"is_input_proj": True, "output_multiplier": 1}, # gate: hidden -> intermediate
|
|
61
|
-
"w2": {"is_input_proj": False, "output_multiplier": 1}, # down: intermediate -> hidden
|
|
62
|
-
"w3": {"is_input_proj": True, "output_multiplier": 1}, # up: hidden -> intermediate
|
|
63
|
-
# DBRX-style
|
|
64
|
-
"v1": {"is_input_proj": True, "output_multiplier": 1},
|
|
65
|
-
"w1_proj": {"is_input_proj": True, "output_multiplier": 1},
|
|
66
|
-
"w2_proj": {"is_input_proj": False, "output_multiplier": 1},
|
|
67
59
|
}
|
|
68
60
|
|
|
69
61
|
|
{defuser-0.0.4/defuser/modeling/fused_moe → defuser-0.0.6/defuser/modeling}/replace_modules.py
RENAMED
|
@@ -16,34 +16,11 @@ import torch
|
|
|
16
16
|
from logbar import LogBar
|
|
17
17
|
from tqdm import tqdm
|
|
18
18
|
|
|
19
|
-
from defuser.
|
|
20
|
-
from defuser.utils.common import is_within_max_layers
|
|
19
|
+
from defuser.utils.common import is_within_max_layers, is_transformers_version_greater_or_equal_5
|
|
21
20
|
|
|
22
21
|
logger = LogBar(__name__)
|
|
23
22
|
|
|
24
23
|
|
|
25
|
-
def is_model_patchable(model: torch.nn.Module) -> bool:
|
|
26
|
-
"""Check if the model has a custom replacement registered via MODEL_CONFIG.
|
|
27
|
-
|
|
28
|
-
Returns True if the model's model_type matches a key in MODEL_CONFIG.
|
|
29
|
-
"""
|
|
30
|
-
if hasattr(model, "config") and hasattr(model.config, "model_type"):
|
|
31
|
-
return model.config.model_type in MODEL_CONFIG
|
|
32
|
-
return False
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def _import_required_replacements(model: torch.nn.Module) -> None:
|
|
36
|
-
"""Import replacement modules required for the model's defuse workflow."""
|
|
37
|
-
if not is_model_patchable(model):
|
|
38
|
-
return
|
|
39
|
-
model_type = model.config.model_type
|
|
40
|
-
module_path = MODEL_CONFIG[model_type].get(PATCH.DEFUSE)
|
|
41
|
-
if not module_path:
|
|
42
|
-
return
|
|
43
|
-
importlib.import_module(module_path)
|
|
44
|
-
logger.debug(f"Loaded replacement module for {model_type}: {module_path}")
|
|
45
|
-
|
|
46
|
-
|
|
47
24
|
def materialize_model(model: torch.nn.Module) -> None:
|
|
48
25
|
def _materialize_module(module: torch.nn.Module) -> None:
|
|
49
26
|
if isinstance(module, ReplacementModuleBase):
|
|
@@ -216,7 +193,7 @@ def _handle_moe_modules(model: torch.nn.Module) -> list[str]:
|
|
|
216
193
|
Returns:
|
|
217
194
|
List of module names that were processed
|
|
218
195
|
"""
|
|
219
|
-
from defuser.modeling.
|
|
196
|
+
from defuser.modeling.moe_experts_interface import (
|
|
220
197
|
is_linear_loop_available,
|
|
221
198
|
prepare_model_for_moe_quantization,
|
|
222
199
|
)
|
|
@@ -259,15 +236,10 @@ def apply_replacements(
|
|
|
259
236
|
Returns:
|
|
260
237
|
The model with modules replaced.
|
|
261
238
|
"""
|
|
262
|
-
_import_required_replacements(model)
|
|
263
|
-
|
|
264
239
|
_log_first_moe_block(model, "before replacement")
|
|
265
240
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
_apply_custom_replacements(model, max_layers=max_layers)
|
|
269
|
-
# if auto_detect_moe and is_transformers_version_greater_or_equal_5():
|
|
270
|
-
# _handle_moe_modules(model)
|
|
241
|
+
if auto_detect_moe and is_transformers_version_greater_or_equal_5():
|
|
242
|
+
_handle_moe_modules(model)
|
|
271
243
|
|
|
272
244
|
_log_first_moe_block(model, "after replacement")
|
|
273
245
|
|
{defuser-0.0.4/defuser/modeling/fused_moe → defuser-0.0.6/defuser/modeling}/update_module.py
RENAMED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
# Adapted from intel/auto-round
|
|
7
7
|
# at https://github.com/intel/auto-round/blob/main/auto_round/special_model_handler.py
|
|
8
8
|
|
|
9
|
-
from defuser.modeling.
|
|
9
|
+
from defuser.modeling.replace_modules import apply_replacements, release_original_module_
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
def update_module(
|
|
@@ -16,8 +16,7 @@ from logbar import LogBar
|
|
|
16
16
|
from packaging import version
|
|
17
17
|
from transformers import AutoConfig
|
|
18
18
|
|
|
19
|
-
from defuser.model_registry import MODEL_CONFIG
|
|
20
|
-
from defuser.utils.common import is_within_max_layers
|
|
19
|
+
from defuser.model_registry import MODEL_CONFIG
|
|
21
20
|
|
|
22
21
|
logger = LogBar(__name__)
|
|
23
22
|
|
|
@@ -105,34 +104,3 @@ def pre_check_config(model_name: str | torch.nn.Module):
|
|
|
105
104
|
except:
|
|
106
105
|
return True
|
|
107
106
|
return True
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
def patch(model: torch.nn.Module, max_layers: int | None = None) -> bool:
|
|
111
|
-
res = pre_check_config(model)
|
|
112
|
-
if not res:
|
|
113
|
-
return False
|
|
114
|
-
model_type = getattr(model.config, "model_type")
|
|
115
|
-
cfg = MODEL_CONFIG[model_type]
|
|
116
|
-
# patch blocks
|
|
117
|
-
for orig_path, custom_path in cfg.get(PATCH.REPLACE_MODULE, []):
|
|
118
|
-
orig_module_path, orig_class_name = orig_path.rsplit(".", 1)
|
|
119
|
-
custom_module_path, custom_class_name = custom_path.rsplit(".", 1)
|
|
120
|
-
try:
|
|
121
|
-
orig_module = importlib.import_module(orig_module_path)
|
|
122
|
-
custom_module = importlib.import_module(custom_module_path)
|
|
123
|
-
custom_class = getattr(custom_module, custom_class_name)
|
|
124
|
-
orig_class = getattr(orig_module, orig_class_name)
|
|
125
|
-
names = []
|
|
126
|
-
for n, m in model.named_modules():
|
|
127
|
-
if isinstance(m, orig_class):
|
|
128
|
-
if not is_within_max_layers(n, max_layers):
|
|
129
|
-
continue
|
|
130
|
-
names.append((n, next(m.parameters()).dtype))
|
|
131
|
-
for (n, orig_dtype) in names:
|
|
132
|
-
model.set_submodule(n, custom_class(model.config).to(orig_dtype), True)
|
|
133
|
-
logger.info(f"Patched module: {orig_path} -> {custom_path}")
|
|
134
|
-
return True
|
|
135
|
-
except Exception as e:
|
|
136
|
-
logger.warn(f"Failed to patch {orig_path}: {e}")
|
|
137
|
-
return False
|
|
138
|
-
return False
|
|
@@ -5,30 +5,34 @@
|
|
|
5
5
|
import torch
|
|
6
6
|
from transformers import AutoConfig, AutoModelForCausalLM, AutoModelForImageTextToText
|
|
7
7
|
|
|
8
|
-
from defuser import
|
|
9
|
-
from defuser.modeling.
|
|
8
|
+
from defuser import convert_model
|
|
9
|
+
from defuser.modeling.replace_modules import materialize_model
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
def test_qwen3_moe():
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
config = AutoConfig.from_pretrained("/monster/data/model/Qwen3-30B-A3B")
|
|
13
|
+
model_id = "Qwen/Qwen3-30B-A3B"
|
|
14
|
+
config = AutoConfig.from_pretrained(model_id)
|
|
16
15
|
config.num_hidden_layers = 1
|
|
17
16
|
model = AutoModelForCausalLM.from_pretrained(
|
|
18
|
-
|
|
17
|
+
model_id,
|
|
19
18
|
config=config,
|
|
20
19
|
ignore_mismatched_sizes=True,
|
|
21
20
|
)
|
|
22
21
|
|
|
23
22
|
assert model.config.model_type == "qwen3_moe"
|
|
24
23
|
|
|
25
|
-
converted =
|
|
24
|
+
converted = convert_model(model, max_layers=1)
|
|
26
25
|
assert converted
|
|
27
|
-
|
|
26
|
+
|
|
27
|
+
experts = model.model.layers[0].mlp.experts
|
|
28
|
+
assert hasattr(experts, "0")
|
|
29
|
+
expert0 = getattr(experts, "0")
|
|
30
|
+
assert hasattr(expert0, "gate_proj")
|
|
31
|
+
assert hasattr(expert0, "up_proj")
|
|
32
|
+
assert hasattr(expert0, "down_proj")
|
|
28
33
|
|
|
29
34
|
|
|
30
35
|
def test_qwen3_5_moe():
|
|
31
|
-
from defuser.modeling.fused_moe.qwen3_5_moe import LinearQwen3_5MoeSparseMoeBlock
|
|
32
36
|
from transformers.models.qwen3_5_moe.modeling_qwen3_5_moe import Qwen3_5MoeSparseMoeBlock
|
|
33
37
|
|
|
34
38
|
config = AutoConfig.from_pretrained("/monster/data/model/Qwen3.5-35B-A3B")
|
|
@@ -50,14 +54,20 @@ def test_qwen3_5_moe():
|
|
|
50
54
|
expected_up = original_moe_block.experts.gate_up_proj[0, intermediate_dim:, :hidden_dim].contiguous().clone()
|
|
51
55
|
expected_down = original_moe_block.experts.down_proj[0, :hidden_dim, :intermediate_dim].contiguous().clone()
|
|
52
56
|
|
|
53
|
-
converted =
|
|
57
|
+
converted = convert_model(model, cleanup_original=False, max_layers=1)
|
|
54
58
|
assert converted
|
|
55
59
|
|
|
56
60
|
moe_block = model.model.language_model.layers[0].mlp
|
|
57
|
-
|
|
61
|
+
experts = moe_block.experts
|
|
62
|
+
|
|
63
|
+
assert hasattr(experts, "0")
|
|
64
|
+
expert0 = getattr(experts, "0")
|
|
65
|
+
assert hasattr(expert0, "gate_proj")
|
|
66
|
+
assert hasattr(expert0, "up_proj")
|
|
67
|
+
assert hasattr(expert0, "down_proj")
|
|
58
68
|
|
|
59
69
|
materialize_model(model.model.language_model.layers[0])
|
|
60
70
|
|
|
61
|
-
torch.testing.assert_close(
|
|
62
|
-
torch.testing.assert_close(
|
|
63
|
-
torch.testing.assert_close(
|
|
71
|
+
torch.testing.assert_close(expert0.gate_proj.weight, expected_gate)
|
|
72
|
+
torch.testing.assert_close(expert0.up_proj.weight, expected_up)
|
|
73
|
+
torch.testing.assert_close(expert0.down_proj.weight, expected_down)
|
defuser-0.0.4/defuser/defuser.py
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
# SPDX-FileCopyrightText: 2026 ModelCloud.ai
|
|
2
|
-
# SPDX-FileCopyrightText: 2026 qubitium@modelcloud.ai
|
|
3
|
-
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
# Contact: qubitium@modelcloud.ai, x.com/qubitium
|
|
5
|
-
|
|
6
|
-
from torch import nn
|
|
7
|
-
|
|
8
|
-
from defuser.modeling.fused_moe.update_module import update_module
|
|
9
|
-
from defuser.utils.hf import patch
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def convert_hf_model(
|
|
13
|
-
model: nn.Module,
|
|
14
|
-
cleanup_original: bool = False,
|
|
15
|
-
max_layers: int | None = None,
|
|
16
|
-
) -> nn.Module:
|
|
17
|
-
if max_layers is not None and max_layers < 1:
|
|
18
|
-
raise ValueError("max_layers must be >= 1 when provided")
|
|
19
|
-
|
|
20
|
-
# Patch modeling structure for legacy Qwen3 MoE
|
|
21
|
-
#
|
|
22
|
-
# There are two slightlyfis_within_max_layers different checkpoint formats we need to support:
|
|
23
|
-
# 1) Qwen3 MoE
|
|
24
|
-
# 2) Qwen3.5 MoE
|
|
25
|
-
#
|
|
26
|
-
# The key difference is how the expert MLP weights are stored in the original
|
|
27
|
-
# checkpoint (fused vs. defused). Because of that, the amount of work needed
|
|
28
|
-
# after replacing the modeling structure is different.
|
|
29
|
-
#
|
|
30
|
-
# ---------------------------------------------------------------------------
|
|
31
|
-
# Step 1: Try applying a lightweight modeling patch
|
|
32
|
-
# ---------------------------------------------------------------------------
|
|
33
|
-
# `apply_modeling_patch(model)` only replaces the *modeling structure*
|
|
34
|
-
# (module definitions / forward logic) to match our runtime implementation.
|
|
35
|
-
#
|
|
36
|
-
# For **Qwen3 MoE**, this is sufficient because:
|
|
37
|
-
# - The original checkpoint already stores `mlp.experts` weights in a
|
|
38
|
-
# **defused format**.
|
|
39
|
-
# - In other words, the tensors are already separated as:
|
|
40
|
-
#
|
|
41
|
-
# gate_proj
|
|
42
|
-
# up_proj
|
|
43
|
-
# down_proj
|
|
44
|
-
#
|
|
45
|
-
# - Therefore we only need to swap the modeling implementation so that the
|
|
46
|
-
# module structure matches the expected layout, without touching the
|
|
47
|
-
# underlying tensors.
|
|
48
|
-
#
|
|
49
|
-
# If this patch succeeds, it means the model is in the Qwen3 MoE format and
|
|
50
|
-
# no further tensor transformation is required.
|
|
51
|
-
is_applied = patch(model, max_layers=max_layers)
|
|
52
|
-
if not is_applied:
|
|
53
|
-
# -----------------------------------------------------------------------
|
|
54
|
-
# Step 2: Handle Qwen3.5 MoE checkpoints
|
|
55
|
-
# -----------------------------------------------------------------------
|
|
56
|
-
#
|
|
57
|
-
# If `apply_modeling_patch` fails, we assume the checkpoint corresponds to
|
|
58
|
-
# **Qwen3.5 MoE**.
|
|
59
|
-
#
|
|
60
|
-
# In Qwen3.5 MoE, the expert MLP weights are stored in a **fused format**.
|
|
61
|
-
# Specifically, the checkpoint keeps tensors such as:
|
|
62
|
-
#
|
|
63
|
-
# gate_up_proj
|
|
64
|
-
# down_proj
|
|
65
|
-
#
|
|
66
|
-
# where `gate_proj` and `up_proj` are fused together.
|
|
67
|
-
#
|
|
68
|
-
# Because our runtime modeling expects **defused tensors**, simply replacing
|
|
69
|
-
# the module structure is not enough. We must also convert the stored
|
|
70
|
-
# parameters.
|
|
71
|
-
#
|
|
72
|
-
# `update_module()` performs two tasks:
|
|
73
|
-
#
|
|
74
|
-
# 1) Replace the modeling structure so that it matches the expected
|
|
75
|
-
# defused MoE implementation.
|
|
76
|
-
#
|
|
77
|
-
# 2) Prepare the module for **tensor defusion** of the expert weights.
|
|
78
|
-
#
|
|
79
|
-
# After the structure update, `materialize_model_()` will be invoked to
|
|
80
|
-
# actually split the fused tensors:
|
|
81
|
-
#
|
|
82
|
-
# gate_up_proj --> gate_proj + up_proj
|
|
83
|
-
#
|
|
84
|
-
# and ensure the module finally contains the expected parameters:
|
|
85
|
-
#
|
|
86
|
-
# gate_proj
|
|
87
|
-
# up_proj
|
|
88
|
-
# down_proj
|
|
89
|
-
#
|
|
90
|
-
# This ensures compatibility between the Qwen3.5 fused checkpoint format
|
|
91
|
-
# and the runtime model implementation that operates on defused weights.
|
|
92
|
-
model = update_module(
|
|
93
|
-
model,
|
|
94
|
-
cleanup_original=cleanup_original,
|
|
95
|
-
max_layers=max_layers,
|
|
96
|
-
)
|
|
97
|
-
return model
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
__all__ = ["convert_hf_model"]
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
# SPDX-FileCopyrightText: 2026 ModelCloud.ai
|
|
2
|
-
# SPDX-FileCopyrightText: 2026 qubitium@modelcloud.ai
|
|
3
|
-
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
# Contact: qubitium@modelcloud.ai, x.com/qubitium
|
|
5
|
-
|
|
6
|
-
from enum import Enum
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class PATCH(str, Enum):
|
|
10
|
-
DEFUSE = "defuse"
|
|
11
|
-
REPLACE_MODULE = "replace_module"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
MODEL_CONFIG = {
|
|
15
|
-
"qwen3_moe": {
|
|
16
|
-
"min_transformers_version": "5.0.0",
|
|
17
|
-
# structure path only replaces modeling structure
|
|
18
|
-
PATCH.REPLACE_MODULE: [
|
|
19
|
-
(
|
|
20
|
-
"transformers.models.qwen3_moe.modeling_qwen3_moe.Qwen3MoeSparseMoeBlock",
|
|
21
|
-
"defuser.modeling.unfused_moe.qwen3_moe.LinearQwen3MoeSparseMoeBlock",
|
|
22
|
-
)
|
|
23
|
-
],
|
|
24
|
-
},
|
|
25
|
-
"qwen3_5_moe": {
|
|
26
|
-
"min_transformers_version": "5.2.0",
|
|
27
|
-
# Replacement module path imported only when the defuse workflow runs
|
|
28
|
-
PATCH.DEFUSE: "defuser.modeling.fused_moe.qwen3_5_moe",
|
|
29
|
-
},
|
|
30
|
-
"qwen3_5_moe_text": {
|
|
31
|
-
"min_transformers_version": "5.2.0",
|
|
32
|
-
# Replacement module path imported only when the defuse workflow runs
|
|
33
|
-
PATCH.DEFUSE: "defuser.modeling.fused_moe.qwen3_5_moe",
|
|
34
|
-
},
|
|
35
|
-
}
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
# SPDX-FileCopyrightText: 2026 ModelCloud.ai
|
|
2
|
-
# SPDX-FileCopyrightText: 2026 qubitium@modelcloud.ai
|
|
3
|
-
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
# Contact: qubitium@modelcloud.ai, x.com/qubitium
|
|
5
|
-
|
|
6
|
-
# Adapted from intel/auto-round
|
|
7
|
-
# at https://github.com/intel/auto-round/blob/main/auto_round/modeling/fused_moe/qwen3_5_moe.py
|
|
8
|
-
|
|
9
|
-
import torch
|
|
10
|
-
from torch.nn import functional as F
|
|
11
|
-
from transformers.models.qwen3_5_moe.modeling_qwen3_5_moe import Qwen3_5MoeMLP
|
|
12
|
-
from transformers.utils.versions import require_version
|
|
13
|
-
|
|
14
|
-
from defuser.modeling.fused_moe.replace_modules import ReplacementModuleBase
|
|
15
|
-
from defuser.utils.device import clear_memory, to_meta, unsupported_meta_device
|
|
16
|
-
|
|
17
|
-
require_version("transformers>=5.2.0")
|
|
18
|
-
|
|
19
|
-
from defuser.utils.model import _update_parameter
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class LinearQwen3_5MoeSparseMoeBlock(ReplacementModuleBase):
|
|
23
|
-
def __init__(self, original, config):
|
|
24
|
-
super().__init__(original)
|
|
25
|
-
self.gate = original.gate
|
|
26
|
-
text_config = config.get_text_config()
|
|
27
|
-
self.shared_expert = original.shared_expert
|
|
28
|
-
self.experts = SequentialQwen3_5MoeExperts(text_config, original.experts)
|
|
29
|
-
self.shared_expert_gate = original.shared_expert_gate
|
|
30
|
-
self.num_experts = text_config.num_experts
|
|
31
|
-
|
|
32
|
-
@classmethod
|
|
33
|
-
def original_module_class(cls) -> str:
|
|
34
|
-
"""Return the class name of the module this replaces."""
|
|
35
|
-
return "Qwen3_5MoeSparseMoeBlock"
|
|
36
|
-
|
|
37
|
-
def _materialize_weights(self) -> None:
|
|
38
|
-
original = self._get_original_module()
|
|
39
|
-
self.experts._materialize_weights(original.experts)
|
|
40
|
-
clear_memory()
|
|
41
|
-
|
|
42
|
-
def experts_forward(
|
|
43
|
-
self,
|
|
44
|
-
hidden_states: torch.Tensor,
|
|
45
|
-
top_k_index: torch.Tensor,
|
|
46
|
-
top_k_weights: torch.Tensor,
|
|
47
|
-
) -> torch.Tensor:
|
|
48
|
-
final_hidden_states = torch.zeros_like(hidden_states)
|
|
49
|
-
with torch.no_grad():
|
|
50
|
-
expert_mask = torch.nn.functional.one_hot(top_k_index, num_classes=self.num_experts)
|
|
51
|
-
expert_mask = expert_mask.permute(2, 1, 0)
|
|
52
|
-
expert_hit = torch.greater(expert_mask.sum(dim=(-1, -2)), 0).nonzero()
|
|
53
|
-
|
|
54
|
-
for expert_idx in expert_hit:
|
|
55
|
-
expert_idx = expert_idx[0]
|
|
56
|
-
if expert_idx == self.num_experts:
|
|
57
|
-
continue
|
|
58
|
-
top_k_pos, token_idx = torch.where(expert_mask[expert_idx])
|
|
59
|
-
current_state = hidden_states[token_idx]
|
|
60
|
-
# gate, up = nn.functional.linear(current_state, self.gate_up_proj[expert_idx]).chunk(2, dim=-1)
|
|
61
|
-
# current_hidden_states = self.act_fn(gate) * up
|
|
62
|
-
# current_hidden_states = nn.functional.linear(current_hidden_states, self.down_proj[expert_idx])
|
|
63
|
-
current_hidden_states = self.experts[expert_idx](current_state)
|
|
64
|
-
current_hidden_states = current_hidden_states * top_k_weights[token_idx, top_k_pos, None]
|
|
65
|
-
final_hidden_states.index_add_(0, token_idx, current_hidden_states.to(final_hidden_states.dtype))
|
|
66
|
-
|
|
67
|
-
return final_hidden_states
|
|
68
|
-
|
|
69
|
-
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
|
|
70
|
-
batch_size, sequence_length, hidden_dim = hidden_states.shape
|
|
71
|
-
hidden_states_reshaped = hidden_states.view(-1, hidden_dim)
|
|
72
|
-
shared_expert_output = self.shared_expert(hidden_states_reshaped)
|
|
73
|
-
_, routing_weights, selected_experts = self.gate(hidden_states_reshaped)
|
|
74
|
-
expert_output = self.experts_forward(hidden_states_reshaped, selected_experts, routing_weights)
|
|
75
|
-
|
|
76
|
-
shared_expert_output = F.sigmoid(self.shared_expert_gate(hidden_states_reshaped)) * shared_expert_output
|
|
77
|
-
|
|
78
|
-
expert_output += shared_expert_output
|
|
79
|
-
expert_output = expert_output.reshape(batch_size, sequence_length, hidden_dim)
|
|
80
|
-
return expert_output
|
|
81
|
-
|
|
82
|
-
@classmethod
|
|
83
|
-
def from_original(
|
|
84
|
-
cls,
|
|
85
|
-
original,
|
|
86
|
-
config,
|
|
87
|
-
**kwargs,
|
|
88
|
-
):
|
|
89
|
-
"""Create an instance from the original module."""
|
|
90
|
-
return cls(original, config)
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
class SequentialQwen3_5MoeExperts(torch.nn.ModuleList):
|
|
94
|
-
def __init__(self, config, original):
|
|
95
|
-
super().__init__()
|
|
96
|
-
self.num_experts = original.gate_up_proj.shape[0]
|
|
97
|
-
intermediate_size = config.moe_intermediate_size
|
|
98
|
-
|
|
99
|
-
super().__init__([Qwen3_5MoeMLP(config, intermediate_size) for _ in range(self.num_experts)])
|
|
100
|
-
|
|
101
|
-
def _materialize_weights(self, original) -> None:
|
|
102
|
-
intermediate_size = original.down_proj.shape[-1]
|
|
103
|
-
if not unsupported_meta_device(original):
|
|
104
|
-
for i in range(self.num_experts):
|
|
105
|
-
gate_up = original.gate_up_proj[i]
|
|
106
|
-
down = original.down_proj[i]
|
|
107
|
-
|
|
108
|
-
gate_proj = gate_up[:intermediate_size, :]
|
|
109
|
-
up_proj = gate_up[intermediate_size:, :]
|
|
110
|
-
|
|
111
|
-
_update_parameter(self[i].gate_proj, "weight", gate_proj.contiguous())
|
|
112
|
-
_update_parameter(self[i].up_proj, "weight", up_proj.contiguous())
|
|
113
|
-
_update_parameter(self[i].down_proj, "weight", down.contiguous())
|
|
114
|
-
del gate_up, down, gate_proj, up_proj
|
|
115
|
-
to_meta(original) # release original experts parameters
|
|
116
|
-
clear_memory()
|
|
File without changes
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
# SPDX-FileCopyrightText: 2026 ModelCloud.ai
|
|
2
|
-
# SPDX-FileCopyrightText: 2026 qubitium@modelcloud.ai
|
|
3
|
-
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
# Contact: qubitium@modelcloud.ai, x.com/qubitium
|
|
5
|
-
|
|
6
|
-
# Adapted from intel/auto-round
|
|
7
|
-
# at https://github.com/intel/auto-round/blob/main/auto_round/modeling/unfused_moe/qwen3_moe.py
|
|
8
|
-
|
|
9
|
-
import torch
|
|
10
|
-
import torch.nn as nn
|
|
11
|
-
from torch.nn import functional as F
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class LinearQwen3MoeSparseMoeBlock(nn.Module):
|
|
15
|
-
def __init__(self, config):
|
|
16
|
-
super().__init__()
|
|
17
|
-
from transformers.models.qwen3_moe.modeling_qwen3_moe import Qwen3MoeMLP
|
|
18
|
-
|
|
19
|
-
self.num_experts = config.num_experts
|
|
20
|
-
self.top_k = config.num_experts_per_tok
|
|
21
|
-
self.norm_topk_prob = config.norm_topk_prob
|
|
22
|
-
|
|
23
|
-
# This must be linear for vllm alignment
|
|
24
|
-
self.gate = nn.Linear(config.hidden_size, config.num_experts, bias=False)
|
|
25
|
-
self.experts = nn.ModuleList(
|
|
26
|
-
[Qwen3MoeMLP(config, intermediate_size=config.moe_intermediate_size) for _ in range(self.num_experts)]
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
|
|
30
|
-
""" """
|
|
31
|
-
batch_size, sequence_length, hidden_dim = hidden_states.shape
|
|
32
|
-
hidden_states = hidden_states.view(-1, hidden_dim)
|
|
33
|
-
# router_logits: (batch * sequence_length, n_experts)
|
|
34
|
-
router_logits = self.gate(hidden_states)
|
|
35
|
-
|
|
36
|
-
routing_weights = F.softmax(router_logits, dim=1, dtype=torch.float)
|
|
37
|
-
routing_weights, selected_experts = torch.topk(routing_weights, self.top_k, dim=-1)
|
|
38
|
-
if self.norm_topk_prob: # only diff with mixtral sparse moe block!
|
|
39
|
-
routing_weights /= routing_weights.sum(dim=-1, keepdim=True)
|
|
40
|
-
# we cast back to the input dtype
|
|
41
|
-
routing_weights = routing_weights.to(hidden_states.dtype)
|
|
42
|
-
|
|
43
|
-
final_hidden_states = torch.zeros(
|
|
44
|
-
(batch_size * sequence_length, hidden_dim), dtype=hidden_states.dtype, device=hidden_states.device
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
# One hot encode the selected experts to create an expert mask
|
|
48
|
-
# this will be used to easily index which expert is going to be solicited
|
|
49
|
-
with torch.no_grad():
|
|
50
|
-
expert_mask = torch.nn.functional.one_hot(selected_experts, num_classes=self.num_experts).permute(2, 1, 0)
|
|
51
|
-
|
|
52
|
-
# Loop over all available experts in the model and perform the computation on each expert
|
|
53
|
-
expert_hit = torch.greater(expert_mask.sum(dim=(-1, -2)), 0).nonzero()
|
|
54
|
-
for expert_idx in expert_hit:
|
|
55
|
-
if expert_idx == self.num_experts:
|
|
56
|
-
continue
|
|
57
|
-
expert_layer = self.experts[expert_idx]
|
|
58
|
-
idx, top_x = torch.where(expert_mask[expert_idx].squeeze(0))
|
|
59
|
-
|
|
60
|
-
# Index the correct hidden states and compute the expert hidden state for
|
|
61
|
-
# the current expert. We need to make sure to multiply the output hidden
|
|
62
|
-
# states by `routing_weights` on the corresponding tokens (top-1 and top-2)
|
|
63
|
-
current_state = hidden_states[None, top_x].reshape(-1, hidden_dim)
|
|
64
|
-
current_hidden_states = expert_layer(current_state) * routing_weights[top_x, idx, None]
|
|
65
|
-
|
|
66
|
-
# However `index_add_` only support torch tensors for indexing so we'll use
|
|
67
|
-
# the `top_x` tensor here.
|
|
68
|
-
final_hidden_states.index_add_(0, top_x, current_hidden_states.to(hidden_states.dtype))
|
|
69
|
-
final_hidden_states = final_hidden_states.reshape(batch_size, sequence_length, hidden_dim)
|
|
70
|
-
return final_hidden_states
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|