Skip to content

GLM-4 GPTQ import: encoder remap + fused-QKV split - #424

Merged
justinchuby merged 12 commits into
mainfrom
feat/glm4-gptq-import
Jul 25, 2026
Merged

GLM-4 GPTQ import: encoder remap + fused-QKV split#424
justinchuby merged 12 commits into
mainfrom
feat/glm4-gptq-import

Conversation

@justinchuby

@justinchuby justinchuby commented Jul 23, 2026

Copy link
Copy Markdown
Member

Summary

  • normalize legacy ChatGLM/GLM-4 config fields, including partial interleaved RoPE and projection biases
  • remap transformer.encoder GPTQ checkpoint names to Mobius canonical initializers
  • split converted fused QKV tensors into q/k/v MatMulNBits weights
  • pre-split repacked gate/up weights into separate gate/up MatMulNBits nodes, removing runtime activation Split seams
  • add focused config, packed-import, bias, and asymmetric zero-point tests

Validation

  • pytest src/mobius/models/chatglm_test.py src/mobius/_configs_test.py -q (83 passed)
  • pytest tests/build_graph_test.py -k chatglm -q (4 passed)
  • Ruff check and format validation passed
  • fresh 9B export: 280 block-128 MatMulNBits, 40 GQA, and 0 Split nodes
  • native CUDA GPU4 capture: 1 captured segment, 0 eager seams, 0 fallbacks
  • controlled release A/B: 110.39 -> 118.24 tok/s (+7.1%); 9.059 -> 8.457 ms/token
  • all 128 greedy token IDs and decoded text matched the prior fused export exactly

Artifact: /home/justinchu/glm-e2e-artifacts/glm-4-9b-int4-cuda-nosplit

Tokenizer note: the source repository only provides a custom slow tiktoken tokenizer. The smoke package uses the compatible fast tokenizer.json from THUDM/glm-4-9b-chat-hf.

This PR is intentionally left draft for Justin to review and merge.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 23, 2026 11:42
@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown

🏗️ Architecture Diff

Comparing 68fdc67cf3542a

Model Sub-model Changes Status
bert (feature-extraction) model 0
falcon model 0
gemma2 model 0
gemma4 (gemma4) decoder 0
gemma4 (gemma4) embedding 0
gemma4 (gemma4) vision_encoder 0
gemma4_text model 0
gpt2 model 0
llama model 0
llama (static-cache) model 0
mamba (ssm-text-generation) model 0
phi3 model 0
phi3 (static-cache) model 0
qwen model 0
qwen (static-cache) model 0
qwen2 model 0
qwen2 (static-cache) model 0
qwen2_moe model 0
qwen2_moe (static-cache) model 0
qwen3 model 0
qwen3 (static-cache) model 0
qwen3_5_moe (hybrid-text-generation) model 0
qwen3_5_text (hybrid-text-generation) model 0
qwen3_5_vl (hybrid-qwen-vl) decoder 0
qwen3_5_vl (hybrid-qwen-vl) embedding 0
qwen3_5_vl (hybrid-qwen-vl) vision_encoder 0
qwen3_moe model 0
qwen3_moe (static-cache) model 0
qwen3_next (hybrid-text-generation) model 0
t5 (seq2seq) decoder 0
t5 (seq2seq) encoder 0
whisper (speech-to-text) decoder 0
whisper (speech-to-text) encoder 0

No architecture changes detected.


Legend: ⚪ No change · 🔵 Minor (attrs/inits) · 🟡 Moderate (nodes added/removed) · 🔴 Major (interface changed)

@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown

Performance Comparison

Comparing 68fdc67cf3542a

Model Metric Baseline Current Delta
bert (feature-extraction) model_size_bytes 359 KB 359 KB +0.0%
bert (feature-extraction) num_nodes 60 60 +0.0%
falcon model_size_bytes 364 KB 364 KB +0.0%
falcon num_nodes 68 68 +0.0%
gemma2 model_size_bytes 428 KB 428 KB +0.0%
gemma2 num_nodes 107 107 +0.0%
gpt2 model_size_bytes 388 KB 388 KB +0.0%
gpt2 num_nodes 54 54 +0.0%
llama model_size_bytes 425 KB 425 KB +0.0%
llama num_nodes 62 62 +0.0%
llama (static-cache) model_size_bytes 425 KB 425 KB +0.0%
llama (static-cache) num_nodes 58 58 +0.0%
mamba (ssm-text-generation) model_size_bytes 296 KB 296 KB +0.0%
mamba (ssm-text-generation) num_nodes 98 98 +0.0%
phi3 model_size_bytes 421 KB 421 KB +0.0%
phi3 num_nodes 60 60 +0.0%
phi3 (static-cache) model_size_bytes 421 KB 421 KB +0.0%
phi3 (static-cache) num_nodes 56 56 +0.0%
qwen2 model_size_bytes 425 KB 425 KB +0.0%
qwen2 num_nodes 62 62 +0.0%
qwen2 (static-cache) model_size_bytes 425 KB 425 KB +0.0%
qwen2 (static-cache) num_nodes 58 58 +0.0%
qwen3_5_moe (hybrid-text-generation) model_size_bytes 506 KB 506 KB +0.0%
qwen3_5_moe (hybrid-text-generation) num_nodes 275 275 +0.0%
qwen3_5_text (hybrid-text-generation) model_size_bytes 458 KB 458 KB +0.0%
qwen3_5_text (hybrid-text-generation) num_nodes 129 129 +0.0%
qwen3_5_vl (hybrid-qwen-vl) model_size_bytes 977 KB 977 KB +0.0%
qwen3_5_vl (hybrid-qwen-vl) num_nodes 413 413 +0.0%
t5 (seq2seq) model_size_bytes 836 KB 836 KB +0.0%
t5 (seq2seq) num_nodes 166 166 +0.0%
whisper (speech-to-text) model_size_bytes 1008 KB 1008 KB +0.0%
whisper (speech-to-text) num_nodes 128 128 +0.0%

No performance regressions.

@codecov

codecov Bot commented Jul 23, 2026

Copy link
Copy Markdown

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves GLM-4 / ChatGLM compatibility in Mobius by normalizing legacy HuggingFace config fields and adding GPTQ checkpoint import handling for the original transformer.encoder.* weight layout, including splitting fused QKV projections after GPTQ preprocessing while keeping fused gate/up projections intact.

Changes:

  • Update ChatGLMCausalLMModel preprocessing to remap legacy checkpoint key prefixes and split fused QKV tensors into q_proj/k_proj/v_proj weights after GPTQ conversion.
  • Extend ArchitectureConfig.from_transformers() to recognize additional ChatGLM/GLM-4 legacy config fields (e.g., num_layers, kv_channels, seq_length, multi-query group fields, bias flags) and set ChatGLM-specific RoPE defaults.
  • Add focused unit tests for config extraction and GPTQ fused-QKV import/splitting.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
src/mobius/models/chatglm.py Remaps legacy transformer.encoder.* keys to Mobius canonical names, runs generic GPTQ preprocessing, then splits fused QKV into separate projections while using fused gate/up import.
src/mobius/models/chatglm_test.py Adds a targeted test covering legacy-name remap + GPTQ preprocessing + fused QKV split behavior.
src/mobius/_configs/_base.py Enhances config normalization for ChatGLM/GLM-4 legacy fields (layers/head_dim/KV heads/bias flags/seq_length) and sets partial_rotary_factor=0.5 for ChatGLM implicit RoPE defaults.
src/mobius/_configs_test.py Adds a regression test for ChatGLM legacy config-field extraction.

# LLaDA/OLMo expose the activation as ``activation_type`` (e.g. "silu").
or getattr(config, "activation_type", None)
or ("silu" if model_type in ("qwen",) else None)
or ("silu" if model_type in ("qwen", "chatglm") else None)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the _resolve_hidden_act() fallback documentation to cover both Qwen and ChatGLM SiLU fallbacks.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 23, 2026 12:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

src/mobius/_configs/_base.py:69

  • The _resolve_hidden_act() docstring still documents the hardcoded SiLU fallback as Qwen-only, but the implementation now also falls back to SiLU for model_type == "chatglm". Update the docstring so it stays accurate for future readers.
        # LLaDA/OLMo expose the activation as ``activation_type`` (e.g. "silu").
        or getattr(config, "activation_type", None)
        or ("silu" if model_type in ("qwen", "chatglm") else None)
        # gelu_activation is a boolean (XLM) — must be after all string

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 23, 2026 13:40
@justinchuby

Copy link
Copy Markdown
Member Author

Added P0 MLA attention-lowering guard in commit 00309be:

  • Both rotary and fallback Attention→GQA rewrites now decline when static K/V or past-K/V last dimensions differ.
  • DeepSeek-V2-Lite CUDA no-weight contract retains 27 ai.onnx::Attention, emits 0 GQA, and asserts cache head dims K=192/V=128.
  • Full _group_query_attention_test.py: 23 passed; Ruff check/format clean.
  • Existing Qwen/Qwen2-bias (Phi-style) and GLM interleaved-RoPE fusion tests remain green.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Comment thread src/mobius/models/chatglm_test.py Outdated
Comment on lines +212 to +216
@pytest.mark.arch_validation
def test_deepseek_v2_lite_unequal_kv_heads_retain_attention(self):
"""CUDA export must not rewrite unequal-dimension MLA K/V to GQA."""
with pytest.warns(UserWarning, match="GQA fusion expected"):
pkg = build(

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replaced the CUDA/HF arch-validation case with the self-contained CPU test in src/mobius/rewrite_rules/_group_query_attention_test.py. Its unequal and equal MLA K/V cases are not marked arch_validation; both are collected by the main CI selection and were run locally.

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Justin Chu <justinchuby@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 23, 2026 14:00
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@justinchuby

Copy link
Copy Markdown
Member Author

P0.2 expert-major int4 QMoE exporter landed in commit 7e82326.

  • Emits one com.microsoft::QMoE per supported GPTQ/AWQ MoE layer with FC1/FC2 packed as [E, out, packed_in], float32 scales [E, out, blocks], packed uint8 zero-points, and swiglu_fusion=2 for chunked gate/up rows.
  • Preserves the explicit router and supplies separate selection/aggregation tensors through input 14; DeepSeek routing scale is applied after QMoE. Shared experts remain separate dense MatMulNBits MLPs.
  • DeepSeek-V2-Lite int4 graph: 26 QMoE, 27 router/plain MatMul, 189 MatMulNBits, 27 Attention, 0 GQA; no .moe.experts.* expansion. Previous unquantized structural graph had 5,208 MatMuls.
  • Synthetic 64-expert/top-6 GPTQ packing differential matches the static loop-over-experts reference (atol=rtol=1e-5).
  • Validation: 150 targeted tests passed (including all 24 GQA rewrite tests); Ruff and git diff --check clean. Full weighted artifact smoke remains follow-up.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.

Copilot AI review requested due to automatic review settings July 23, 2026 14:07
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@justinchuby

Copy link
Copy Markdown
Member Author

Copilot review follow-ups are resolved in bb70029:

  1. Replaced the HF/network/CUDA-marked DeepSeek guard test with a self-contained one-layer MLA unit test. The unequal case (K=16, V=8) retains Attention; the equal case (K=V=16) rewrites to GroupQueryAttention. It has no arch_validation marker and is collected by the main CI expression -m "not integration and not arch_validation" (both parameter cases confirmed with --collect-only).
  2. Updated _resolve_hidden_act() documentation to describe the SiLU fallback for both Qwen and ChatGLM.
  3. chatglm_test.py now imports MLP from the public mobius.components API (already applied by 85fb2c6).

Validation: CI-selected GQA suite 24 passed / 1 deselected; ChatGLM tests 2 passed; Ruff and git diff --check clean.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.

Comment on lines +39 to +44
def qmoe_routing(self, op: OpBuilder, hidden_states: ir.Value):
"""Return selection and aggregation tensors for QMoE."""
weight_t = op.Transpose(self.weight, perm=[1, 0])
router_logits = op.MatMul(hidden_states, weight_t)
router_logits = op.Cast(router_logits, to=ir.DataType.FLOAT.value)
return router_logits, None, True, 1.0

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified against the ORT QMoE kernels — passing raw logits here is intentional and correct, so no routing-math change was needed (fixed by adding a clarifying docstring in 5b0903c).

TopKGate.qmoe_routing returns (router_logits_f32, router_weights=None, normalize=True, 1.0). With router_weights omitted and normalize_routing_weights=1, com.microsoft::QMoE reproduces TopKGate.forward exactly:

  • Selection: top-k is taken over router_probs by value. Softmax is monotonic, so argmax over logits == argmax over softmax — selection is identical.
  • Aggregation (router_weights=None path): the kernel computes softmax over the k selected logits. See contrib_ops/cpu/moe/moe_quantization_cpu.cc (default branch): route_scale = exp(logit_j - max) / Σ_{top-k} exp(logit). The CUDA path (ft_moe/moe_kernel.cu, fused topk_gating_softmax / moe_softmax+moe_top_k) computes full softmax then, for normalize=1, renormalizes over the selected k — the full-softmax denominator Z cancels, giving the same exp(logit)/Σ_{top-k} exp(logit).

That is precisely Softmax(TopK(router_logits)), i.e. TopKGate.forward.

Re: why router_weights differs across gates — the ORT schema (contrib_defs.cc, QMoE Input 14) says when router_weights is omitted, router_probs is used for both selection and the internal softmax-over-selected; when provided, its values are gathered at the selected experts (no internal softmax) and optionally renormalized. So TopKGate correctly delegates the softmax-over-selected to the op via None, whereas SoftmaxTopKGate/SigmoidTopKGate must pass their already-activated probs as router_weights to avoid a double softmax (or softmax-of-sigmoid). All three are mathematically consistent with their forward references.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correction to the prior reply: ORT CPU QMoE honors Input 14 (router_weights) and gathers it at selected experts, but CUDA QMoE ignores Input 14 and always runs softmax-top-k on router_probs (contrib_ops/cpu/moe/moe_quantization_cpu.cc; contrib_ops/cuda/moe/moe_quantization.cc). Therefore raw logits with router_weights=None in TopKGate are correct on both EPs: selected experts match by monotonic ordering and their weights equal Softmax(TopK(logits)). Activated router_weights is CPU-correct but double-activated on CUDA.

Copilot AI review requested due to automatic review settings July 23, 2026 14:15

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.

Comment on lines +261 to +265
from transformers import AutoConfig

hf_config = AutoConfig.from_pretrained(
"deepseek-ai/DeepSeek-V2-Lite", trust_remote_code=True
)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 5b0903c: the test is now self-contained and the arch_validation marker is removed, so it runs in the offline main CI (pytest -m 'not integration and not arch_validation').

Instead of downloading deepseek-ai/DeepSeek-V2-Lite from HuggingFace, it builds a tiny DeepSeek-V2-Lite-shaped model from an inline ArchitectureConfig (MLA attention, first_k_dense_replace=1, softmax routing, shared experts). hidden_size/moe_intermediate_size are multiples of the quant group_size (128) as QMoE requires; the small MLA/dense dims are fine for MatMulNBits (ceil-padded K). No network, no CUDA required.

The invariant is unchanged and still asserted: one com.microsoft::QMoE per routed MoE layer (2 QMoE / 3 Attention here), shared experts stay dense MatMulNBits, QMoE float routing inputs (1/3/6/14), and no .moe.experts. initializers leak into the graph. Verified green offline: pytest src/mobius/rewrite_rules/_group_query_attention_test.py -k deepseek_v2_lite -v passes with HF_HUB_OFFLINE=1 TRANSFORMERS_OFFLINE=1.

justinchuby and others added 2 commits July 24, 2026 17:12
Address Copilot review feedback questioning whether TopKGate.qmoe_routing
should pass raw router logits (instead of probabilities) as the QMoE
`router_probs` input. This is intentional and correct: with
`router_weights=None` and `normalize_routing_weights=1`, com.microsoft::QMoE
reproduces `TopKGate.forward` exactly — it selects the top-k experts by logit
value (softmax is monotonic) and, because `router_weights` is omitted, computes
aggregation weights as softmax over the k selected logits, i.e.
`Softmax(TopK(router_logits))`.

Verified against the ORT QMoE kernels: the CPU default path in
contrib_ops/cpu/moe/moe_quantization_cpu.cc computes `exp(logit - max) / sum`
over the selected experts, and the CUDA fused/unfused paths in
contrib_ops/cuda/moe/ft_moe/moe_kernel.cu apply full softmax then renormalize
over the selected k when normalize=1 (the softmax denominator cancels). The
Softmax/Sigmoid gates instead pass their activated probabilities via
`router_weights` to bypass the op's internal softmax-over-selected.

No routing math changes; adds a clarifying docstring only.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
`test_deepseek_v2_lite_int4_uses_one_qmoe_per_moe_layer` was marked
`@pytest.mark.arch_validation` but lives under `src/`, so it ran in no CI job:
main CI excludes `arch_validation` and nightly L2 only scans
`tests/arch_validation_test.py`. It also downloaded the config from HuggingFace
(`AutoConfig.from_pretrained("deepseek-ai/DeepSeek-V2-Lite", trust_remote_code=True)`).

Make it a self-contained regression guard: build a tiny DeepSeek-V2-Lite-shaped
model from an inline `ArchitectureConfig` (MLA attention, first_k_dense_replace,
softmax routing, shared experts) with no network access, and drop the
`arch_validation` marker so it runs in the offline main CI. hidden_size and
moe_intermediate_size are multiples of the quantization group_size (128) as QMoE
requires; small MLA/dense dims are fine for MatMulNBits (ceil-padded K).

The invariant is unchanged: one com.microsoft::QMoE per routed MoE layer, shared
experts stay dense MatMulNBits, QMoE float routing inputs (1/3/6/14), and no
`.moe.experts.` initializers leak into the graph.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 24, 2026 17:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

src/mobius/_configs/_base.py:312

  • Same as above: # ruff:ignore[unused-import] is likely not honored by Ruff. Prefer # noqa: F401 for this intentional side-effect import to avoid lint failures.
    from mobius._configs import per_model  # ruff:ignore[unused-import] - side effect
    from mobius._configs._extractors import extract_audio_config as _dispatch

Comment thread src/mobius/_configs/_base.py Outdated
registered with :mod:`mobius._configs._extractors` at import time.
"""
from mobius._configs import per_model # noqa: F401 - side-effect import
from mobius._configs import per_model # ruff:ignore[unused-import] - side effect

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a387ee2. Replaced the non-standard `# ruff:ignore[unused-import]` on both lines (298 and 311) with the repo-standard `# noqa: F401` (with a short "imported for registration side effect" note). This clears the RUF102 (invalid-rule-code) and F401 (unused-import) warnings that were failing Lint. lintrunner --all-files is now clean.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 24, 2026 17:30

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (3)

src/mobius/_configs/_base.py:298

  • # ruff:ignore[unused-import] is not a recognized Ruff suppression in this repo (and there are no other # ruff: directives elsewhere), so this likely won’t silence the intended unused-import warning for the side-effect import.
    from mobius._configs import per_model  # ruff:ignore[unused-import] - side effect

src/mobius/_configs/_base.py:311

  • # ruff:ignore[unused-import] is not a recognized Ruff suppression in this repo (and there are no other # ruff: directives elsewhere), so this likely won’t silence the intended unused-import warning for the side-effect import.
    from mobius._configs import per_model  # ruff:ignore[unused-import] - side effect

src/mobius/components/_moe.py:254

  • MoELayer’s QMoE fastpath is enabled for any gate that implements qmoe_routing. However, TopKGate.qmoe_routing’s docstring explicitly calls out that CUDA QMoE ignores router_weights and always uses softmax-top-k on router_probs, which cannot reproduce SigmoidTopKGate.forward (sigmoid-first routing). To avoid silently changing routing behavior on CUDA, SigmoidTopKGate should not go through the QMoE path.
        if self._qmoe_quantization is not None and hasattr(self.gate, "qmoe_routing"):
            self.experts = None
            self._init_qmoe_parameters(expert_config)

Comment thread src/mobius/components/_moe.py Outdated
Comment on lines +94 to +98
weight_t = op.Transpose(self.weight, perm=[1, 0])
router_logits = op.MatMul(hidden_states, weight_t)
router_logits = op.Cast(router_logits, to=ir.DataType.FLOAT.value)
routing_probs = op.Softmax(router_logits, axis=-1)
return routing_probs, routing_probs, self.norm_topk_prob, 1.0

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a387ee2. SoftmaxTopKGate.qmoe_routing (and SigmoidTopKGate.qmoe_routing on the other flagged line) now pass the raw float32 logits as router_probs (QMoE input 1) and the activated probabilities as router_weights (input 14), instead of passing the activated probs as both.

This matches the EP semantics established for TopKGate: CUDA QMoE ignores router_weights and runs softmax-top-k on router_probs, so feeding pre-activated probs there was a softmax-of-softmax (softmax-of-sigmoid for the sigmoid gate). Feeding logits reproduces forward on both EPs:

  • CUDA: softmax(logits) → top-k → renormalize == SoftmaxTopKGate.forward.
  • CPU: top-k selection over logits is identical (softmax/sigmoid are monotonic), and router_weights (the activated probs) is gathered at the selected experts and renormalized == forward.

The DeepSeek-V2-Lite int4 QMoE regression test (_group_query_attention_test.py, softmax routing) still passes — its FLOAT dtype/presence assertions on inputs 1/14 are unaffected. pytest src/mobius/components/_moe_test.py src/mobius/rewrite_rules/_group_query_attention_test.py -q → green.

- Replace invalid '# ruff:ignore[unused-import]' directives in _configs/_base.py
  with the repo-standard '# noqa: F401' for the per_model side-effect imports
  (fixes RUF102 + F401 Lint failures).
- Apply ruff-format to PR-touched files (_moe_test, _weight_utils_test, deepseek,
  _moe) that had line-length reflow violations.
- SoftmaxTopKGate/SigmoidTopKGate.qmoe_routing now pass raw float32 logits as the
  QMoE router_probs (input 1) and the activated probabilities as router_weights
  (input 14). CUDA QMoE ignores router_weights and applies softmax-top-k on
  router_probs, so passing activated probs there double-activated (softmax-of-
  softmax / softmax-of-sigmoid); logits reproduce forward on both EPs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 24, 2026 23:47
@justinchuby

Copy link
Copy Markdown
Member Author

Pushed a387ee2 to get Lint and test green:

Lint — now clean (lintrunner --all-files → "ok No lint issues"). Fixes: the invalid # ruff:ignore[unused-import] directives in _configs/_base.py (→ # noqa: F401, resolving RUF102 + F401) and ruff-format reflows in PR-touched files (_moe.py, _moe_test.py, _weight_utils_test.py, deepseek.py).

Unit test — the only remaining failures in the test job are the 11 src/mobius/models/nemo_rnnt_test.py cases (OverflowError: Python int too large to convert to C long in onnxscript's FoldConstantsPass). These are pre-existing and unrelated to this PR: they reproduce verbatim on origin/main (44bbfe0), and this PR touches no nemo/optimization/constant-folding code. They stem from an upstream onnxscript constant-folding regression and would need a shared-dependency pin or a core optimization-pass guard — out of scope for #424. Excluding those, all 4683 collected tests pass locally (pytest -n auto -m 'not integration and not arch_validation').

Review comments addressed in code + replied on-thread: the ruff:ignore directive fix, and the SoftmaxTopKGate/SigmoidTopKGate qmoe_routing fix (pass logits as router_probs, activated probs as router_weights — avoids the CUDA softmax-of-softmax / softmax-of-sigmoid). L4/L5 golden/e2e are out of scope (HF/GPU).

Note: overlaps with #404 (GLM-5.2 MoE export), which also edits _moe.py, _moe_test.py, _configs/_base.py, _builder.py, _deepseek_mla.py. Not consolidating per instruction — flagging for merge-order awareness.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.

Comment on lines 259 to +263
assert config.num_local_experts is not None
assert config.num_experts_per_tok is not None
self.num_experts = config.num_local_experts
self.top_k = config.num_experts_per_tok
self._qmoe_quantization = _supported_qmoe_quantization(config.quantization)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 98153fd. Updated the MoELayer class docstring to document both dispatch paths: the default loop-over-experts MLP path, and the fused com.microsoft::QMoE path (experts=None) selected when the quantization config matches the native QMoE ABI (_supported_qmoe_quantization) and the gate implements qmoe_routing. Also notes that the QMoE path packs quantized fc1/fc2 parameters instead of per-expert MLP modules, so the weight/preprocess expectations are clear.

…dispatch

MoELayer now switches to a fused com.microsoft::QMoE path (experts=None)
when the quantization config matches the native QMoE ABI and the gate
implements qmoe_routing, otherwise it uses the loop-over-experts MLP
dispatch. Update the class docstring to describe both paths so the
weight/preprocess expectations are clear when debugging.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 25, 2026 00:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.

Comment on lines +99 to +107
def qmoe_routing(self, op: OpBuilder, hidden_states: ir.Value):
"""Return distinct expert-selection and aggregation scores for QMoE."""
scores, scores_for_choice = self._routing_scores(op, hidden_states)
return (
scores_for_choice,
scores,
self.norm_topk_prob,
float(self.routed_scaling_factor),
)
Copilot AI review requested due to automatic review settings July 25, 2026 01:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.

@justinchuby
justinchuby marked this pull request as ready for review July 25, 2026 02:12
@justinchuby
justinchuby requested a review from a team July 25, 2026 02:12
@justinchuby
justinchuby merged commit 94a0423 into main Jul 25, 2026
22 of 24 checks passed
@justinchuby
justinchuby deleted the feat/glm4-gptq-import branch July 25, 2026 02:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants