Skip to content

Commit e1d4ae3

Browse files
committed
added LinearQwen3_5MoeSparseMoeBlock
Signed-off-by: ZX-ModelCloud <zx@modelcloud.ai>
1 parent 2cf8a46 commit e1d4ae3

6 files changed

Lines changed: 425 additions & 1 deletion

File tree

defuser/modeling/fused_moe/__init__.py

Whitespace-only changes.
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
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+
import torch
7+
from torch.nn import functional as F
8+
9+
from transformers.models.qwen3_5_moe.modeling_qwen3_5_moe import Qwen3_5MoeMLP
10+
from transformers.utils.versions import require_version
11+
12+
from defuser.modeling.fused_moe.replace_modules import ReplacementModuleBase
13+
from defuser.utils.device import clear_memory, unsupported_meta_device
14+
15+
require_version("transformers>=5.2.0")
16+
17+
from defuser.utils.model import _update_parameter
18+
19+
20+
class LinearQwen3_5MoeSparseMoeBlock(ReplacementModuleBase):
21+
def __init__(self, original, config):
22+
super().__init__(original)
23+
self.gate = original.gate
24+
text_config = config.get_text_config()
25+
self.shared_expert = original.shared_expert
26+
with torch.device("meta"):
27+
self.experts = SequentialQwen3_5MoeExperts(text_config, original.experts)
28+
self.shared_expert_gate = original.shared_expert_gate
29+
self.num_experts = text_config.num_experts
30+
31+
@classmethod
32+
def original_module_class(cls) -> str:
33+
"""Return the class name of the module this replaces."""
34+
return "Qwen3_5MoeSparseMoeBlock"
35+
36+
def _materialize_weights(self) -> None:
37+
original = self._get_original_module()
38+
self.experts._materialize_weights(original.experts)
39+
clear_memory()
40+
41+
def experts_forward(
42+
self,
43+
hidden_states: torch.Tensor,
44+
top_k_index: torch.Tensor,
45+
top_k_weights: torch.Tensor,
46+
) -> torch.Tensor:
47+
final_hidden_states = torch.zeros_like(hidden_states)
48+
with torch.no_grad():
49+
expert_mask = torch.nn.functional.one_hot(top_k_index, num_classes=self.num_experts)
50+
expert_mask = expert_mask.permute(2, 1, 0)
51+
expert_hit = torch.greater(expert_mask.sum(dim=(-1, -2)), 0).nonzero()
52+
53+
for expert_idx in expert_hit:
54+
expert_idx = expert_idx[0]
55+
if expert_idx == self.num_experts:
56+
continue
57+
top_k_pos, token_idx = torch.where(expert_mask[expert_idx])
58+
current_state = hidden_states[token_idx]
59+
# gate, up = nn.functional.linear(current_state, self.gate_up_proj[expert_idx]).chunk(2, dim=-1)
60+
# current_hidden_states = self.act_fn(gate) * up
61+
# current_hidden_states = nn.functional.linear(current_hidden_states, self.down_proj[expert_idx])
62+
current_hidden_states = self.experts[expert_idx](current_state)
63+
current_hidden_states = current_hidden_states * top_k_weights[token_idx, top_k_pos, None]
64+
final_hidden_states.index_add_(0, token_idx, current_hidden_states.to(final_hidden_states.dtype))
65+
66+
return final_hidden_states
67+
68+
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
69+
batch_size, sequence_length, hidden_dim = hidden_states.shape
70+
hidden_states_reshaped = hidden_states.view(-1, hidden_dim)
71+
shared_expert_output = self.shared_expert(hidden_states_reshaped)
72+
_, routing_weights, selected_experts = self.gate(hidden_states_reshaped)
73+
expert_output = self.experts_forward(hidden_states_reshaped, selected_experts, routing_weights)
74+
75+
shared_expert_output = F.sigmoid(self.shared_expert_gate(hidden_states_reshaped)) * shared_expert_output
76+
77+
expert_output += shared_expert_output
78+
expert_output = expert_output.reshape(batch_size, sequence_length, hidden_dim)
79+
return expert_output
80+
81+
@classmethod
82+
def from_original(
83+
cls,
84+
original,
85+
config,
86+
**kwargs,
87+
):
88+
"""Create an instance from the original module."""
89+
return cls(original, config)
90+
91+
92+
class SequentialQwen3_5MoeExperts(torch.nn.ModuleList):
93+
def __init__(self, config, original):
94+
super().__init__()
95+
self.num_experts = original.gate_up_proj.shape[0]
96+
intermediate_size = config.moe_intermediate_size
97+
98+
with torch.device("meta"):
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+
original.to_empty(device="meta") # release original experts parameters
116+
clear_memory()
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
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 abc import ABC, abstractmethod
7+
8+
import torch
9+
from typing import Dict, Type
10+
from defuser.logger import logger
11+
from dataclasses import dataclass
12+
13+
14+
class ReplacementModuleBase(ABC, torch.nn.Module):
15+
"""
16+
Abstract base class for module replacement during calibration phase.
17+
18+
Replacement modules replace original modules to ensure all components
19+
receive data for proper quantization statistics.
20+
21+
Subclasses must:
22+
1. Implement `original_module_class()` to return the target module class name
23+
2. Implement `__init__()` with signature:
24+
(self, original, config)
25+
"""
26+
27+
# Registry: module class name -> replacement module class
28+
_replacement_registry: Dict[str, Type["ReplacementModuleBase"]] = {}
29+
30+
def __init_subclass__(cls, **kwargs):
31+
"""Automatically register subclasses in the replacement registry."""
32+
super().__init_subclass__(**kwargs)
33+
34+
# Only register if it's a concrete implementation (not ABC)
35+
if not getattr(cls, "__abstractmethods__", None):
36+
if cls.original_module_class() is None:
37+
raise TypeError(
38+
f"{cls.__name__} must implement 'original_module_class()' class method "
39+
"to return the name of the module class it replaces"
40+
)
41+
42+
if cls.original_module_class() in cls._replacement_registry:
43+
existing = cls._replacement_registry[cls.original_module_class()]
44+
raise ValueError(
45+
f"Module '{cls.original_module_class()}' already registered to "
46+
f"{existing.__name__}. Cannot register {cls.__name__}."
47+
)
48+
49+
cls._replacement_registry[cls.original_module_class()] = cls
50+
logger.trace(f"Registered {cls.__name__} for replacing {cls.original_module_class()}")
51+
52+
def __init__(self, original: torch.nn.Module):
53+
super().__init__()
54+
_global_tracker.register_replacement(
55+
name=str(id(self)),
56+
original=original,
57+
replacement=self,
58+
)
59+
self._materialized = False
60+
61+
@classmethod
62+
def get_replacement_class(cls, module_class_name: str) -> Type["ReplacementModuleBase"]:
63+
"""Get replacement class for a given module class name."""
64+
return cls._replacement_registry.get(module_class_name)
65+
66+
@classmethod
67+
def is_registered(cls, module_class_name: str) -> bool:
68+
"""Check if a module class has a replacement implementation."""
69+
return module_class_name in cls._replacement_registry
70+
71+
@classmethod
72+
def is_to_be_replaced(
73+
cls,
74+
original: torch.nn.Module,
75+
) -> bool:
76+
"""Determine if the given module should be replaced.
77+
78+
Users can extend this method to add custom logic for replacement.
79+
"""
80+
return cls.is_registered(original.__class__.__name__)
81+
82+
@classmethod
83+
def get_registered_modules(cls) -> list:
84+
"""Get list of all registered module class names."""
85+
return list(cls._replacement_registry.keys())
86+
87+
@classmethod
88+
@abstractmethod
89+
def original_module_class(cls) -> str:
90+
"""Return the class name of the module this replaces."""
91+
pass
92+
93+
@classmethod
94+
@abstractmethod
95+
def from_original(
96+
cls,
97+
original: torch.nn.Module,
98+
config,
99+
) -> "ReplacementModuleBase":
100+
"""Create replacement module from original module."""
101+
pass
102+
103+
def materialize_weights(self):
104+
"""Materialize weights if needed."""
105+
if not self._materialized:
106+
self._materialize_weights()
107+
self.post_process_materialization()
108+
109+
def _materialize_weights(self) -> None:
110+
"""Materialize weights from the original module.
111+
112+
Subclasses should override this method to implement
113+
weight materialization logic.
114+
"""
115+
pass
116+
117+
def release_original_module(self) -> None:
118+
"""Release reference to the original module to free memory."""
119+
# Release from global tracker
120+
_global_tracker.release_original(self)
121+
122+
def _get_original_module(self) -> torch.nn.Module:
123+
"""Get the original module associated with this replacement."""
124+
return _global_tracker.get_original(self)
125+
126+
def post_process_materialization(self) -> None:
127+
"""Mark the replacement module as materialized."""
128+
self._materialized = True
129+
self.release_original_module()
130+
131+
132+
@dataclass
133+
class ReplacedModuleInfo:
134+
original_module: torch.nn.Module
135+
replacement_module: ReplacementModuleBase
136+
137+
138+
class ModuleReplacementTracker:
139+
"""Tracker to maintain mapping between replacement modules and their original modules.
140+
141+
This is a singleton class - only one instance can exist.
142+
"""
143+
144+
_instance = None
145+
_initialized = False
146+
147+
def __new__(cls):
148+
if cls._instance is None:
149+
cls._instance = super(ModuleReplacementTracker, cls).__new__(cls)
150+
return cls._instance
151+
152+
def __init__(self):
153+
# Only initialize once
154+
if ModuleReplacementTracker._initialized:
155+
return
156+
157+
# Map from replacement module id to original module
158+
self._replacement_to_original: Dict[int, torch.nn.Module] = {}
159+
# Map from module name to ReplacedModuleInfo
160+
self._name_to_info: Dict[str, ReplacedModuleInfo] = {}
161+
162+
ModuleReplacementTracker._initialized = True
163+
164+
@classmethod
165+
def get_instance(cls) -> "ModuleReplacementTracker":
166+
"""Get the singleton instance of the tracker."""
167+
if cls._instance is None:
168+
cls._instance = cls()
169+
return cls._instance
170+
171+
def register_replacement(self, name: str, original: torch.nn.Module, replacement: ReplacementModuleBase) -> None:
172+
"""Register a module replacement."""
173+
self._replacement_to_original[id(replacement)] = original
174+
self._name_to_info[name] = ReplacedModuleInfo(original_module=original, replacement_module=replacement)
175+
logger.trace(f"Registered replacement for module: {name}")
176+
177+
def get_original(self, replacement: ReplacementModuleBase) -> torch.nn.Module:
178+
"""Get the original module for a given replacement module."""
179+
return self._replacement_to_original.get(id(replacement))
180+
181+
def get_info_by_name(self, name: str) -> ReplacedModuleInfo:
182+
"""Get replacement info by module name."""
183+
return self._name_to_info.get(name)
184+
185+
def release_original(self, replacement: ReplacementModuleBase) -> None:
186+
"""Release the original module associated with a replacement module."""
187+
replacement_id = id(replacement)
188+
if replacement_id in self._replacement_to_original:
189+
original = self._replacement_to_original[replacement_id]
190+
# Delete the original module to free memory
191+
del original
192+
del self._replacement_to_original[replacement_id]
193+
logger.trace(f"Released original module for replacement {replacement_id}")
194+
195+
def release_all_originals(self) -> None:
196+
"""Release all tracked original modules."""
197+
count = len(self._replacement_to_original)
198+
if count > 0:
199+
self._replacement_to_original.clear()
200+
logger.debug(f"Released {count} original modules from tracker")
201+
202+
def clear(self) -> None:
203+
"""Clear all tracked information."""
204+
self._replacement_to_original.clear()
205+
self._name_to_info.clear()
206+
logger.debug("Cleared module replacement tracker")
207+
208+
209+
_global_tracker = ModuleReplacementTracker()

0 commit comments

Comments
 (0)