Defuser 0.0.28__tar.gz → 0.0.29__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 (43) hide show
  1. {defuser-0.0.28 → defuser-0.0.29}/Defuser.egg-info/PKG-INFO +2 -2
  2. {defuser-0.0.28 → defuser-0.0.29}/PKG-INFO +2 -2
  3. {defuser-0.0.28 → defuser-0.0.29}/README.md +1 -1
  4. {defuser-0.0.28 → defuser-0.0.29}/defuser/model_registry.py +30 -0
  5. {defuser-0.0.28 → defuser-0.0.29}/pyproject.toml +1 -1
  6. {defuser-0.0.28 → defuser-0.0.29}/tests/test_convert_model.py +208 -0
  7. {defuser-0.0.28 → defuser-0.0.29}/tests/test_meta_model_defusion.py +27 -0
  8. {defuser-0.0.28 → defuser-0.0.29}/tests/test_moe_experts_interface.py +12 -0
  9. {defuser-0.0.28 → defuser-0.0.29}/Defuser.egg-info/SOURCES.txt +0 -0
  10. {defuser-0.0.28 → defuser-0.0.29}/Defuser.egg-info/dependency_links.txt +0 -0
  11. {defuser-0.0.28 → defuser-0.0.29}/Defuser.egg-info/requires.txt +0 -0
  12. {defuser-0.0.28 → defuser-0.0.29}/Defuser.egg-info/top_level.txt +0 -0
  13. {defuser-0.0.28 → defuser-0.0.29}/LICENSE +0 -0
  14. {defuser-0.0.28 → defuser-0.0.29}/defuser/__init__.py +0 -0
  15. {defuser-0.0.28 → defuser-0.0.29}/defuser/checkpoint_ops.py +0 -0
  16. {defuser-0.0.28 → defuser-0.0.29}/defuser/defuser.py +0 -0
  17. {defuser-0.0.28 → defuser-0.0.29}/defuser/modeling/__init__.py +0 -0
  18. {defuser-0.0.28 → defuser-0.0.29}/defuser/modeling/glm4v.py +0 -0
  19. {defuser-0.0.28 → defuser-0.0.29}/defuser/modeling/model_patches.py +0 -0
  20. {defuser-0.0.28 → defuser-0.0.29}/defuser/modeling/moe_experts_interface.py +0 -0
  21. {defuser-0.0.28 → defuser-0.0.29}/defuser/modeling/replace_modules.py +0 -0
  22. {defuser-0.0.28 → defuser-0.0.29}/defuser/modeling/runtime_defusion.py +0 -0
  23. {defuser-0.0.28 → defuser-0.0.29}/defuser/modeling/unfused_moe/__init__.py +0 -0
  24. {defuser-0.0.28 → defuser-0.0.29}/defuser/modeling/unfused_moe/common.py +0 -0
  25. {defuser-0.0.28 → defuser-0.0.29}/defuser/modeling/unfused_moe/glm4_moe.py +0 -0
  26. {defuser-0.0.28 → defuser-0.0.29}/defuser/modeling/unfused_moe/glm4_moe_lite.py +0 -0
  27. {defuser-0.0.28 → defuser-0.0.29}/defuser/modeling/unfused_moe/mixtral.py +0 -0
  28. {defuser-0.0.28 → defuser-0.0.29}/defuser/modeling/unfused_moe/qwen2_moe.py +0 -0
  29. {defuser-0.0.28 → defuser-0.0.29}/defuser/modeling/unfused_moe/qwen3_moe.py +0 -0
  30. {defuser-0.0.28 → defuser-0.0.29}/defuser/modeling/unfused_moe/qwen3_next.py +0 -0
  31. {defuser-0.0.28 → defuser-0.0.29}/defuser/modeling/unfused_moe/qwen3_omni_moe.py +0 -0
  32. {defuser-0.0.28 → defuser-0.0.29}/defuser/modeling/update_module.py +0 -0
  33. {defuser-0.0.28 → defuser-0.0.29}/defuser/utils/__init__.py +0 -0
  34. {defuser-0.0.28 → defuser-0.0.29}/defuser/utils/common.py +0 -0
  35. {defuser-0.0.28 → defuser-0.0.29}/defuser/utils/device.py +0 -0
  36. {defuser-0.0.28 → defuser-0.0.29}/defuser/utils/hf.py +0 -0
  37. {defuser-0.0.28 → defuser-0.0.29}/defuser/utils/model.py +0 -0
  38. {defuser-0.0.28 → defuser-0.0.29}/setup.cfg +0 -0
  39. {defuser-0.0.28 → defuser-0.0.29}/setup.py +0 -0
  40. {defuser-0.0.28 → defuser-0.0.29}/tests/test_candidate_coverage.py +0 -0
  41. {defuser-0.0.28 → defuser-0.0.29}/tests/test_device_utils.py +0 -0
  42. {defuser-0.0.28 → defuser-0.0.29}/tests/test_filter_rules.py +0 -0
  43. {defuser-0.0.28 → defuser-0.0.29}/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.28
