Skip to content

Commit 5ba283f

Browse files
committed
Guard fused QMoE CUDA routing
Keep fused QMoE activation inputs in the model dtype and fail fast for CUDA exports that require router_weights, since ORT CUDA QMoE currently ignores input 14. Add regression tests for activation dtype and the CUDA guard.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: justinchuby <justinchuby@users.noreply.github.com>
1 parent 3fb1729 commit 5ba283f

2 files changed

Lines changed: 67 additions & 3 deletions

File tree

src/mobius/components/_moe.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import torch
1515
from onnxscript import OpBuilder, nn
1616

17+
from mobius._build_context import ep_capabilities
1718
from mobius._configs import ArchitectureConfig, QuantizationConfig
1819
from mobius._weight_utils import preprocess_awq_weights, preprocess_gptq_weights
1920
from mobius.components._mlp import MLP
@@ -279,6 +280,7 @@ def __init__(
279280
config: ArchitectureConfig,
280281
gate: nn.Module | None = None,
281282
linear_class: type | None = None,
283+
enable_qmoe: bool = True,
282284
):
283285
super().__init__()
284286
assert config.num_local_experts is not None
@@ -296,7 +298,11 @@ def __init__(
296298
if config.moe_intermediate_size is not None
297299
else config
298300
)
299-
if self._qmoe_quantization is not None and hasattr(self.gate, "qmoe_routing"):
301+
if (
302+
enable_qmoe
303+
and self._qmoe_quantization is not None
304+
and hasattr(self.gate, "qmoe_routing")
305+
):
300306
self.experts = None
301307
self._init_qmoe_parameters(expert_config)
302308
else:
@@ -529,12 +535,20 @@ def __init__(
529535
)
530536

531537
def forward(self, op: OpBuilder, hidden_states: ir.Value) -> ir.Value:
538+
if ep_capabilities().name == "cuda":
539+
raise ValueError(
540+
"FusedQuantizedMoE is disabled for CUDA because ORT CUDA QMoE "
541+
"currently ignores router_weights (input 14), which is required "
542+
"for GLM/DeepSeek group-limited routing. Use the decomposed "
543+
"MatMulNBits export path for CUDA."
544+
)
532545
hidden = self._hidden
533546

534547
# QMoE requires 2-D router_probs, so flatten [B, S, H] -> [rows, H].
535548
orig_shape = op.Shape(hidden_states)
536549
flat = op.Reshape(hidden_states, op.Constant(value_ints=[-1, hidden]))
537-
# QMoE input/router_probs must be float32.
550+
# Router math must be float32, but QMoE activation input stays in the
551+
# model dtype: CUDA QMoE kernels are registered for fp16/bf16 inputs.
538552
flat_f32 = op.Cast(flat, to=1)
539553

540554
scores_for_choice, routing_weights, selected_experts = self._route(op, flat_f32)
@@ -545,7 +559,7 @@ def forward(self, op: OpBuilder, hidden_states: ir.Value) -> ir.Value:
545559
aggregation = op.ScatterElements(zeros, selected_experts, routing_weights, axis=-1)
546560

547561
moe_out = op.QMoE(
548-
flat_f32, # 0: input
562+
flat, # 0: input
549563
scores_for_choice, # 1: router_probs (selection logits)
550564
self.fc1_experts_weights, # 2
551565
op.Cast(self.fc1_scales, to=1), # 3

src/mobius/components/_moe_test.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,30 @@ def test_fused_qmoe_wires_asymmetric_zero_points():
186186
assert layer.fc2_experts_zero_points.shape == ir.Shape([4, 32, 1])
187187

188188

189+
def test_fused_qmoe_keeps_activation_input_model_dtype():
190+
config = make_config(
191+
hidden_size=32,
192+
intermediate_size=16,
193+
moe_intermediate_size=16,
194+
num_local_experts=4,
195+
num_experts_per_tok=2,
196+
quantization=QuantizationConfig(
197+
bits=4,
198+
group_size=16,
199+
quant_method="gptq",
200+
sym=True,
201+
),
202+
)
203+
layer = FusedQuantizedMoE(config, DeepSeekMoEGate(config))
204+
builder, op, graph = create_test_builder()
205+
hidden = create_test_input(builder, "hidden", [1, 2, 32], dtype=ir.DataType.FLOAT16)
206+
builder._adapt_outputs([layer(op, hidden)], "")
207+
208+
qmoe = next(node for node in graph if node.op_type == "QMoE")
209+
assert qmoe.inputs[0].dtype == ir.DataType.FLOAT16
210+
assert qmoe.inputs[1].dtype == ir.DataType.FLOAT
211+
212+
189213
def test_deepseek_fused_qmoe_graph_has_one_node_per_moe_layer():
190214
config = make_config(
191215
hidden_size=32,
@@ -230,6 +254,32 @@ def test_deepseek_fused_qmoe_graph_has_one_node_per_moe_layer():
230254
)
231255

232256

257+
def test_deepseek_fused_qmoe_rejects_cuda():
258+
config = make_config(
259+
hidden_size=32,
260+
intermediate_size=32,
261+
moe_intermediate_size=16,
262+
num_hidden_layers=2,
263+
first_k_dense_replace=1,
264+
num_local_experts=4,
265+
num_experts_per_tok=2,
266+
n_shared_experts=1,
267+
fused_quantized_moe=True,
268+
quantization=QuantizationConfig(
269+
bits=4,
270+
group_size=16,
271+
quant_method="gguf",
272+
sym=True,
273+
),
274+
)
275+
with pytest.raises(ValueError, match="CUDA.*ignores router_weights"):
276+
build_from_module(
277+
DeepSeekV3CausalLMModel(config),
278+
config,
279+
execution_provider="cuda",
280+
)
281+
282+
233283
def test_expert_major_packing_matches_static_64_expert_top6_reference():
234284
torch.manual_seed(0)
235285
experts, top_k = 64, 6

0 commit comments

Comments
 (0)