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.
Files changed (32) hide show
  1. {defuser-0.0.4 → defuser-0.0.6}/Defuser.egg-info/PKG-INFO +1 -1
  2. {defuser-0.0.4 → defuser-0.0.6}/Defuser.egg-info/SOURCES.txt +3 -7
  3. {defuser-0.0.4 → defuser-0.0.6}/PKG-INFO +1 -1
  4. {defuser-0.0.4 → defuser-0.0.6}/defuser/__init__.py +4 -4
  5. defuser-0.0.6/defuser/defuser.py +124 -0
  6. defuser-0.0.6/defuser/model_registry.py +16 -0
  7. {defuser-0.0.4/defuser/modeling/fused_moe → defuser-0.0.6/defuser/modeling}/moe_experts_interface.py +0 -8
  8. {defuser-0.0.4/defuser/modeling/fused_moe → defuser-0.0.6/defuser/modeling}/replace_modules.py +4 -32
  9. {defuser-0.0.4/defuser/modeling/fused_moe → defuser-0.0.6/defuser/modeling}/update_module.py +1 -1
  10. {defuser-0.0.4 → defuser-0.0.6}/defuser/utils/hf.py +1 -33
  11. {defuser-0.0.4 → defuser-0.0.6}/pyproject.toml +1 -1
  12. {defuser-0.0.4 → defuser-0.0.6}/tests/test_convert_model.py +24 -14
  13. {defuser-0.0.4 → defuser-0.0.6}/tests/test_replace_modules_tracker.py +1 -1
  14. defuser-0.0.4/defuser/defuser.py +0 -100
  15. defuser-0.0.4/defuser/model_registry.py +0 -35
  16. defuser-0.0.4/defuser/modeling/fused_moe/qwen3_5_moe.py +0 -116
  17. defuser-0.0.4/defuser/modeling/unfused_moe/__init__.py +0 -0
  18. defuser-0.0.4/defuser/modeling/unfused_moe/qwen3_moe.py +0 -70
  19. defuser-0.0.4/defuser/utils/__init__.py +0 -0
  20. {defuser-0.0.4 → defuser-0.0.6}/Defuser.egg-info/dependency_links.txt +0 -0
  21. {defuser-0.0.4 → defuser-0.0.6}/Defuser.egg-info/requires.txt +0 -0
  22. {defuser-0.0.4 → defuser-0.0.6}/Defuser.egg-info/top_level.txt +0 -0
  23. {defuser-0.0.4 → defuser-0.0.6}/LICENSE +0 -0
  24. {defuser-0.0.4 → defuser-0.0.6}/README.md +0 -0
  25. {defuser-0.0.4 → defuser-0.0.6}/defuser/modeling/__init__.py +0 -0
  26. {defuser-0.0.4/defuser/modeling/fused_moe → defuser-0.0.6/defuser/utils}/__init__.py +0 -0
  27. {defuser-0.0.4 → defuser-0.0.6}/defuser/utils/common.py +0 -0
  28. {defuser-0.0.4 → defuser-0.0.6}/defuser/utils/device.py +0 -0
  29. {defuser-0.0.4 → defuser-0.0.6}/defuser/utils/model.py +0 -0
  30. {defuser-0.0.4 → defuser-0.0.6}/setup.cfg +0 -0
  31. {defuser-0.0.4 → defuser-0.0.6}/setup.py +0 -0
  32. {defuser-0.0.4 → defuser-0.0.6}/tests/test_device_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Defuser
3
- Version: 0.0.4
3
+ Version: 0.0.6
4
4
  Summary: Model defuser helper for HF Transformers.
5
5
  Author-email: ModelCloud <qubitium@modelcloud.ai>
6
6
  License-Expression: Apache-2.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/fused_moe/__init__.py
15
- defuser/modeling/fused_moe/moe_experts_interface.py
16
- defuser/modeling/fused_moe/qwen3_5_moe.py
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Defuser
3
- Version: 0.0.4
3
+ Version: 0.0.6
4
4
  Summary: Model defuser helper for HF Transformers.
5
5
  Author-email: ModelCloud <qubitium@modelcloud.ai>
6
6
  License-Expression: Apache-2.0
@@ -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 convert_hf_model(*args, **kwargs):
6
+ def convert_model(*args, **kwargs):
7
7
  """Lazily import conversion entrypoint to avoid import-time cycles."""
8
- from .defuser import convert_hf_model as _convert_hf_model
8
+ from .defuser import convert_model as _convert_model
9
9
 
10
- return _convert_hf_model(*args, **kwargs)
10
+ return _convert_model(*args, **kwargs)
11
11
 
12
12
 
13
- __all__ = ["convert_hf_model"]
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
+ }
@@ -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
 
@@ -16,34 +16,11 @@ import torch
16
16
  from logbar import LogBar
17
17
  from tqdm import tqdm
18
18
 
19
- from defuser.model_registry import MODEL_CONFIG, PATCH
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.fused_moe.moe_experts_interface import (
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
- # Custom replacements first
267
- if is_model_patchable(model):
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
 
@@ -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.fused_moe.replace_modules import apply_replacements, release_original_module_
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, PATCH
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
@@ -9,7 +9,7 @@ build-backend = "setuptools.build_meta"
9
9
 
10
10
  [project]
11
11
  name = "Defuser"
12
- version = "0.0.4"
12
+ version = "0.0.6"
13
13
  description = "Model defuser helper for HF Transformers."
14
14
  readme = "README.md"
15
15
  requires-python = ">=3.9"
@@ -5,30 +5,34 @@
5
5
  import torch
6
6
  from transformers import AutoConfig, AutoModelForCausalLM, AutoModelForImageTextToText
7
7
 
8
- from defuser import convert_hf_model
9
- from defuser.modeling.fused_moe.replace_modules import materialize_model
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
- from defuser.modeling.unfused_moe.qwen3_moe import LinearQwen3MoeSparseMoeBlock
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
- "/monster/data/model/Qwen3-30B-A3B",
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 = convert_hf_model(model, max_layers=1)
24
+ converted = convert_model(model, max_layers=1)
26
25
  assert converted
27
- assert isinstance(model.model.layers[0].mlp, LinearQwen3MoeSparseMoeBlock)
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 = convert_hf_model(model, cleanup_original=False, max_layers=1)
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
- assert isinstance(moe_block, LinearQwen3_5MoeSparseMoeBlock)
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(moe_block.experts[0].gate_proj.weight, expected_gate)
62
- torch.testing.assert_close(moe_block.experts[0].up_proj.weight, expected_up)
63
- torch.testing.assert_close(moe_block.experts[0].down_proj.weight, expected_down)
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)
@@ -9,7 +9,7 @@ import weakref
9
9
  import pytest
10
10
  import torch
11
11
 
12
- from defuser.modeling.fused_moe.replace_modules import (
12
+ from defuser.modeling.replace_modules import (
13
13
  ModuleReplacementTracker,
14
14
  ReplacementModuleBase,
15
15
  release_original_module_,
@@ -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