3
+ Version: 0.0.29
4
4
  Summary: Model defuser helper for HF Transformers.
5
5
  Author-email: ModelCloud <qubitium@modelcloud.ai>
6
6
  License-Expression: Apache-2.0
@@ -92,7 +92,7 @@ Defuser currently supports the following `transformers>=5.3.0` `model_type` valu
92
92
 
93
93
  | Pattern | Supported model types | Defused op performed ⚙️ |
94
94
  | --- |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| --- |
95
- | Standard routed expert tensors 🧱 | `deepseek_v2`, `diffusion_gemma`, `dots1`, `ernie4_5_moe`, `ernie4_5_vl_moe`, `exaone_moe`, `flex_olmo`, `glm4_moe_lite`, `glm4v_moe`, `hunyuan_v1_moe`, `jamba`, `laguna`, `lfm2_moe`, `minimax`, `minimax_m2`, `olmoe`, `qwen3_vl_moe`, `solar_open`, `solar_open2` | Splits fused expert tensors or registered expert buffers into numbered expert `nn.Linear` modules with per-expert `gate_proj`, `up_proj`, and `down_proj`. |
95
+ | Standard routed expert tensors 🧱 | `afmoe`, `bailing_hybrid`, `deepseek_v2`, `diffusion_gemma`, `dots1`, `ernie4_5_moe`, `ernie4_5_vl_moe`, `exaone_moe`, `flex_olmo`, `glm4_moe_lite`, `glm4v_moe`, `hunyuan_v1_moe`, `jamba`, `laguna`, `lfm2_moe`, `minimax`, `minimax_m2`, `olmoe`, `qwen3_vl_moe`, `solar_open`, `solar_open2` | Splits fused expert tensors or registered expert buffers into numbered expert `nn.Linear` modules with per-expert `gate_proj`, `up_proj`, and `down_proj`. |
96
96
  | Mixed sparse and shared experts | `deepseek_v3`, `deepseek_v32`, `deepseek_v4`, `glm5_next`, `glm_moe_dsa`, `qwen3_5_moe`, `qwen3_5_moe_text`, `qwen4_exp`, `qwen4_exp_text` | Runtime expert tensor defusion for routed experts while preserving the model's shared-expert path. |
97
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. |
98
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. |
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Defuser
3
- Version: 0.0.28
3
+ Version: 0.0.29
4
4
  Summary: Model defuser helper for HF Transformers.
5
5
  Author-email: ModelCloud <qubitium@modelcloud.ai>
6
6
  License-Expression: Apache-2.0
@@ -92,7 +92,7 @@ Defuser currently supports the following `transformers>=5.3.0` `model_type` valu
92
92
 
93
93
  | Pattern | Supported model types | Defused op performed ⚙️ |
94
94
  | --- |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| --- |
