Defuser 0.0.18__tar.gz → 0.0.19__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 (47) hide show
  1. {defuser-0.0.18 → defuser-0.0.19}/Defuser.egg-info/PKG-INFO +9 -5
  2. {defuser-0.0.18 → defuser-0.0.19}/Defuser.egg-info/SOURCES.txt +1 -0
  3. {defuser-0.0.18 → defuser-0.0.19}/PKG-INFO +9 -5
  4. {defuser-0.0.18 → defuser-0.0.19}/README.md +7 -4
  5. {defuser-0.0.18 → defuser-0.0.19}/defuser/checkpoint_ops.py +4 -0
  6. {defuser-0.0.18 → defuser-0.0.19}/defuser/defuser.py +21 -3
  7. defuser-0.0.19/defuser/modeling/__init__.py +4 -0
  8. {defuser-0.0.18 → defuser-0.0.19}/defuser/modeling/glm4v.py +4 -0
  9. {defuser-0.0.18 → defuser-0.0.19}/defuser/modeling/moe_experts_interface.py +60 -29
  10. defuser-0.0.19/defuser/modeling/unfused_moe/__init__.py +4 -0
  11. defuser-0.0.19/defuser/utils/__init__.py +4 -0
  12. {defuser-0.0.18 → defuser-0.0.19}/pyproject.toml +2 -1
  13. defuser-0.0.19/setup.py +7 -0
  14. {defuser-0.0.18 → defuser-0.0.19}/tests/test_candidate_coverage.py +31 -3
  15. {defuser-0.0.18 → defuser-0.0.19}/tests/test_meta_model_defusion.py +20 -1
  16. defuser-0.0.19/tests/test_moe_experts_interface.py +62 -0
  17. defuser-0.0.18/defuser/modeling/__init__.py +0 -0
  18. defuser-0.0.18/defuser/modeling/unfused_moe/__init__.py +0 -0
  19. defuser-0.0.18/defuser/utils/__init__.py +0 -0
  20. defuser-0.0.18/setup.py +0 -3
  21. {defuser-0.0.18 → defuser-0.0.19}/Defuser.egg-info/dependency_links.txt +0 -0
  22. {defuser-0.0.18 → defuser-0.0.19}/Defuser.egg-info/requires.txt +0 -0
  23. {defuser-0.0.18 → defuser-0.0.19}/Defuser.egg-info/top_level.txt +0 -0
  24. {defuser-0.0.18 → defuser-0.0.19}/LICENSE +0 -0
  25. {defuser-0.0.18 → defuser-0.0.19}/defuser/__init__.py +0 -0
  26. {defuser-0.0.18 → defuser-0.0.19}/defuser/model_registry.py +0 -0
  27. {defuser-0.0.18 → defuser-0.0.19}/defuser/modeling/model_patches.py +0 -0
  28. {defuser-0.0.18 → defuser-0.0.19}/defuser/modeling/replace_modules.py +0 -0
  29. {defuser-0.0.18 → defuser-0.0.19}/defuser/modeling/runtime_defusion.py +0 -0
  30. {defuser-0.0.18 → defuser-0.0.19}/defuser/modeling/unfused_moe/common.py +0 -0
  31. {defuser-0.0.18 → defuser-0.0.19}/defuser/modeling/unfused_moe/glm4_moe.py +0 -0
  32. {defuser-0.0.18 → defuser-0.0.19}/defuser/modeling/unfused_moe/glm4_moe_lite.py +0 -0
  33. {defuser-0.0.18 → defuser-0.0.19}/defuser/modeling/unfused_moe/mixtral.py +0 -0
  34. {defuser-0.0.18 → defuser-0.0.19}/defuser/modeling/unfused_moe/qwen2_moe.py +0 -0
  35. {defuser-0.0.18 → defuser-0.0.19}/defuser/modeling/unfused_moe/qwen3_moe.py +0 -0
  36. {defuser-0.0.18 → defuser-0.0.19}/defuser/modeling/unfused_moe/qwen3_next.py +0 -0
  37. {defuser-0.0.18 → defuser-0.0.19}/defuser/modeling/unfused_moe/qwen3_omni_moe.py +0 -0
  38. {defuser-0.0.18 → defuser-0.0.19}/defuser/modeling/update_module.py +0 -0
  39. {defuser-0.0.18 → defuser-0.0.19}/defuser/utils/common.py +0 -0
  40. {defuser-0.0.18 → defuser-0.0.19}/defuser/utils/device.py +0 -0
  41. {defuser-0.0.18 → defuser-0.0.19}/defuser/utils/hf.py +0 -0
  42. {defuser-0.0.18 → defuser-0.0.19}/defuser/utils/model.py +0 -0
  43. {defuser-0.0.18 → defuser-0.0.19}/setup.cfg +0 -0
  44. {defuser-0.0.18 → defuser-0.0.19}/tests/test_convert_model.py +0 -0
  45. {defuser-0.0.18 → defuser-0.0.19}/tests/test_device_utils.py +0 -0
  46. {defuser-0.0.18 → defuser-0.0.19}/tests/test_filter_rules.py +0 -0
  47. {defuser-0.0.18 → defuser-0.0.19}/tests/test_replace_modules_tracker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Defuser
3
- Version: 0.0.18
3
+ Version: 0.0.19
4
4
  Summary: Model defuser helper for HF Transformers.
5
5
  Author-email: ModelCloud <qubitium@modelcloud.ai>
6
6
  License-Expression: Apache-2.0
@@ -13,6 +13,7 @@ Classifier: Programming Language :: Python :: 3.11
13
13
  Classifier: Programming Language :: Python :: 3.12
14
14
  Classifier: Programming Language :: Python :: 3.13
15
15
  Classifier: Programming Language :: Python :: 3.14
16
+ Classifier: Programming Language :: Python :: 3.15
16
17
  Classifier: Programming Language :: C
17
18
  Classifier: Topic :: Software Development :: Libraries
18
19
  Classifier: Topic :: Text Processing :: Linguistic
@@ -48,7 +49,7 @@ Depending on the model family, Defuser can:
48
49
 
49
50
  - patch a supported model class before load so HF instantiates a defused block directly
50
51
  - split fused tensors such as `gate_up_proj` into `gate_proj` + `up_proj`
51
- - convert 3D expert tensors into numbered expert `nn.Linear` modules
52
+ - convert 3D expert tensors, including registered expert buffers, into numbered expert `nn.Linear` modules
52
53
  - preserve the original fused math while presenting a naive module structure again
53
54
 
54
55
  Public API:
@@ -58,8 +59,9 @@ from defuser import convert_model, replace_fused_blocks
58
59
  ```
59
60
 
60
61
  - `replace_fused_blocks(model_type)` patches supported HF model classes before `from_pretrained()` or direct model construction.
61
- - `convert_model(model, cleanup_original=True, max_layers=None, filter=None)` converts an already loaded model in place. This is the runtime defusion path for supported post-load expert and MLP conversions, including `qwen3_5_moe` style checkpoints.
62
+ - `convert_model(model, cleanup_original=False, max_layers=None, filter=None)` converts an already loaded model in place. This is the runtime defusion path for supported post-load expert and MLP conversions, including `qwen3_5_moe` style checkpoints.
62
63
  - Defuser is designed and CI-tested for `transformers>=5.3.0`, and support is only offered for that version range. Older versions log a warning on these public APIs and are skipped as unsupported.
64
+ - Some model families appear in both support tables. Full models can be prepatched with `replace_fused_blocks(...)`, while standalone fused expert modules from those same families can still be runtime-defused with `convert_model(...)`.
63
65
 
64
66
  `filter` is an optional list of PCRE regex rules evaluated against full module paths such as `model.layers.0.mlp.experts`:
65
67
 
@@ -71,7 +73,7 @@ from defuser import convert_model, replace_fused_blocks
71
73
 
72
74
  ## Supported Models
73
75
 
74
- Defuser currently supports the following `transformers==5.3.0` `model_type` values.
76
+ Defuser currently supports the following `transformers>=5.3.0` `model_type` values.
75
77
 
76
78
  ### `replace_fused_blocks(model_type)` before load
77
79
 
@@ -90,7 +92,7 @@ Defuser currently supports the following `transformers==5.3.0` `model_type` valu
90
92
 
91
93
  | Pattern | Supported model types | Defused op performed |
92
94
  | --- | --- | --- |
93
- | Standard routed expert tensors | `deepseek_v2`, `dots1`, `ernie4_5_moe`, `ernie4_5_vl_moe`, `exaone_moe`, `flex_olmo`, `glm4_moe_lite`, `glm4v_moe`, `hunyuan_v1_moe`, `jamba`, `lfm2_moe`, `minimax`, `minimax_m2`, `olmoe`, `qwen3_vl_moe`, `solar_open` | Splits fused expert tensors into numbered expert `nn.Linear` modules with per-expert `gate_proj`, `up_proj`, and `down_proj`. |
95
+ | Standard routed expert tensors | `deepseek_v2`, `dots1`, `ernie4_5_moe`, `ernie4_5_vl_moe`, `exaone_moe`, `flex_olmo`, `glm4_moe_lite`, `glm4v_moe`, `hunyuan_v1_moe`, `jamba`, `lfm2_moe`, `minimax`, `minimax_m2`, `olmoe`, `qwen3_vl_moe`, `solar_open` | Splits fused expert tensors or registered expert buffers into numbered expert `nn.Linear` modules with per-expert `gate_proj`, `up_proj`, and `down_proj`. |
94
96
  | Mixed sparse and shared experts | `deepseek_v3`, `glm_moe_dsa`, `qwen3_5_moe`, `qwen3_5_moe_text` | Runtime expert tensor defusion for routed experts while preserving the model's shared-expert path. |
95
97
  | Transposed or packed expert tensors | `gpt_oss`, `phimoe` | Splits transposed fused expert `gate_up_proj` tensors into per-expert `gate_proj` + `up_proj`, preserves expert bias when present, and converts expert tensors into numbered expert `nn.Linear` modules. |
96
98
  | Flattened expert layout | `dbrx` | Rebuilds the flattened DBRX expert FFN weights into numbered expert `gate_proj`, `up_proj`, and `down_proj` `nn.Linear` modules. |
@@ -125,6 +127,8 @@ converted = convert_model(model)
125
127
  print(converted) # True when runtime defusion happened
126
128
  ```
127
129
 
130
+ `convert_model(model)` also preserves meta-device construction for supported meta-initialized models, so structural validation can run without materializing weights.
131
+
128
132
  Use `filter` when only specific blocks should be defused:
129
133
 
130
134
  ```python
@@ -37,4 +37,5 @@ tests/test_convert_model.py
37
37
  tests/test_device_utils.py
38
38
  tests/test_filter_rules.py
39
39
  tests/test_meta_model_defusion.py
40
+ tests/test_moe_experts_interface.py
40
41
  tests/test_replace_modules_tracker.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Defuser
3
- Version: 0.0.18
3
+ Version: 0.0.19
4
4
  Summary: Model defuser helper for HF Transformers.
5
5
  Author-email: ModelCloud <qubitium@modelcloud.ai>
6
6
  License-Expression: Apache-2.0
@@ -13,6 +13,7 @@ Classifier: Programming Language :: Python :: 3.11
13
13
  Classifier: Programming Language :: Python :: 3.12
14
14
  Classifier: Programming Language :: Python :: 3.13
15
15
  Classifier: Programming Language :: Python :: 3.14
16
+ Classifier: Programming Language :: Python :: 3.15
16
17
  Classifier: Programming Language :: C
17
18
  Classifier: Topic :: Software Development :: Libraries
18
19
  Classifier: Topic :: Text Processing :: Linguistic
@@ -48,7 +49,7 @@ Depending on the model family, Defuser can:
48
49
 
49
50
  - patch a supported model class before load so HF instantiates a defused block directly
50
51
  - split fused tensors such as `gate_up_proj` into `gate_proj` + `up_proj`
51
- - convert 3D expert tensors into numbered expert `nn.Linear` modules
52
+ - convert 3D expert tensors, including registered expert buffers, into numbered expert `nn.Linear` modules
52
53
  - preserve the original fused math while presenting a naive module structure again
53
54
 
54
55
  Public API:
@@ -58,8 +59,9 @@ from defuser import convert_model, replace_fused_blocks
58
59
  ```
59
60
 
60
61
  - `replace_fused_blocks(model_type)` patches supported HF model classes before `from_pretrained()` or direct model construction.
61
- - `convert_model(model, cleanup_original=True, max_layers=None, filter=None)` converts an already loaded model in place. This is the runtime defusion path for supported post-load expert and MLP conversions, including `qwen3_5_moe` style checkpoints.
62
+ - `convert_model(model, cleanup_original=False, max_layers=None, filter=None)` converts an already loaded model in place. This is the runtime defusion path for supported post-load expert and MLP conversions, including `qwen3_5_moe` style checkpoints.
62
63
  - Defuser is designed and CI-tested for `transformers>=5.3.0`, and support is only offered for that version range. Older versions log a warning on these public APIs and are skipped as unsupported.
64
+ - Some model families appear in both support tables. Full models can be prepatched with `replace_fused_blocks(...)`, while standalone fused expert modules from those same families can still be runtime-defused with `convert_model(...)`.
63
65
 
64
66
  `filter` is an optional list of PCRE regex rules evaluated against full module paths such as `model.layers.0.mlp.experts`:
65
67
 
@@ -71,7 +73,7 @@ from defuser import convert_model, replace_fused_blocks
71
73
 
72
74
  ## Supported Models
73
75
 
74
- Defuser currently supports the following `transformers==5.3.0` `model_type` values.
76
+ Defuser currently supports the following `transformers>=5.3.0` `model_type` values.
75
77
 
76
78
  ### `replace_fused_blocks(model_type)` before load
77
79
 
@@ -90,7 +92,7 @@ Defuser currently supports the following `transformers==5.3.0` `model_type` valu
90
92
 
91
93
  | Pattern | Supported model types | Defused op performed |
92
94
  | --- | --- | --- |
93
- | Standard routed expert tensors | `deepseek_v2`, `dots1`, `ernie4_5_moe`, `ernie4_5_vl_moe`, `exaone_moe`, `flex_olmo`, `glm4_moe_lite`, `glm4v_moe`, `hunyuan_v1_moe`, `jamba`, `lfm2_moe`, `minimax`, `minimax_m2`, `olmoe`, `qwen3_vl_moe`, `solar_open` | Splits fused expert tensors into numbered expert `nn.Linear` modules with per-expert `gate_proj`, `up_proj`, and `down_proj`. |
95
+ | Standard routed expert tensors | `deepseek_v2`, `dots1`, `ernie4_5_moe`, `ernie4_5_vl_moe`, `exaone_moe`, `flex_olmo`, `glm4_moe_lite`, `glm4v_moe`, `hunyuan_v1_moe`, `jamba`, `lfm2_moe`, `minimax`, `minimax_m2`, `olmoe`, `qwen3_vl_moe`, `solar_open` | Splits fused expert tensors or registered expert buffers into numbered expert `nn.Linear` modules with per-expert `gate_proj`, `up_proj`, and `down_proj`. |
94
96
  | Mixed sparse and shared experts | `deepseek_v3`, `glm_moe_dsa`, `qwen3_5_moe`, `qwen3_5_moe_text` | Runtime expert tensor defusion for routed experts while preserving the model's shared-expert path. |
95
97
  | Transposed or packed expert tensors | `gpt_oss`, `phimoe` | Splits transposed fused expert `gate_up_proj` tensors into per-expert `gate_proj` + `up_proj`, preserves expert bias when present, and converts expert tensors into numbered expert `nn.Linear` modules. |
96
98
  | Flattened expert layout | `dbrx` | Rebuilds the flattened DBRX expert FFN weights into numbered expert `gate_proj`, `up_proj`, and `down_proj` `nn.Linear` modules. |
@@ -125,6 +127,8 @@ converted = convert_model(model)
125
127
  print(converted) # True when runtime defusion happened
126
128
  ```
127
129
 
130
+ `convert_model(model)` also preserves meta-device construction for supported meta-initialized models, so structural validation can run without materializing weights.
131
+
128
132
  Use `filter` when only specific blocks should be defused:
129
133
 
130
134
  ```python
@@ -23,7 +23,7 @@ Depending on the model family, Defuser can:
23
23
 
24
24
  - patch a supported model class before load so HF instantiates a defused block directly
25
25
  - split fused tensors such as `gate_up_proj` into `gate_proj` + `up_proj`
26
- - convert 3D expert tensors into numbered expert `nn.Linear` modules
26
+ - convert 3D expert tensors, including registered expert buffers, into numbered expert `nn.Linear` modules
27
27
  - preserve the original fused math while presenting a naive module structure again
28
28
 
29
29
  Public API:
@@ -33,8 +33,9 @@ from defuser import convert_model, replace_fused_blocks
33
33
  ```
34
34
 
35
35
  - `replace_fused_blocks(model_type)` patches supported HF model classes before `from_pretrained()` or direct model construction.
36
- - `convert_model(model, cleanup_original=True, max_layers=None, filter=None)` converts an already loaded model in place. This is the runtime defusion path for supported post-load expert and MLP conversions, including `qwen3_5_moe` style checkpoints.
36
+ - `convert_model(model, cleanup_original=False, max_layers=None, filter=None)` converts an already loaded model in place. This is the runtime defusion path for supported post-load expert and MLP conversions, including `qwen3_5_moe` style checkpoints.
37
37
  - Defuser is designed and CI-tested for `transformers>=5.3.0`, and support is only offered for that version range. Older versions log a warning on these public APIs and are skipped as unsupported.
38
+ - Some model families appear in both support tables. Full models can be prepatched with `replace_fused_blocks(...)`, while standalone fused expert modules from those same families can still be runtime-defused with `convert_model(...)`.
38
39
 
39
40
  `filter` is an optional list of PCRE regex rules evaluated against full module paths such as `model.layers.0.mlp.experts`:
40
41
 
@@ -46,7 +47,7 @@ from defuser import convert_model, replace_fused_blocks
46
47
 
47
48
  ## Supported Models
48
49
 
49
- Defuser currently supports the following `transformers==5.3.0` `model_type` values.
50
+ Defuser currently supports the following `transformers>=5.3.0` `model_type` values.
50
51
 
51
52
  ### `replace_fused_blocks(model_type)` before load
52
53
 
@@ -65,7 +66,7 @@ Defuser currently supports the following `transformers==5.3.0` `model_type` valu
65
66
 
66
67
  | Pattern | Supported model types | Defused op performed |
67
68
  | --- | --- | --- |
68
- | Standard routed expert tensors | `deepseek_v2`, `dots1`, `ernie4_5_moe`, `ernie4_5_vl_moe`, `exaone_moe`, `flex_olmo`, `glm4_moe_lite`, `glm4v_moe`, `hunyuan_v1_moe`, `jamba`, `lfm2_moe`, `minimax`, `minimax_m2`, `olmoe`, `qwen3_vl_moe`, `solar_open` | Splits fused expert tensors into numbered expert `nn.Linear` modules with per-expert `gate_proj`, `up_proj`, and `down_proj`. |
69
+ | Standard routed expert tensors | `deepseek_v2`, `dots1`, `ernie4_5_moe`, `ernie4_5_vl_moe`, `exaone_moe`, `flex_olmo`, `glm4_moe_lite`, `glm4v_moe`, `hunyuan_v1_moe`, `jamba`, `lfm2_moe`, `minimax`, `minimax_m2`, `olmoe`, `qwen3_vl_moe`, `solar_open` | Splits fused expert tensors or registered expert buffers into numbered expert `nn.Linear` modules with per-expert `gate_proj`, `up_proj`, and `down_proj`. |
69
70
  | Mixed sparse and shared experts | `deepseek_v3`, `glm_moe_dsa`, `qwen3_5_moe`, `qwen3_5_moe_text` | Runtime expert tensor defusion for routed experts while preserving the model's shared-expert path. |
70
71
  | Transposed or packed expert tensors | `gpt_oss`, `phimoe` | Splits transposed fused expert `gate_up_proj` tensors into per-expert `gate_proj` + `up_proj`, preserves expert bias when present, and converts expert tensors into numbered expert `nn.Linear` modules. |
71
72
  | Flattened expert layout | `dbrx` | Rebuilds the flattened DBRX expert FFN weights into numbered expert `gate_proj`, `up_proj`, and `down_proj` `nn.Linear` modules. |
@@ -100,6 +101,8 @@ converted = convert_model(model)
100
101
  print(converted) # True when runtime defusion happened
101
102
  ```
102
103
 
104
+ `convert_model(model)` also preserves meta-device construction for supported meta-initialized models, so structural validation can run without materializing weights.
105
+
103
106
  Use `filter` when only specific blocks should be defused:
104
107
 
105
108
  ```python
@@ -1,3 +1,7 @@
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
1
5
  import torch
2
6
  from transformers.core_model_loading import Chunk, Concatenate, ConversionOps, MergeModulelist
3
7
 
@@ -36,9 +36,26 @@ def get_checkpoint_conversion_mapping(model_type):
36
36
 
37
37
 
38
38
  class PatchError(Exception):
39
+ """Raised when Defuser cannot patch a registered Transformers class."""
40
+
39
41
  pass
40
42
 
41
43
 
44
+ def _has_prebuilt_replacements(model: nn.Module, model_type: str) -> bool:
45
+ """Detect models that were already instantiated with registry-backed replacements."""
46
+ replacement_paths = MODEL_CONFIG[model_type].get(PATCH.REPLACE_MODULE, [])
47
+ replacement_class_paths = {custom_path for _, custom_path in replacement_paths}
48
+ if not replacement_class_paths:
49
+ return False
50
+
51
+ for module in model.modules():
52
+ class_path = f"{module.__class__.__module__}.{module.__class__.__name__}"
53
+ if class_path in replacement_class_paths:
54
+ return True
55
+
56
+ return False
57
+
58
+
42
59
  def replace_fused_blocks(model_type: str) -> bool:
43
60
  """Patch supported HF model classes so future loads instantiate defused blocks."""
44
61
  if warn_if_public_api_transformers_unsupported("replace_fused_blocks()", logger):
@@ -202,9 +219,10 @@ def convert_model(
202
219
 
203
220
  apply_model_patches(model, max_layers=max_layers, filter_rules=filter)
204
221
 
205
- # If fused blocks have already been structurally replaced at load model before,
206
- # there is no need to perform runtime defusing again
207
- if MODEL_CONFIG[model.config.model_type].get(PATCH.REPLACE_MODULE):
222
+ # Full models patched at construction time already contain the defused
223
+ # replacement modules, but standalone experts from those model types can
224
+ # still use runtime tensor defusion.
225
+ if _has_prebuilt_replacements(model, model.config.model_type):
208
226
  return False
209
227
 
210
228
  # Perform runtime defusing of fused projections
@@ -0,0 +1,4 @@
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
@@ -1,3 +1,7 @@
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
1
5
  from torch import nn
2
6
  from transformers.activations import ACT2FN
3
7
 
@@ -292,28 +292,36 @@ def _install_instance_forward(module: nn.Module, implementation: str) -> None:
292
292
  def _detect_expert_projections(module: nn.Module) -> dict[str, dict]:
293
293
  """Detect which expert projections exist in the module.
294
294
 
295
- This function scans the module for any 3D nn.Parameter attributes.
296
- It first checks known projection names, then discovers any unknown 3D parameters.
295
+ This function scans the module for any registered 3D Parameter / Tensor
296
+ attributes. It first checks known projection names, then discovers any
297
+ unknown 3D registered tensors.
297
298
 
298
299
  Returns:
299
300
  Dict mapping projection names to their config, only for projections that exist
300
- as 3D nn.Parameter in the module.
301
+ as 3D registered Parameter / Tensor in the module.
301
302
  """
302
303
  detected = {}
304
+ # Only inspect registered tensors here. Scanning arbitrary attributes can
305
+ # trigger unrelated properties such as Transformers' `loss_function`.
306
+ local_tensors = {
307
+ name: tensor
308
+ for registry in (module._parameters, module._buffers)
309
+ for name, tensor in registry.items()
310
+ if isinstance(tensor, torch.Tensor)
311
+ }
303
312
 
304
313
  # First, check known projection patterns
305
314
  for proj_name, config in KNOWN_PROJECTION_PATTERNS.items():
306
- param = getattr(module, proj_name, None)
307
- if param is not None and isinstance(param, nn.Parameter) and param.dim() == 3:
315
+ param = local_tensors.get(proj_name)
316
+ if param is not None and param.dim() == 3:
308
317
  detected[proj_name] = config
309
318
 
310
- # If no known patterns found, scan for any 3D Parameter (future-proofing)
319
+ # If no known patterns found, scan for any 3D registered tensor (future-proofing)
311
320
  if not detected:
312
- for attr_name in dir(module):
321
+ for attr_name, param in local_tensors.items():
313
322
  if attr_name.startswith("_"):
314
323
  continue
315
- param = getattr(module, attr_name, None)
316
- if param is not None and isinstance(param, nn.Parameter) and param.dim() == 3:
324
+ if param is not None and param.dim() == 3:
317
325
  # Use default config for unknown projections
318
326
  if DEBUG_ON: logger.debug(f"Discovered unknown 3D projection: {attr_name}")
319
327
  detected[attr_name] = {"is_input_proj": True, "output_multiplier": 1}
@@ -321,6 +329,29 @@ def _detect_expert_projections(module: nn.Module) -> dict[str, dict]:
321
329
  return detected
322
330
 
323
331
 
332
+ def _get_registered_tensor(module: nn.Module, name: str) -> torch.Tensor | None:
333
+ """Return a direct registered parameter or buffer without touching properties."""
334
+
335
+ tensor = module._parameters.get(name)
336
+ if isinstance(tensor, torch.Tensor):
337
+ return tensor
338
+
339
+ tensor = module._buffers.get(name)
340
+ if isinstance(tensor, torch.Tensor):
341
+ return tensor
342
+
343
+ return None
344
+
345
+
346
+ def _set_registered_tensor_like(module: nn.Module, name: str, tensor: torch.Tensor, source: torch.Tensor) -> None:
347
+ """Register ``tensor`` using the same parameter-vs-buffer kind as ``source``."""
348
+
349
+ if isinstance(source, nn.Parameter):
350
+ module.register_parameter(name, nn.Parameter(tensor, requires_grad=source.requires_grad))
351
+ else:
352
+ module.register_buffer(name, tensor)
353
+
354
+
324
355
  def _experts_supports_decorator(module: nn.Module) -> bool:
325
356
  """Check if experts module supports @use_experts_implementation decorator.
326
357
 
@@ -334,11 +365,11 @@ def _experts_supports_decorator(module: nn.Module) -> bool:
334
365
  return hasattr(forward_method, "__wrapped__")
335
366
 
336
367
 
337
- def _infer_dimensions(param: nn.Parameter, config: dict, is_transposed: bool) -> tuple[int, int]:
368
+ def _infer_dimensions(param: torch.Tensor, config: dict, is_transposed: bool) -> tuple[int, int]:
338
369
  """Infer input and output dimensions for a projection.
339
370
 
340
371
  Args:
341
- param: The 3D parameter (num_experts, dim1, dim2)
372
+ param: The 3D projection tensor (num_experts, dim1, dim2)
342
373
  config: Projection config with is_input_proj and output_multiplier
343
374
  is_transposed: Whether weights are stored transposed
344
375
 
@@ -370,7 +401,7 @@ def _unfuse_single_projection(
370
401
  dtype: torch.dtype,
371
402
  target_device: torch.device,
372
403
  ) -> list | None:
373
- """Unfuse a single projection from 3D Parameter to a list of Linear layers.
404
+ """Unfuse a single projection from a 3D registered tensor to Linear layers.
374
405
 
375
406
  Optimized to keep peak device memory low while preserving the module's
376
407
  original device placement:
@@ -390,8 +421,8 @@ def _unfuse_single_projection(
390
421
  Returns:
391
422
  List of Linear layers, or None if projection doesn't exist
392
423
  """
393
- param = getattr(module, proj_name, None)
394
- if param is None or not isinstance(param, nn.Parameter) or param.dim() != 3:
424
+ param = _get_registered_tensor(module, proj_name)
425
+ if param is None or param.dim() != 3:
395
426
  return None
396
427
 
397
428
  # Get projection config
@@ -402,17 +433,17 @@ def _unfuse_single_projection(
402
433
 
403
434
  # Check for bias
404
435
  bias_name = f"{proj_name}_bias"
405
- bias_param = getattr(module, bias_name, None)
406
- has_bias = bias_param is not None
436
+ bias_param = _get_registered_tensor(module, bias_name)
437
+ has_bias = isinstance(bias_param, torch.Tensor)
407
438
 
408
439
  source_device = param.device
409
440
  is_meta = source_device.type == "meta"
410
- weight_requires_grad = param.requires_grad
441
+ weight_requires_grad = param.requires_grad if isinstance(param, nn.Parameter) else False
411
442
 
412
443
  # Prepare weight slices on CPU in batch (single D2H transfer + batch transpose)
413
444
  if not is_meta:
414
445
  # Single transfer: GPU -> CPU (or no-op if already on CPU)
415
- weights_cpu = param.data.cpu() # (num_experts, dim1, dim2)
446
+ weights_cpu = param.detach().cpu() # (num_experts, dim1, dim2)
416
447
  if is_transposed:
417
448
  # Batch transpose: (num_experts, in, out) -> (num_experts, out, in)
418
449
  weights_cpu = weights_cpu.transpose(1, 2)
@@ -425,12 +456,12 @@ def _unfuse_single_projection(
425
456
  weight_slices = weights_cpu.unbind(0)
426
457
 
427
458
  if has_bias:
428
- bias_cpu = bias_param.data.cpu()
459
+ bias_cpu = bias_param.detach().cpu()
429
460
  # Ensure contiguous — bias may come from a chunk split (Phase 1)
430
461
  if not bias_cpu.is_contiguous():
431
462
  bias_cpu = bias_cpu.contiguous()
432
463
  bias_slices = bias_cpu.unbind(0)
433
- bias_requires_grad = bias_param.requires_grad
464
+ bias_requires_grad = bias_param.requires_grad if isinstance(bias_param, nn.Parameter) else False
434
465
 
435
466
  # Drop the original fused parameter before allocating the defused
436
467
  # per-expert linears back on the original device.
@@ -588,7 +619,7 @@ def _unfuse_experts_weights_inplace(
588
619
 
589
620
  # Get first projection to determine num_experts and layout
590
621
  first_proj_name = next(iter(detected_projections))
591
- first_param = getattr(module, first_proj_name)
622
+ first_param = _get_registered_tensor(module, first_proj_name)
592
623
  num_experts = first_param.shape[0]
593
624
 
594
625
  # Detect if transposed
@@ -608,15 +639,15 @@ def _unfuse_experts_weights_inplace(
608
639
  dtype = first_param.dtype
609
640
  target_device = first_param.device if first_param.device.type != "meta" else "cpu"
610
641
 
611
- # Phase 1: Split fused projections (e.g., gate_up_proj -> gate_proj + up_proj) into separate 3D params
642
+ # Phase 1: Split fused projections (e.g., gate_up_proj -> gate_proj + up_proj) into separate 3D tensors
612
643
  extra_projections = {}
613
644
  fused_to_remove = []
614
645
  for proj_name, config in detected_projections.items():
615
646
  split_into = config.get("split_into")
616
647
  if not split_into:
617
648
  continue
618
- param = getattr(module, proj_name, None)
619
- if param is None or not isinstance(param, nn.Parameter) or param.dim() != 3:
649
+ param = _get_registered_tensor(module, proj_name)
650
+ if param is None or param.dim() != 3:
620
651
  continue
621
652
  # Split along output dimension
622
653
  split_dim = 2 if is_transposed else 1
@@ -624,17 +655,17 @@ def _unfuse_experts_weights_inplace(
624
655
 
625
656
  # Also split bias if present (e.g., gate_up_proj_bias -> gate_proj_bias + up_proj_bias)
626
657
  bias_name = f"{proj_name}_bias"
627
- bias_param = getattr(module, bias_name, None)
658
+ bias_param = _get_registered_tensor(module, bias_name)
628
659
  bias_splits = None
629
- if bias_param is not None and isinstance(bias_param, nn.Parameter) and bias_param.dim() == 2:
660
+ if isinstance(bias_param, torch.Tensor) and bias_param.dim() == 2:
630
661
  bias_splits = bias_param.chunk(len(split_into), dim=1)
631
662
 
632
663
  for i, (split_name, split_param) in enumerate(zip(split_into, split_params)):
633
664
  # Avoid .contiguous() here — _unfuse_single_projection will handle it
634
665
  # during batch transpose/unbind, saving a full-tensor copy
635
- setattr(module, split_name, nn.Parameter(split_param))
666
+ _set_registered_tensor_like(module, split_name, split_param, param)
636
667
  if bias_splits is not None:
637
- setattr(module, f"{split_name}_bias", nn.Parameter(bias_splits[i]))
668
+ _set_registered_tensor_like(module, f"{split_name}_bias", bias_splits[i], bias_param)
638
669
  extra_projections[split_name] = KNOWN_PROJECTION_PATTERNS.get(
639
670
  split_name, {"is_input_proj": True, "output_multiplier": 1}
640
671
  )
@@ -649,7 +680,7 @@ def _unfuse_experts_weights_inplace(
649
680
  del detected_projections[name]
650
681
  detected_projections.update(extra_projections)
651
682
 
652
- # Phase 2: Unfuse all 3D params into per-expert Linear layers
683
+ # Phase 2: Unfuse all 3D tensors into per-expert Linear layers
653
684
  proj_linears = {} # proj_name -> [Linear_expert0, Linear_expert1, ...]
654
685
  for proj_name in detected_projections:
655
686
  linears = _unfuse_single_projection(module, proj_name, num_experts, is_transposed, dtype, target_device)
@@ -0,0 +1,4 @@
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
@@ -0,0 +1,4 @@
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
@@ -9,7 +9,7 @@ build-backend = "setuptools.build_meta"
9
9
 
10
10
  [project]
11
11
  name = "Defuser"
12
- version = "0.0.18"
12
+ version = "0.0.19"
13
13
  description = "Model defuser helper for HF Transformers."
14
14
  readme = "README.md"
15
15
  requires-python = ">=3.9"
@@ -25,6 +25,7 @@ classifiers = [
25
25
  "Programming Language :: Python :: 3.12",
26
26
  "Programming Language :: Python :: 3.13",
27
27
  "Programming Language :: Python :: 3.14",
28
+ "Programming Language :: Python :: 3.15",
28
29
  "Programming Language :: C",
29
30
  "Topic :: Software Development :: Libraries",
30
31
  "Topic :: Text Processing :: Linguistic"
@@ -0,0 +1,7 @@
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
+ from setuptools import setup
6
+
7
+ setup()
@@ -17,12 +17,16 @@ from defuser.model_registry import MODEL_CONFIG
17
17
 
18
18
 
19
19
  class _DummyLayer(nn.Module):
20
+ """Wrap one candidate module so convert_model sees a layer-shaped path."""
21
+
20
22
  def __init__(self, module: nn.Module):
21
23
  super().__init__()
22
24
  self.module = module
23
25
 
24
26
 
25
27
  class _DummyModel(nn.Module):
28
+ """Minimal model shell for exercising runtime conversion helpers."""
29
+
26
30
  def __init__(self, model_type: str, module: nn.Module):
27
31
  super().__init__()
28
32
  self.config = getattr(module, "config", None)
@@ -33,10 +37,21 @@ class _DummyModel(nn.Module):
33
37
 
34
38
 
35
39
  def _load(module_path: str, attr_name: str):
40
+ """Import one symbol lazily so candidate tables stay compact."""
36
41
  return getattr(import_module(module_path), attr_name)
37
42
 
38
43
 
44
+ def _normalize_attr_value(obj, attr: str, value):
45
+ """Preserve list-valued config shapes when tests shrink scalar attributes."""
46
+ current = getattr(obj, attr)
47
+ if isinstance(current, list) and not isinstance(value, list):
48
+ width = len(current) or 1
49
+ return [value for _ in range(width)]
50
+ return value
51
+
52
+
39
53
  def _build_config(case: dict):
54
+ """Build a tiny but valid config for one candidate module case."""
40
55
  config = _load(case["config_module"], case["config_name"])()
41
56
  sub_attr = case.get("sub_attr")
42
57
  if sub_attr is not None:
@@ -73,24 +88,29 @@ def _build_config(case: dict):
73
88
  "dropout_rate": 0.0,
74
89
  }.items():
75
90
  if hasattr(config, attr):
76
- setattr(config, attr, value)
91
+ setattr(config, attr, _normalize_attr_value(config, attr, value))
77
92
  for attr, value in case.get("config_updates", {}).items():
78
93
  if hasattr(config, attr):
79
- setattr(config, attr, value)
94
+ setattr(config, attr, _normalize_attr_value(config, attr, value))
80
95
  return config
81
96
 
82
97
 
83
98
  def _build_module(case: dict) -> nn.Module:
99
+ """Instantiate one candidate module in the format its constructor expects."""
84
100
  module_cls = _load(case["module_path"], case["class_name"])
85
101
  kind = case.get("kind", "config")
86
102
  if kind == "parallel":
87
103
  return module_cls(case["num_experts"], case["input_size"], case["output_size"]).eval()
88
104
  if kind == "zamba2":
89
105
  return module_cls(_build_config(case), num_fwd_mem_blocks=1, block_id=0).eval()
90
- return module_cls(_build_config(case)).eval()
106
+ config = _build_config(case)
107
+ if case["model_type"] == "ernie4_5_vl_moe" and isinstance(getattr(config, "moe_intermediate_size", None), list):
108
+ return module_cls(config, intermediate_size=config.moe_intermediate_size[0]).eval()
109
+ return module_cls(config).eval()
91
110
 
92
111
 
93
112
  def _wrapped_model(case: dict) -> tuple[_DummyModel, nn.Module]:
113
+ """Seed one module deterministically and wrap it as a dummy model."""
94
114
  module = _build_module(case)
95
115
  generator = torch.Generator(device="cpu").manual_seed(0)
96
116
  with torch.no_grad():
@@ -101,10 +121,12 @@ def _wrapped_model(case: dict) -> tuple[_DummyModel, nn.Module]:
101
121
 
102
122
 
103
123
  def _patched_module(model: _DummyModel) -> nn.Module:
124
+ """Return the converted test module from the dummy model wrapper."""
104
125
  return model.layers[0].module
105
126
 
106
127
 
107
128
  def _assert_expert_container(module: nn.Module, attrs: tuple[str, ...]) -> None:
129
+ """Verify that expert ``0`` exposes the expected defused projections."""
108
130
  assert hasattr(module, "0")
109
131
  expert0 = getattr(module, "0")
110
132
  for attr in attrs:
@@ -112,6 +134,7 @@ def _assert_expert_container(module: nn.Module, attrs: tuple[str, ...]) -> None:
112
134
 
113
135
 
114
136
  def _standard_hidden(case: dict) -> torch.Tensor:
137
+ """Generate default hidden states for sparse-MoE candidate coverage tests."""
115
138
  return torch.randn(case.get("hidden_shape", (5, case["input_dim"])), dtype=torch.float32)
116
139
 
117
140
 
@@ -553,11 +576,13 @@ ALL_CANDIDATE_MODEL_TYPES = {
553
576
 
554
577
 
555
578
  def test_model_registry_covers_all_scanned_candidates():
579
+ """Every scanned candidate model type should be represented in the registry."""
556
580
  assert ALL_CANDIDATE_MODEL_TYPES.issubset(MODEL_CONFIG)
557
581
 
558
582
 
559
583
  @pytest.mark.parametrize("case", STANDARD_MOE_CASES, ids=[case["model_type"] for case in STANDARD_MOE_CASES])
560
584
  def test_standard_moe_candidates_convert_and_preserve_forward(case):
585
+ """Standard sparse-MoE candidates should convert without changing outputs."""
561
586
  torch.manual_seed(0)
562
587
  model, original_module = _wrapped_model(case)
563
588
  hidden_states = _standard_hidden(case)
@@ -593,6 +618,7 @@ def test_standard_moe_candidates_convert_and_preserve_forward(case):
593
618
 
594
619
  @pytest.mark.parametrize("case", PARALLEL_CASES, ids=[case["model_type"] for case in PARALLEL_CASES])
595
620
  def test_parallel_expert_candidates_convert_and_preserve_forward(case):
621
+ """Parallel expert containers should convert without changing outputs."""
596
622
  torch.manual_seed(0)
597
623
  model, original_module = _wrapped_model(case)
598
624
  expert_size = [2, 1, 0, 3]
@@ -616,6 +642,7 @@ def test_parallel_expert_candidates_convert_and_preserve_forward(case):
616
642
 
617
643
  @pytest.mark.parametrize("case", DENSE_CASES, ids=[case["label"] for case in DENSE_CASES])
618
644
  def test_dense_candidates_convert_and_preserve_forward(case):
645
+ """Dense split-gate MLP candidates should preserve forward results after conversion."""
619
646
  torch.manual_seed(0)
620
647
  model, original_module = _wrapped_model(case)
621
648
  hidden_states = torch.randn(3, case["hidden_size"], dtype=torch.float32)
@@ -644,6 +671,7 @@ def test_dense_candidates_convert_and_preserve_forward(case):
644
671
 
645
672
 
646
673
  def test_runtime_model_patches_respect_max_layers():
674
+ """Runtime-only patches should stop once ``max_layers`` is reached."""
647
675
  module0 = _build_module(next(case for case in DENSE_CASES if case["label"] == "phi3"))
648
676
  module1 = _build_module(next(case for case in DENSE_CASES if case["label"] == "phi3"))
649
677
 
@@ -15,16 +15,28 @@ from defuser.model_registry import MODEL_CONFIG
15
15
 
16
16
 
17
17
  def _load(module_path: str, attr_name: str):
18
+ """Import one symbol lazily so the model case table stays readable."""
18
19
  return getattr(import_module(module_path), attr_name)
19
20
 
20
21
 
22
+ def _normalize_attr_value(obj, attr: str, value):
23
+ """Preserve list-valued config fields when shrinking configs for meta tests."""
24
+ current = getattr(obj, attr)
25
+ if isinstance(current, list) and not isinstance(value, list):
26
+ width = len(current) or 1
27
+ return [value for _ in range(width)]
28
+ return value
29
+
30
+
21
31
  def _set_if_has(obj, **kwargs) -> None:
32
+ """Apply overrides only to attributes exposed by the current config node."""
22
33
  for attr, value in kwargs.items():
23
34
  if hasattr(obj, attr):
24
- setattr(obj, attr, value)
35
+ setattr(obj, attr, _normalize_attr_value(obj, attr, value))
25
36
 
26
37
 
27
38
  def _mutate_common_config_tree(config, visited: set[int] | None = None) -> None:
39
+ """Shrink nested configs recursively so meta-model construction stays lightweight."""
28
40
  if config is None or isinstance(config, (int, float, str, bool, list, tuple, dict)):
29
41
  return
30
42
 
@@ -125,6 +137,7 @@ def _mutate_common_config_tree(config, visited: set[int] | None = None) -> None:
125
137
 
126
138
 
127
139
  def _build_model_config(case: dict):
140
+ """Construct a small config tree for one registered public model type."""
128
141
  config = _load(case["config_module"], case["config_class"])()
129
142
  _mutate_common_config_tree(config)
130
143
 
@@ -180,6 +193,7 @@ def _build_model_config(case: dict):
180
193
 
181
194
 
182
195
  def _find_module_hits(model, class_paths: tuple[str, ...]) -> list[tuple[str, str]]:
196
+ """Collect modules whose class path matches one of the expected targets."""
183
197
  hits = []
184
198
  wanted = set(class_paths)
185
199
  for name, module in model.named_modules():
@@ -190,16 +204,19 @@ def _find_module_hits(model, class_paths: tuple[str, ...]) -> list[tuple[str, st
190
204
 
191
205
 
192
206
  def _assert_meta_parameters(module) -> None:
207
+ """Assert that one module keeps all of its parameters on meta."""
193
208
  for _, param in module.named_parameters(recurse=True):
194
209
  assert param.is_meta
195
210
 
196
211
 
197
212
  def _assert_all_model_parameters_meta(model) -> None:
213
+ """Assert that conversion does not materialize weights during meta tests."""
198
214
  for _, param in model.named_parameters():
199
215
  assert param.is_meta
200
216
 
201
217
 
202
218
  def _validate_defused_module(case: dict, module) -> None:
219
+ """Run the case-specific structural checks on one converted module."""
203
220
  kind = case["validator"]
204
221
 
205
222
  if kind == "experts":
@@ -729,11 +746,13 @@ META_MODEL_CASES = [
729
746
 
730
747
 
731
748
  def test_meta_model_cases_cover_registered_public_models():
749
+ """Meta-model coverage should stay aligned with the public registry."""
732
750
  assert {case["model_type"] for case in META_MODEL_CASES} == set(MODEL_CONFIG) - {"qwen3_5_moe_text"}
733
751
 
734
752
 
735
753
  @pytest.mark.parametrize("case", META_MODEL_CASES, ids=[case["model_type"] for case in META_MODEL_CASES])
736
754
  def test_each_model_defuses_direct_meta_model(case):
755
+ """Each registered public model should expose the expected defused modules on meta."""
737
756
  if case["mode"] == "replace":
738
757
  replace_fused_blocks(case["model_type"])
739
758
 
@@ -0,0 +1,62 @@
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 __future__ import annotations
7
+
8
+ import torch
9
+ import torch.nn as nn
10
+
11
+ from defuser.modeling.moe_experts_interface import _detect_expert_projections, _unfuse_experts_weights_inplace
12
+
13
+
14
+ class _UnknownExpertsWithLossProperty(nn.Module):
15
+ """Regression fixture for expert detection on modules with unrelated properties."""
16
+
17
+ def __init__(self) -> None:
18
+ super().__init__()
19
+ self.expert_weight = nn.Parameter(torch.randn(4, 8, 16))
20
+ self.loss_property_accesses = 0
21
+
22
+ @property
23
+ def loss_function(self):
24
+ self.loss_property_accesses += 1
25
+ raise AssertionError("expert detection should not touch unrelated properties")
26
+
27
+
28
+ def test_detect_expert_projections_ignores_unrelated_properties() -> None:
29
+ """Projection detection should ignore unrelated properties on expert fixtures."""
30
+ module = _UnknownExpertsWithLossProperty()
31
+
32
+ detected = _detect_expert_projections(module)
33
+
34
+ assert detected == {
35
+ "expert_weight": {"is_input_proj": True, "output_multiplier": 1},
36
+ }
37
+ assert module.loss_property_accesses == 0
38
+
39
+
40
+ class _BufferBackedExperts(nn.Module):
41
+ """Exercise the buffer-backed fused expert path end to end."""
42
+
43
+ def __init__(self) -> None:
44
+ super().__init__()
45
+ self.register_buffer("gate_up_proj", torch.arange(2 * 6 * 4, dtype=torch.float32).reshape(2, 6, 4))
46
+ self.register_buffer("down_proj", torch.arange(2 * 4 * 3, dtype=torch.float32).reshape(2, 4, 3))
47
+
48
+
49
+ def test_unfuse_experts_supports_registered_buffers() -> None:
50
+ """Buffer-backed fused experts should unfuse into per-expert Linear layers."""
51
+ module = _BufferBackedExperts()
52
+ expected_gate_proj = module.gate_up_proj[0, :3].clone()
53
+ expected_up_proj = module.gate_up_proj[0, 3:].clone()
54
+ expected_down_proj = module.down_proj[0].clone()
55
+
56
+ changed = _unfuse_experts_weights_inplace(module, check_decorator=False)
57
+
58
+ assert changed is True
59
+ expert0 = getattr(module, "0")
60
+ torch.testing.assert_close(expert0.gate_proj.weight, expected_gate_proj)
61
+ torch.testing.assert_close(expert0.up_proj.weight, expected_up_proj)
62
+ torch.testing.assert_close(expert0.down_proj.weight, expected_down_proj)
File without changes
File without changes
File without changes
defuser-0.0.18/setup.py DELETED
@@ -1,3 +0,0 @@
1
- from setuptools import setup
2
-
3
- setup()
File without changes
File without changes
File without changes
File without changes