Skip to content

Add GLM-5.2 IndexShare DSA, MTP, and GGUF export support - #404

Open
justinchuby wants to merge 25 commits into
mainfrom
glm5.2-moe-export
Open

Add GLM-5.2 IndexShare DSA, MTP, and GGUF export support#404
justinchuby wants to merge 25 commits into
mainfrom
glm5.2-moe-export

Conversation

@justinchuby

@justinchuby justinchuby commented Jul 16, 2026

Copy link
Copy Markdown
Member

Summary

  • register Hugging Face glm_moe_dsa and llama.cpp/GGUF glm-dsa
  • export the 78-layer MLA+MoE backbone with GLM-5.2 IndexShare DSA
  • implement the fp32 indexer, official full/shared schedule (0, 1, 2, then every four layers from 6), cross-layer top-k reuse, and packed index-key cache
  • export the improved layer-78 MTP head as mtp/model.onnx, including embedding/hidden fusion, DSA+MoE block, and shared-head norm
  • map DSA indexer and supported NextN tensors from HF and GGUF
  • emit mtp_config.json for --runtime onnx-genai / ort-genai
  • retain dense MLA fallback through --glm-full-attention
  • preserve Tyrell's Q4/Q8 MatMulNBits and split-MLA/expert-streaming support

Runtime status

Portable ONNX preserves DSA selection numerics with standard ops (MatMul, LayerNormalization, Relu, TopK, ScatterElements, and Attention). No custom op is required for correctness. A selected-token sparse-attention kernel is still needed to realize GLM-5.2's long-context FLOP reduction instead of evaluating masked positions.

The MTP graph and step-0 topk_indices are exported. ORT GenAI does not currently expose separate MLA/indexer cache lengths or native MTP sidecar orchestration, so index_share_for_mtp_iteration remains a runtime follow-up; later speculative iterations recompute selection rather than silently reusing incorrect cache state.

IQ1/IQ2/IQ3 preservation remains a separate workstream and is unchanged here.

Validation

  • focused GLM/GGUF/ORT export set: 160 passed
  • related builder/config/task/MLA regressions: 279 passed
  • generic GLM graph/coverage/parity checks: 9 passed, 2 skipped
  • full non-integration suite excluding the pre-existing flaky ModernBERT parity case: 4509 passed, 329 skipped, 60 xfailed
  • generated target and MTP ONNX files pass onnx.checker, load in ONNX Runtime CPU, and execute one-token prefill with empty caches
  • Ruff import/unused checks and git diff --check pass

Follow-ups

  • add an efficient selected-token sparse-attention runtime kernel
  • add ORT GenAI native MTP orchestration with independent indexer-cache/control state
  • add an efficient selected-expert MoE runtime kernel
  • handle IQ1_M/IQ2_XXS/IQ3_XXS/IQ4_XS in the separate sub-4-bit workstream

@github-actions

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown

Performance Comparison

Comparing 37910805ba283f

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.

@github-actions

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown

🏗️ Architecture Diff

Comparing 37910805ba283f

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)

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

Adds first-pass export support for GLM-5.2 / GLM-MoE DSA by wiring the architecture into the registry, enabling GGUF (glm-dsa) config/weight ingestion (including split MLA KV-B fusion and expert-by-expert streaming), and extending the quantized import path to cover additional supported layouts. Also accepts --runtime onnx-genai as an alias for ort-genai.

Changes:

  • Register glm_moe_dsa (HF) / glm-dsa (GGUF) and add a GLM-specific model wrapper reusing the DeepSeek-V3 MLA+MoE backbone with dense-attention fallback.
  • Extend GGUF import: map GLM tensor names, extract GLM metadata into ArchitectureConfig, fuse split MLA KV-B weights, and stream stacked expert tensors.
  • Extend quantized repacking/CLI: support float requantization to 8-bit block-32 targets and accept --runtime onnx-genai.

Reviewed changes

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

Show a summary per file
File Description
tests/cli_test.py Adds CLI coverage for --runtime onnx-genai alias.
src/mobius/main.py Implements onnx-genai as an alias for ort-genai.
src/mobius/models/glm_moe_dsa.py Adds GLM-5.2 DSA model wrapper over DeepSeek MLA+MoE with weight filtering.
src/mobius/models/glm_moe_dsa_test.py Adds GLM-specific graph/config/quantization/preprocess tests.
src/mobius/models/deepseek.py Adds quantized linear/embedding injection via linear_class + QuantizedEmbedding option.
src/mobius/components/_deepseek_mla.py Threads linear_class through MLA projections.
src/mobius/components/_moe.py Threads linear_class into expert MLPs.
src/mobius/models/init.py Exports GlmMoeDsaCausalLMModel.
src/mobius/_registry.py Registers glm_moe_dsa and adds a default model id.
src/mobius/_configs/_base.py Adds DSA/MTP-related config fields and enables rope_interleave default for glm_moe_dsa.
src/mobius/integrations/gguf/_tensor_mapping.py Adds GLM-DDA tensor mapping + skips indexer/nextn tensors.
src/mobius/integrations/gguf/_tensor_mapping_test.py Tests GLM tensor mapping behavior.
src/mobius/integrations/gguf/_config_mapping.py Adds GLM metadata key mapping + postprocess to derive MLA/DSA fields.
src/mobius/integrations/gguf/_reader_test.py Tests GGUF arch→model_type mapping and GLM config extraction.
src/mobius/integrations/gguf/_builder.py Adds split KV-B fusion, expert-by-expert repack, and allows 8-bit requant targets.
src/mobius/integrations/gguf/_builder_test.py Tests GLM normalization behaviors and Q8 requantized head behavior.
src/mobius/integrations/gguf/_repacker.py Extends float requantization to bits=8, block=32.
src/mobius/integrations/gguf/_repacker_test.py Adds Q8 float requantization round-trip bound test.
docs/research/glm52-export.md Documents GLM-5.2 verified architecture, current scope, and follow-ups.

Comment on lines +689 to +713
if source_params == (target_bits, target_block_size):
repacked = repack_gguf_tensor(
raw_expert.ravel().view(np.uint8),
qtype_val,
(n_out, k_in),
)
else:
_require_supported_requantization(
bits=target_bits,
block_size=target_block_size,
tensor_name=hf_name,
)
values = gguf_model.dequantize_raw_tensor(
raw_expert,
qtype,
(n_out, k_in),
)
repacked = repack_dequantized_tensor(
values,
bits=target_bits,
block_size=target_block_size,
symmetric=target_symmetric,
)
n_requantized += 1

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 751645b. The per-expert fast path now checks whether the repacked tensor zero-point presence matches the target symmetric/asymmetric config. A mismatch forces dequantize+requantize, ensuring required zero-point initializers are emitted with the target semantics.

Comment on lines +16 to +20
def _tiny_glm_moe_dsa_config(**overrides):
defaults = dict(
model_type="glm_moe_dsa",
num_hidden_layers=4,
num_attention_heads=4,

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.

Confirmed the branch now includes a representative tiny glm_moe_dsa entry in tests/_test_configs.py, marked representative so config-driven build and alignment suites cover it.

@justinchuby justinchuby changed the title Add GLM-5.2 / GLM-MoE export support Add GLM-5.2 IndexShare DSA, MTP, and GGUF export support Jul 16, 2026
@justinchuby

Copy link
Copy Markdown
Member Author

♻️ Rebased onto main to pick up #398 (onnx-genai InferenceMetadata export). Resolved a __main__.py conflict: --runtime onnx-genai now routes to #398's write_inference_metadata (emits inference_metadata.yaml) instead of the earlier ad-hoc ort-genai alias, and the stale alias CLI test was updated to assert the metadata path. GLM-5.2 MoE + IndexShare DSA + MTP commits are intact on top.

justinchuby and others added 7 commits July 21, 2026 22:47
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The IndexShare DSA indexer shares the model's rotary_emb cos/sin cache,
which is sized qk_rope_head_dim/2. GlmMoeDsaIndexer.project_key passed
rotary_embedding_dim=index_head_dim//2 to the opset-24 RotaryEmbedding op,
which then expects a cos cache of (index_head_dim//2)/2 and fails shape
validation at runtime:

  Input 'cos_cache' dimension 2 should be same as head_size / 2 or
  rotary_embedding_dim / 2, got 4

The indexer key spans the full index_head_dim and must be fully rotated,
matching the main MLA q_rope/k_rope calls (rotary_embedding_dim=0). Fixes
end-to-end inference of glm_moe_dsa models through the onnx-genai engine.

Adds a regression test asserting indexer RotaryEmbedding nodes use full
rotation (rotary_embedding_dim=0).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sibling of export_glm_tiny.py. Attaches an int4 block-32 asymmetric
QuantizationConfig so the tiny glm_moe_dsa linear projections and
per-expert MoE MLPs are emitted as com.microsoft::MatMulNBits, producing
an onnx-genai-loadable artifact for the quantized E2E smoke test.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…experts

Add FusedQuantizedMoE component that emits a single int4/block-32 expert-major
com.microsoft::QMoE node (swiglu_fusion=1) for the shared GLM/DeepSeek routed
expert stack, gated behind ArchitectureConfig.fused_quantized_moe. GLM sigmoid +
noaux_tc routing is preserved exactly: scores_for_choice feeds QMoE router_probs
for top-k selection, and the normalized+scaled combine weights are scattered
into the optional router_weights input (normalize_routing_weights=0). Adds
DeepSeekMoEGate.route_for_qmoe, export_glm_tiny_qmoe.py, and a fused-path unit
test.

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

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 30 out of 30 changed files in this pull request and generated 6 comments.

Comment on lines +289 to +291
qc = config.quantization
bits = qc.bits
block_size = qc.group_size

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 751645b by wiring asymmetric quantization through. FusedQuantizedMoE now declares packed FC1/FC2 zero-point parameters when sym=False and passes them to QMoE inputs 11/12; symmetric configs continue using implicit midpoint zero-points.

Comment thread export_glm_tiny_quant.py Outdated
Comment on lines +7 to +11
The GLM/DeepSeek MoE path decomposes experts into per-expert MLPs, so each
expert's gate/up/down projections also become MatMulNBits when quantized.
mobius has no fused ``com.microsoft::QMoE`` emitter for any model (only an
unquantized ``com.microsoft::MoE`` path in gemma4), so QMoE is NOT emitted
here — see the decision note for the scoping gap.

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 docstring to distinguish this decomposed MatMulNBits exporter from the fused QMoE path in export_glm_tiny_qmoe.py.

Comment thread export_glm_tiny_quant.py Outdated
Comment on lines +35 to +36
OUT = "/home/justinchu/glm-e2e-artifacts/glm-5.2-tiny-q4"
FP32_OUT = "/home/justinchu/glm-e2e-artifacts/glm-5.2-tiny"

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 absolute paths with repo-relative artifacts/ defaults, configurable via --output-dir, --fp32-output-dir, or MOBIUS_ARTIFACTS_DIR.

Comment thread export_glm_tiny_qmoe.py Outdated
Comment on lines +35 to +36
OUT = "/home/justinchu/glm-e2e-artifacts/glm-5.2-tiny-qmoe"
FP32_OUT = "/home/justinchu/glm-e2e-artifacts/glm-5.2-tiny"

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 absolute paths with repo-relative artifacts/ defaults, configurable via --output-dir, --fp32-output-dir, or MOBIUS_ARTIFACTS_DIR.

Comment thread export_deepseek_v2_tiny.py Outdated
from mobius.integrations.onnx_genai import write_inference_metadata
from mobius.tasks import get_task

OUT = "/home/justinchu/ds-e2e-artifacts/deepseek-v2-tiny"

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 absolute path with a repo-relative artifacts/ default, configurable via --output-dir or MOBIUS_ARTIFACTS_DIR.

Comment on lines +63 to +65
Multi-component packages contain `model/model.onnx`, `mtp/model.onnx`, and an
`mtp_config.json` sidecar. Both runtime aliases (`onnx-genai` and `ort-genai`)
emit these artifacts. ORT GenAI does not yet orchestrate the sidecar itself.

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.

Chose option (a): onnx-genai now recognizes the exact {model, mtp} package, emits normal decoder metadata plus the same mtp_config.json contract as ORT GenAI, and reports it through the CLI. This was lower risk than weakening the documented cross-runtime artifact contract because it is a narrow exact-shape dispatch with focused tests.

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

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 30 out of 30 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (5)

src/mobius/components/_moe.py:296

  • FusedQuantizedMoE is documented as symmetric-only (no zero-points), but init does not enforce QuantizationConfig.sym=True. If a caller enables fused_quantized_moe with asymmetric quantization, the graph will still be built but will pass None for *zero_points inputs to QMoE, which is inconsistent with the quantization setup.
        qc = config.quantization
        bits = qc.bits
        block_size = qc.group_size
        if bits not in (1, 2, 4, 8):
            raise ValueError(f"QMoE expert_weight_bits must be 1/2/4/8, got {bits}")
        if block_size < 16 or (block_size & (block_size - 1)):
            raise ValueError(f"QMoE block_size must be a power of 2 >= 16, got {block_size}")

export_glm_tiny_quant.py:11

  • This docstring states mobius has no fused com.microsoft::QMoE emitter and that QMoE is not emitted, but this PR introduces FusedQuantizedMoE and export_glm_tiny_qmoe.py. The docstring is now inaccurate and will mislead readers.
mobius has no fused ``com.microsoft::QMoE`` emitter for any model (only an
unquantized ``com.microsoft::MoE`` path in gemma4), so QMoE is NOT emitted
here — see the decision note for the scoping gap.

export_glm_tiny_quant.py:36

  • Hard-coded absolute output paths make this script non-portable (and leak a developer-specific directory layout into the repo). Prefer environment variables and a safe default like /tmp so contributors can run it without editing source.
OUT = "/home/justinchu/glm-e2e-artifacts/glm-5.2-tiny-q4"
FP32_OUT = "/home/justinchu/glm-e2e-artifacts/glm-5.2-tiny"

export_glm_tiny_qmoe.py:36

  • Hard-coded absolute output paths make this script non-portable. Prefer environment variables and a safe default like /tmp so contributors can run it without editing source.
OUT = "/home/justinchu/glm-e2e-artifacts/glm-5.2-tiny-qmoe"
FP32_OUT = "/home/justinchu/glm-e2e-artifacts/glm-5.2-tiny"

export_deepseek_v2_tiny.py:19

  • Hard-coded absolute output path makes this script non-portable. Prefer an environment variable and a safe default like /tmp so contributors can run it without editing source.
OUT = "/home/justinchu/ds-e2e-artifacts/deepseek-v2-tiny"

Comment on lines +324 to +331
qc = config.quantization
use_fused_qmoe = (
config.fused_quantized_moe and qc is not None and qc.quant_method != "none"
)
if use_fused_qmoe:
self.moe = FusedQuantizedMoE(config, gate=gate)
else:
self.moe = MoELayer(config, gate=gate, linear_class=linear_class)

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 751645b. DeepSeek preprocessing now diverts routed-expert tensors into a QMoE packer: fused 3-D GPTQ/AWQ tensors and per-expert GGUF tensors become expert-major FC1/FC2 weights, scales, and zero-points. FC1 gate/up rows are interleaved for swiglu_fusion=1; shared experts remain on the dense path.

Copilot AI review requested due to automatic review settings July 21, 2026 23:01

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 30 out of 30 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (4)

src/mobius/models/deepseek.py:328

  • fused_quantized_moe switches the MoE implementation to FusedQuantizedMoE, which expects packed expert-major tensors (fc1_experts_weights, fc1_scales, fc2_experts_weights, fc2_scales) instead of the existing per-expert projections. The current GGUF loader streams routed expert tensors into per-expert keys (e.g. ...mlp.experts.{i}.gate_proj.weight), and existing preprocessors map those under ...mlp.moe.experts.*, so enabling this flag is likely to make weight application fail due to missing packed initializers. Either add a packing step (similar to Gemma4’s fused-expert preprocessing) or raise a clear error when packed tensors aren’t provided.
        qc = config.quantization
        use_fused_qmoe = (
            config.fused_quantized_moe and qc is not None and qc.quant_method != "none"
        )
        if use_fused_qmoe:

export_glm_tiny_quant.py:11

  • The module docstring claims mobius has no fused com.microsoft::QMoE emitter, but this PR adds FusedQuantizedMoE and a fused_quantized_moe config flag. This comment is now misleading for readers using these export helpers.
mobius has no fused ``com.microsoft::QMoE`` emitter for any model (only an
unquantized ``com.microsoft::MoE`` path in gemma4), so QMoE is NOT emitted
here — see the decision note for the scoping gap.

export_glm_tiny_quant.py:36

  • Hard-coded absolute output paths (/home/justinchu/...) make this script non-portable and will fail in most environments/CI. Consider deriving these from CLI args or environment variables with a safe default (e.g., relative to the current working directory).
OUT = "/home/justinchu/glm-e2e-artifacts/glm-5.2-tiny-q4"
FP32_OUT = "/home/justinchu/glm-e2e-artifacts/glm-5.2-tiny"

export_glm_tiny_qmoe.py:36

  • Hard-coded absolute output paths (/home/justinchu/...) make this script non-portable and will fail in most environments/CI. Consider deriving these from CLI args or environment variables with a safe default (e.g., relative to the current working directory).
OUT = "/home/justinchu/glm-e2e-artifacts/glm-5.2-tiny-qmoe"
FP32_OUT = "/home/justinchu/glm-e2e-artifacts/glm-5.2-tiny"

Comment thread tests/cli_test.py Outdated
Comment on lines +256 to +258
def test_runtime_onnx_genai_calls_write_onnx_genai_config(self):
"""--runtime onnx-genai calls the unified config writer."""
with (

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.

Chose option (a): onnx-genai now recognizes the exact {model, mtp} package and emits mtp_config.json; the CLI test verifies the returned sidecar is surfaced. The change is narrowly scoped to that exact component set, so it preserves the existing rejection of unknown multi-component packages.

Comment thread src/mobius/models/glm_moe_dsa.py Outdated
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 21, 2026 23:15
Forward fused GPTQ g_idx tensors, omit symmetric 8-bit zero points, and pass string paths to ONNX Runtime sessions.

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

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 47 out of 47 changed files in this pull request and generated 3 comments.

Comment thread export_glm_tiny_quant.py Outdated
Comment thread export_glm_tiny_qmoe.py Outdated
Comment thread src/mobius/__main__.py
Comment on lines +577 to +581
build_parser.add_argument(
"--glm-full-attention",
action="store_true",
help="Disable GLM-5.2 IndexShare DSA and export the dense MLA fallback.",
)
@justinchuby

Copy link
Copy Markdown
Member Author

⚠️ Heads-up: QMoE router_weights is honored on the ORT CPU EP but ignored on the CUDA EP — affects this PR's fused_quantized_moe path

While reviewing the related GLM-4 GPTQ PR (#424), we verified an ORT-side EP divergence that directly impacts the opt-in fused-QMoE path introduced here. Sharing so it isn't lost — no code changed on this branch.

Verified ORT semantics (com.microsoft::QMoE, Input 14 router_weights):

  • CPU EPcontrib_ops/cpu/moe/moe_quantization_cpu.cc:912,983-1043: reads Input 14; when provided, gathers those weights at the selected experts (no internal softmax). ✅ correct for activated gates.
  • CUDA EPcontrib_ops/cuda/moe/moe_quantization.cc:200-238,457-489: never reads Input 14; always LaunchSoftmaxTopK(router_probs).

Consequence for this PR: FusedQuantizedMoE passes already-activated scores as router_probs and duplicates the intended weights into router_weights. On CPU that's correct; on CUDA the kernel re-applies softmax over the already sigmoid/softmax-activated router_probsdouble activation → numerically wrong routing weights for the sigmoid/softmax DeepSeekMoEGate path (GLM-5.2, DeepSeek-V3/dots1 sigmoid, DeepSeek-V2 softmax). Plain TopKGate (logits + router_weights=None) is unaffected — correct on both EPs.

Severity: currently latent — the flag is default-off (_configs/_base.py), only the synthetic export_glm_tiny_qmoe.py enables it, the artifact is EP-agnostic with no CUDA guard, and there is no dense-vs-QMoE numerical parity test that would catch it.

Suggested fix (before this path is enabled on CUDA): emit EP-neutral routing — bake the exact intended per-expert weights into router_probs as masked log(weight) (non-selected experts at a large negative), so CUDA's mandatory softmax reconstructs the intended normalized weights, keep the explicit router_weights for CPU, and fold any removed normalization mass / routing scale into an output multiply. Add dense-vs-QMoE numerical parity tests on both CPU and CUDA for GLM-5.2 (sigmoid) and DeepSeek-V2 (softmax) routing.

Filed by the Squad review team; happy to implement once you're ready — flagging first to avoid conflicting with your in-flight edits on this branch.

@justinchuby

Copy link
Copy Markdown
Member Author

Triage/update from Sapper:

  • Resolved the current main conflicts in an isolated worktree while preserving GLM-5.2 DSA/pkg.nxrt::IndexShare, MTP, GGUF, and fused com.microsoft::QMoE behavior.
  • Addressed the remaining safe review items: fixed both script docstring backticks and added focused --glm-full-attention CLI coverage.
  • Previously unresolved threads for config-suite coverage, asymmetric QMoE zero points, DeepSeek QMoE packing, and onnx-genai MTP artifacts are already implemented on the branch; no additional design-level comment was changed.
  • Validation: Ruff check + format clean; 378 affected tests passed (GLM/QMoE/MLA, GGUF, onnx-genai/ORT-GenAI metadata, ChatGLM, GQA rewrite, CLI).
  • lintrunner itself cannot run in this environment because its adapter writes response files under forbidden /tmp; direct project Ruff fallback is clean.

The shared PR branch was intentionally not force-updated because the conflict resolution is a large merge and another 404 worktree exists. Merge-ready replacement branch: sapper/404-rebase at fa30534.

Justin: please replace/update glm5.2-moe-export from sapper/404-rebase after review. I did not merge this PR.

Resolve conflicts with current main while preserving GLM-5.2 IndexShare DSA, MTP, GGUF, and fused QMoE support. Incorporate incoming DeepSeek V4, pipeline metadata, attention decomposition, and CI updates. Address remaining review items for GLM export script docstrings and --glm-full-attention CLI coverage.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Signed-off-by: justinchuby <justinchuby@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 29, 2026 17:30
@CLAassistant

CLAassistant commented Jul 29, 2026

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ justinchuby
❌ Copilot
You have signed the CLA already but the status is still pending? Let us recheck it.

@@ -0,0 +1,618 @@
# Copyright (c) Microsoft Corporation.

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 43 out of 43 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (4)

src/mobius/models/glm_moe_dsa.py:294

  • The PR description states the DSA path is exported using only standard ONNX ops (no custom op required for correctness), but this implementation emits a custom pkg.nxrt::IndexShare node. That makes the exported model dependent on a non-standard opset and contradicts the stated portability/correctness claim.

Please either (a) add a portable path that lowers DSA to standard ops (e.g., dense additive mask + Attention) behind a config/CLI flag, or (b) update the PR description/docs to explicitly call out the custom-op dependency for DSA builds.
docs/research/glm52-export.md:88

  • This section claims the exported DSA graph uses only standard ONNX ops and requires no custom op for correctness, but the implementation and tests in this PR emit a custom pkg.nxrt::IndexShare op (and assert Attention count is 0). The doc should be updated to match the current export contract.
Portable ONNX preserves DSA selection numerics by converting the selected
indices to an additive mask for the standard `Attention` operator. No custom op
is required for correctness. Practical million-token performance still needs
a selected-token sparse-attention kernel so the runtime avoids evaluating
masked K/V positions.

src/mobius/rewrite_rules/_group_query_attention_test.py:206

  • _build_tiny_mla_graph() is introduced here but is not referenced anywhere else in this test module. Keeping unused helpers in test code makes future refactors harder and can confuse readers about intended coverage.

Either wire this helper into a test case, or remove it if it’s leftover from earlier iterations.
src/mobius/rewrite_rules/_decompose_attention_test.py:172

  • The acceptable error tolerance for the softcap cases was loosened from 1e-5 to 5e-3, which is a large relaxation and risks masking real parity regressions in the attention decomposition.

Please add a short comment explaining why this larger atol is required (e.g., known numerical instability/EP differences), and consider tightening it as much as possible.

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>
Copilot AI review requested due to automatic review settings July 29, 2026 17:38
Comment thread src/mobius/components/_moe_test.py Outdated
sym=True,
),
)
with pytest.raises(ValueError, match="CUDA.*ignores router_weights"):

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 43 out of 43 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (2)

src/mobius/main.py:291

  • In the --model code path, --glm-full-attention always passes config_overrides into build(), so it will also disable DSA/MTP for any model type, not just glm_moe_dsa. This is surprising for a GLM-specific flag and can silently alter other architectures that use these fields. Consider loading the HF config when the flag is present, verifying model_type == 'glm_moe_dsa', and only then applying overrides.
            config_overrides=(
                {"use_dsa": False, "num_nextn_predict_layers": 0}
                if args.glm_full_attention
                else None
            ),

src/mobius/main.py:251

  • --glm-full-attention is applied unconditionally when building from --config, even if the resolved model_type is not glm_moe_dsa. Since use_dsa / num_nextn_predict_layers also affect other architectures (e.g. DeepSeek-V4 CSA/MTP), this flag can silently change unrelated exports. Please validate the model type and fail fast when the flag is used on non-GLM-5.2 configs.

This issue also appears on line 287 of the same file.

        if args.glm_full_attention:
            config = dataclasses.replace(
                config,
                use_dsa=False,
                num_nextn_predict_layers=0,

@justinchuby
justinchuby marked this pull request as ready for review July 30, 2026 04:57
@justinchuby
justinchuby requested a review from a team July 30, 2026 04:57
@justinchuby

Copy link
Copy Markdown
Member Author

@copilot merge from main

Copilot AI added 2 commits July 30, 2026 05:02
Signed-off-by: GitHub <noreply@github.com>

# Conflicts:
#	src/mobius/integrations/gguf/_tensor_mapping.py
#	src/mobius/integrations/onnx_genai/auto_export.py
Copilot AI review requested due to automatic review settings July 30, 2026 05:03

Copilot AI commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

@copilot merge from main

Merged origin/main (6 commits: WebGPU GQA tests, Phi-3.5 LongRoPE/CLIP, Qwen3.5-dense, explicit ONNX GenAI metadata, Gemma3 multimodal, ORT ≥1.28 softcap test) in 3da96d0. Resolved two file conflicts by keeping both sides (_GLM_DSA_MAPPING + updated _QWEN35_HYBRID_EXTRAS comment in _tensor_mapping.py; _write_mtp_config + _add_explicit_io_to_file in auto_export.py).

Also fixed the two CI linter failures brought in with the last commits (6f0cfe2):

  • RUF043 (_moe_test.py:275): changed match="CUDA.*ignores router_weights"match=r"CUDA.*ignores router_weights" (raw string).
  • RUFF-FORMAT (glm_moe_dsa.py): ran ruff format to apply the pending whitespace/line-wrap changes.

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 43 out of 43 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (4)

src/mobius/models/glm_moe_dsa.py:287

  • The PR description claims Portable ONNX correctness for DSA uses only standard ops and that no custom op is required, but this implementation hard-depends on the custom pkg.nxrt::IndexShare op for the DSA path. That means a generic ONNX Runtime / non-nxrt environment cannot run the exported graph as-is.

Either (a) add a portable standard-op fallback for DSA (e.g., dense Attention with a ScatterElements-built sparse mask) behind a config flag, or (b) update the PR description/docs to state that DSA execution requires the pkg.nxrt custom-op runtime.
src/mobius/models/glm_moe_dsa.py:576

  • GlmMoeDsaCausalLMModel.__init__ only computes/validates the IndexShare schedule when config.indexer_types is None. If a caller provides an explicit indexer_types list with the wrong length, the model will accept it and later code (task cache packing, per-layer indexing) can fail with an IndexError instead of the intended clear ValueError.

Consider validating whenever DSA is enabled (e.g., if config.use_dsa: config.indexer_types = _indexer_types(config)), since _indexer_types() already performs the length check for caller-supplied schedules.
src/mobius/models/glm_moe_dsa.py:26

  • This file imports apply_rotary_pos_emb from a private module path (mobius.components._rotary_embedding). Project convention is to import components/helpers from the public mobius.components API to avoid brittle private-module dependencies.

Consider re-exporting apply_rotary_pos_emb from mobius.components and importing it from there (or provide a small local wrapper) so the new model doesn’t depend on a private module path.
src/mobius/rewrite_rules/_decompose_attention_test.py:172

  • The parity tolerance for the softcap cases was loosened from 1e-5 to 5e-3, which is a large change and can mask regressions in the softcap path. If this is required due to a known ORT/kernel numeric change, please add a short comment explaining why this tolerance is expected/acceptable (and ideally link to an issue or ORT version note).

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.

5 participants