95
- | Standard routed expert tensors 🧱 | `deepseek_v2`, `diffusion_gemma`, `dots1`, `ernie4_5_moe`, `ernie4_5_vl_moe`, `exaone_moe`, `flex_olmo`, `glm4_moe_lite`, `glm4v_moe`, `hunyuan_v1_moe`, `jamba`, `laguna`, `lfm2_moe`, `minimax`, `minimax_m2`, `olmoe`, `qwen3_vl_moe`, `solar_open`, `solar_open2` | Splits fused expert tensors or registered expert buffers into numbered expert `nn.Linear` modules with per-expert `gate_proj`, `up_proj`, and `down_proj`. |
95
+ | Standard routed expert tensors 🧱 | `afmoe`, `bailing_hybrid`, `deepseek_v2`, `diffusion_gemma`, `dots1`, `ernie4_5_moe`, `ernie4_5_vl_moe`, `exaone_moe`, `flex_olmo`, `glm4_moe_lite`, `glm4v_moe`, `hunyuan_v1_moe`, `jamba`, `laguna`, `lfm2_moe`, `minimax`, `minimax_m2`, `olmoe`, `qwen3_vl_moe`, `solar_open`, `solar_open2` | Splits fused expert tensors or registered expert buffers into numbered expert `nn.Linear` modules with per-expert `gate_proj`, `up_proj`, and `down_proj`. |
96
96
  | Mixed sparse and shared experts | `deepseek_v3`, `deepseek_v32`, `deepseek_v4`, `glm5_next`, `glm_moe_dsa`, `qwen3_5_moe`, `qwen3_5_moe_text`, `qwen4_exp`, `qwen4_exp_text` | Runtime expert tensor defusion for routed experts while preserving the model's shared-expert path. |
97
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. |
98
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. |
@@ -66,7 +66,7 @@ Defuser currently supports the following `transformers>=5.3.0` `model_type` valu
66
66
 
67
67
  | Pattern | Supported model types | Defused op performed ⚙️ |
68
68
  | --- |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| --- |
69
- | Standard routed expert tensors 🧱 | `deepseek_v2`, `diffusion_gemma`, `dots1`, `ernie4_5_moe`, `ernie4_5_vl_moe`, `exaone_moe`, `flex_olmo`, `glm4_moe_lite`, `glm4v_moe`, `hunyuan_v1_moe`, `jamba`, `laguna`, `lfm2_moe`, `minimax`, `minimax_m2`, `olmoe`, `qwen3_vl_moe`, `solar_open`, `solar_open2` | 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
+ | Standard routed expert tensors 🧱 | `afmoe`, `bailing_hybrid`, `deepseek_v2`, `diffusion_gemma`, `dots1`, `ernie4_5_moe`, `ernie4_5_vl_moe`, `exaone_moe`, `flex_olmo`, `glm4_moe_lite`, `glm4v_moe`, `hunyuan_v1_moe`, `jamba`, `laguna`, `lfm2_moe`, `minimax`, `minimax_m2`, `olmoe`, `qwen3_vl_moe`, `solar_open`, `solar_open2` | Splits fused expert tensors or registered expert buffers into numbered expert `nn.Linear` modules with per-expert `gate_proj`, `up_proj`, and `down_proj`. |
70
70
  | Mixed sparse and shared experts | `deepseek_v3`, `deepseek_v32`, `deepseek_v4`, `glm5_next`, `glm_moe_dsa`, `qwen3_5_moe`, `qwen3_5_moe_text`, `qwen4_exp`, `qwen4_exp_text` | Runtime expert tensor defusion for routed experts while preserving the model's shared-expert path. |
71
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. |
72
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. |
@@ -16,9 +16,27 @@ class PATCH(str, Enum):
16
16
 
17
17
 
18
18
  MODEL_CONFIG = {
19
+ "afmoe": {
20
+ "min_transformers_version": MIN_SUPPORTED_TRANSFORMERS_VERSION,
21
+ PATCH.EXPERTS_DEFUSE: [
22
+ {
23
+ "module_class": "transformers.models.afmoe.modeling_afmoe.AfmoeExperts",
24
+ "forward_impl": "linear_loop",
25
+ }
26
+ ],
27
+ },
19
28
  "axk2": {
20
29
  "min_transformers_version": MIN_SUPPORTED_TRANSFORMERS_VERSION,
21
30
  },
31
+ "bailing_hybrid": {
32
+ "min_transformers_version": MIN_SUPPORTED_TRANSFORMERS_VERSION,
33
+ PATCH.EXPERTS_DEFUSE: [
34
+ {
35
+ "module_class": "transformers.models.bailing_hybrid.modeling_bailing_hybrid.BailingHybridExperts",
36
+ "forward_impl": "linear_loop",
37
+ }
38
+ ],
39
+ },
22
40
  "cohere2_moe": {
23
41
  "min_transformers_version": MIN_SUPPORTED_TRANSFORMERS_VERSION,
24
42
  },
@@ -76,6 +94,18 @@ MODEL_CONFIG = {
76
94
  "flex_olmo": {
77
95
  "min_transformers_version": MIN_SUPPORTED_TRANSFORMERS_VERSION,
78
96
  },
97
+ "flint": {
98
+ "min_transformers_version": MIN_SUPPORTED_TRANSFORMERS_VERSION,
99
+ PATCH.EXPERTS_DEFUSE: [
100
+ {
101
+ # Flint keeps gate/up and down projections as 3-D expert
102
+ # parameters. Expand them into per-expert Linear modules so
103
+ # GPTQModel can discover and quantize every routed expert.
104
+ "module_class": "transformers.models.flint.modeling_flint.FlintExperts",
105
+ "forward_impl": "linear_loop",
106
+ }
107
+ ],
108
+ },
79
109
  "glm": {
80
110
  "min_transformers_version": MIN_SUPPORTED_TRANSFORMERS_VERSION,
81
111
  },
@@ -9,7 +9,7 @@ build-backend = "setuptools.build_meta"
9
9
 
10
10
  [project]
11
11
  name = "Defuser"
12
- version = "0.0.28"
12
+ version = "0.0.29"
13
13
  description = "Model defuser helper for HF Transformers."
14
14
  readme = "README.md"
15
15
  requires-python = ">=3.9"
@@ -9,6 +9,13 @@ import torch
9
9
  from safetensors.torch import save_file
10
10
  from torch import nn
11
11
  from transformers.core_model_loading import WeightConverter
12
+ from transformers.models.afmoe.configuration_afmoe import AfmoeConfig
13
+ from transformers.models.afmoe.modeling_afmoe import AfmoeExperts, AfmoeForCausalLM
14
+ from transformers.models.bailing_hybrid.configuration_bailing_hybrid import BailingHybridConfig
15
+ from transformers.models.bailing_hybrid.modeling_bailing_hybrid import (
16
+ BailingHybridExperts,
17
+ BailingHybridForCausalLM,
18
+ )
12
19
  from transformers.models.glm4_moe.modeling_glm4_moe import Glm4MoeConfig, Glm4MoeForCausalLM, Glm4MoeMoE
13
20
  from transformers.models.glm4_moe_lite.configuration_glm4_moe_lite import Glm4MoeLiteConfig
14
21
  from transformers.models.glm4_moe_lite.modeling_glm4_moe_lite import (
@@ -85,6 +92,53 @@ def _tiny_moe_config(config_cls, num_hidden_layers: int=1):
85
92
  )
86
93
 
87
94
 
95
+ def _tiny_afmoe_config(num_hidden_layers: int = 1):
96
+ return AfmoeConfig(
97
+ num_hidden_layers=num_hidden_layers,
98
+ # Keep every layer on the MoE path so the routed experts are present.
99
+ num_dense_layers=0,
100
+ hidden_size=64,
101
+ intermediate_size=128,
102
+ moe_intermediate_size=32,
103
+ num_attention_heads=4,
104
+ num_key_value_heads=4,
105
+ head_dim=16,
106
+ num_experts=4,
107
+ num_experts_per_tok=2,
108
+ num_shared_experts=2,
109
+ vocab_size=128,
110
+ max_position_embeddings=64,
111
+ pad_token_id=0,
112
+ bos_token_id=1,
113
+ eos_token_id=2,
114
+ )
115
+
116
+
117
+ def _tiny_bailing_hybrid_config(num_hidden_layers: int = 2):
118
+ return BailingHybridConfig(
119
+ num_hidden_layers=num_hidden_layers,
120
+ # Keep every layer on the MoE path so the routed experts are present.
121
+ first_k_dense_replace=0,
122
+ layer_types=(
123
+ ["linear_attention", "full_attention"] * ((num_hidden_layers + 1) // 2)
124
+ )[:num_hidden_layers],
125
+ hidden_size=64,
126
+ intermediate_size=128,
127
+ moe_intermediate_size=32,
128
+ num_attention_heads=4,
129
+ num_key_value_heads=4,
130
+ head_dim=16,
131
+ num_local_experts=4,
132
+ num_experts_per_tok=2,
133
+ n_shared_experts=2,
134
+ vocab_size=128,
135
+ max_position_embeddings=64,
136
+ pad_token_id=0,
137
+ bos_token_id=1,
138
+ eos_token_id=2,
139
+ )
140
+
141
+
88
142
  def _tiny_qwen3_omni_config():
89
143
  return Qwen3OmniMoeConfig(
90
144
  initializer_range=0.02,
@@ -1251,6 +1305,160 @@ def test_gpt_oss_split_forward_matches_fused_math():
1251
1305
  torch.testing.assert_close(actual, expected)
1252
1306
 
1253
1307
 
1308
+ def test_afmoe_registry_and_defusion():
1309
+ from defuser.modeling.moe_experts_interface import linear_loop_experts_forward
1310
+
1311
+ assert MODEL_CONFIG["afmoe"][PATCH.EXPERTS_DEFUSE] == [
1312
+ {
1313
+ "module_class": "transformers.models.afmoe.modeling_afmoe.AfmoeExperts",
1314
+ "forward_impl": "linear_loop",
1315
+ }
1316
+ ]
1317
+
1318
+ model = AfmoeForCausalLM(_tiny_afmoe_config())
1319
+ assert model.config.model_type == "afmoe"
1320
+
1321
+ experts = model.model.layers[0].mlp.experts
1322
+ assert isinstance(experts, AfmoeExperts)
1323
+ num_experts = model.config.num_experts
1324
+ hidden_dim = model.config.hidden_size
1325
+ intermediate_dim = model.config.moe_intermediate_size
1326
+ assert tuple(experts.gate_up_proj.shape) == (num_experts, 2 * intermediate_dim, hidden_dim)
1327
+ assert tuple(experts.down_proj.shape) == (num_experts, hidden_dim, intermediate_dim)
1328
+
1329
+ expected_gate = experts.gate_up_proj[0, :intermediate_dim, :].clone()
1330
+ expected_up = experts.gate_up_proj[0, intermediate_dim:, :].clone()
1331
+ expected_down = experts.down_proj[0, :, :].clone()
1332
+
1333
+ converted = convert_model(model, cleanup_original=False, max_layers=1)
1334
+ assert converted
1335
+
1336
+ experts = model.model.layers[0].mlp.experts
1337
+ assert isinstance(experts, AfmoeExperts)
1338
+ assert not hasattr(experts, "gate_up_proj")
1339
+ assert not hasattr(experts, "down_proj")
1340
+ assert experts.forward.__func__ is linear_loop_experts_forward
1341
+
1342
+ containers = [name for name, _ in experts.named_children() if name.isdigit()]
1343
+ assert sorted(containers, key=int) == [str(idx) for idx in range(num_experts)]
1344
+
1345
+ expert0 = getattr(experts, "0")
1346
+ assert isinstance(expert0.gate_proj, nn.Linear)
1347
+ assert isinstance(expert0.up_proj, nn.Linear)
1348
+ assert isinstance(expert0.down_proj, nn.Linear)
1349
+
1350
+ torch.testing.assert_close(expert0.gate_proj.weight, expected_gate)
1351
+ torch.testing.assert_close(expert0.up_proj.weight, expected_up)
1352
+ torch.testing.assert_close(expert0.down_proj.weight, expected_down)
1353
+
1354
+
1355
+ def test_afmoe_split_forward_matches_fused_math():
1356
+ model = AfmoeForCausalLM(_tiny_afmoe_config())
1357
+ fused_experts = model.model.layers[0].mlp.experts
1358
+ assert isinstance(fused_experts, AfmoeExperts)
1359
+
1360
+ input_ids = torch.randint(0, model.config.vocab_size, (2, 7))
1361
+ hidden_states = torch.randn(5, model.config.hidden_size, dtype=torch.float32)
1362
+ top_k_index = torch.randint(0, model.config.num_experts, (hidden_states.size(0), 2))
1363
+ top_k_weights = torch.rand((hidden_states.size(0), 2), dtype=hidden_states.dtype)
1364
+
1365
+ with torch.no_grad():
1366
+ expected_experts = fused_experts(hidden_states, top_k_index, top_k_weights)
1367
+ expected_logits = model(input_ids).logits
1368
+
1369
+ converted = convert_model(model, cleanup_original=False, max_layers=1)
1370
+ assert converted
1371
+
1372
+ split_experts = model.model.layers[0].mlp.experts
1373
+ _assert_unfused_expert_module(split_experts)
1374
+ with torch.no_grad():
1375
+ actual_experts = split_experts(hidden_states, top_k_index, top_k_weights)
1376
+ actual_logits = model(input_ids).logits
1377
+
1378
+ # The defused linear_loop path should reproduce the original fused experts math.
1379
+ torch.testing.assert_close(actual_experts, expected_experts, atol=1e-5, rtol=1e-5)
1380
+ torch.testing.assert_close(actual_logits, expected_logits, atol=1e-5, rtol=1e-5)
1381
+
1382
+
1383
+ def test_bailing_hybrid_registry_and_defusion():
1384
+ from defuser.modeling.moe_experts_interface import linear_loop_experts_forward
1385
+
1386
+ assert MODEL_CONFIG["bailing_hybrid"][PATCH.EXPERTS_DEFUSE] == [
1387
+ {
1388
+ "module_class": (
1389
+ "transformers.models.bailing_hybrid.modeling_bailing_hybrid."
1390
+ "BailingHybridExperts"
1391
+ ),
1392
+ "forward_impl": "linear_loop",
1393
+ }
1394
+ ]
1395
+
1396
+ model = BailingHybridForCausalLM(_tiny_bailing_hybrid_config())
1397
+ assert model.config.model_type == "bailing_hybrid"
1398
+
1399
+ experts = model.model.layers[0].mlp.experts
1400
+ assert isinstance(experts, BailingHybridExperts)
1401
+ num_experts = model.config.num_local_experts
1402
+ hidden_dim = model.config.hidden_size
1403
+ intermediate_dim = model.config.moe_intermediate_size
1404
+ assert tuple(experts.gate_up_proj.shape) == (num_experts, 2 * intermediate_dim, hidden_dim)
1405
+ assert tuple(experts.down_proj.shape) == (num_experts, hidden_dim, intermediate_dim)
1406
+
1407
+ expected_gate = experts.gate_up_proj[0, :intermediate_dim, :].clone()
1408
+ expected_up = experts.gate_up_proj[0, intermediate_dim:, :].clone()
1409
+ expected_down = experts.down_proj[0, :, :].clone()
1410
+
1411
+ converted = convert_model(model, cleanup_original=False, max_layers=1)
1412
+ assert converted
1413
+
1414
+ experts = model.model.layers[0].mlp.experts
1415
+ assert isinstance(experts, BailingHybridExperts)
1416
+ assert not hasattr(experts, "gate_up_proj")
1417
+ assert not hasattr(experts, "down_proj")
1418
+ assert experts.forward.__func__ is linear_loop_experts_forward
1419
+
1420
+ containers = [name for name, _ in experts.named_children() if name.isdigit()]
1421
+ assert sorted(containers, key=int) == [str(idx) for idx in range(num_experts)]
1422
+
1423
+ expert0 = getattr(experts, "0")
1424
+ assert isinstance(expert0.gate_proj, nn.Linear)
1425
+ assert isinstance(expert0.up_proj, nn.Linear)
1426
+ assert isinstance(expert0.down_proj, nn.Linear)
1427
+
1428
+ torch.testing.assert_close(expert0.gate_proj.weight, expected_gate)
1429
+ torch.testing.assert_close(expert0.up_proj.weight, expected_up)
1430
+ torch.testing.assert_close(expert0.down_proj.weight, expected_down)
1431
+
1432
+
1433
+ def test_bailing_hybrid_split_forward_matches_fused_math():
1434
+ model = BailingHybridForCausalLM(_tiny_bailing_hybrid_config())
1435
+ fused_experts = model.model.layers[0].mlp.experts
1436
+ assert isinstance(fused_experts, BailingHybridExperts)
1437
+
1438
+ hidden_states = torch.randn(5, model.config.hidden_size, dtype=torch.float32)
1439
+ top_k_index = torch.randint(
1440
+ 0, model.config.num_local_experts, (hidden_states.size(0), 2)
1441
+ )
1442
+ top_k_weights = torch.rand((hidden_states.size(0), 2), dtype=hidden_states.dtype)
1443
+
1444
+ with torch.no_grad():
1445
+ expected_experts = fused_experts(hidden_states, top_k_index, top_k_weights)
1446
+
1447
+ converted = convert_model(model, cleanup_original=False, max_layers=1)
1448
+ assert converted
1449
+
1450
+ split_experts = model.model.layers[0].mlp.experts
1451
+ _assert_unfused_expert_module(split_experts)
1452
+ with torch.no_grad():
1453
+ actual_experts = split_experts(hidden_states, top_k_index, top_k_weights)
1454
+
1455
+ # The defused linear_loop path should reproduce the original fused experts
1456
+ # math. The full decoder forward is not exercised here because
1457
+ # ``bailing_hybrid``'s delta-attention path calls ``causal_conv1d``, which
1458
+ # requires a CUDA device.
1459
+ torch.testing.assert_close(actual_experts, expected_experts, atol=1e-5, rtol=1e-5)
1460
+
1461
+
1254
1462
  def test_laguna():
1255
1463
  from transformers.models.laguna.modeling_laguna import LagunaExperts
1256
1464
 
@@ -197,6 +197,11 @@ def _build_model_config(case: dict):
197
197
  config.ffn_config.ffn_act_fn = {"name": "silu"}
198
198
  elif model_type == "deepseek_v3":
199
199
  config.first_k_dense_replace = 0
200
+ elif model_type == "bailing_hybrid":
201
+ # The default config keeps the first layers dense, so the routed
202
+ # experts would not be constructed. Force every tiny layer onto MoE.
203
+ config.first_k_dense_replace = 0
204
+ config.mlp_layer_types = ["sparse"] * config.num_hidden_layers
200
205
  elif model_type == "deepseek_v32":
201
206
  config.mlp_layer_types = ["sparse"] * config.num_hidden_layers
202
207
  elif model_type == "ernie4_5_vl_moe":
@@ -346,6 +351,28 @@ def _validate_defused_module(case: dict, module) -> None:
346
351
 
347
352
 
348
353
  META_MODEL_CASES = [
354
+ {
355
+ "model_type": "afmoe",
356
+ "mode": "convert",
357
+ "model_module": "transformers.models.afmoe.modeling_afmoe",
358
+ "model_class": "AfmoeForCausalLM",
359
+ "config_module": "transformers.models.afmoe.configuration_afmoe",
360
+ "config_class": "AfmoeConfig",
361
+ "target_class_paths": ("transformers.models.afmoe.modeling_afmoe.AfmoeExperts",),
362
+ "validator": "experts",
363
+ },
364
+ {
365
+ "model_type": "bailing_hybrid",
366
+ "mode": "convert",
367
+ "model_module": "transformers.models.bailing_hybrid.modeling_bailing_hybrid",
368
+ "model_class": "BailingHybridForCausalLM",
369
+ "config_module": "transformers.models.bailing_hybrid.configuration_bailing_hybrid",
370
+ "config_class": "BailingHybridConfig",
371
+ "target_class_paths": (
372
+ "transformers.models.bailing_hybrid.modeling_bailing_hybrid.BailingHybridExperts",
373
+ ),
374
+ "validator": "experts",
375
+ },
349
376
  {
350
377
  "model_type": "axk2",
351
378
  "mode": "convert",
@@ -9,6 +9,7 @@ import torch
9
9
  import torch.nn as nn
10
10
 
11
11
  from defuser.modeling.moe_experts_interface import _detect_expert_projections, _unfuse_experts_weights_inplace
12
+ from defuser.model_registry import MODEL_CONFIG, PATCH
12
13
 
13
14
 
14
15
  class _UnknownExpertsWithLossProperty(nn.Module):
@@ -60,3 +61,14 @@ def test_unfuse_experts_supports_registered_buffers() -> None:
60
61
  torch.testing.assert_close(expert0.gate_proj.weight, expected_gate_proj)
61
62
  torch.testing.assert_close(expert0.up_proj.weight, expected_up_proj)
62
63
  torch.testing.assert_close(expert0.down_proj.weight, expected_down_proj)
64
+
65
+
66
+ def test_flint_registry_defuses_stacked_expert_weights() -> None:
67
+ specs = MODEL_CONFIG["flint"][PATCH.EXPERTS_DEFUSE]
68
+
69
+ assert specs == [
70
+ {
71
+ "module_class": "transformers.models.flint.modeling_flint.FlintExperts",
72
+ "forward_impl": "linear_loop",
73
+ }
74
+ ]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes