Skip to content

Commit b6d4db0

Browse files
justinchubyCopilot
andcommitted
Register qwen3_5_moe_text for text-only Qwen3.6 export
Qwen/Qwen3.6-35B-A3B ships model_type=qwen3_5_moe with a vision_config, so the builder overrides it to qwen3_5_moe_vl and unwraps text_config (whose own model_type is qwen3_5_moe_text). Unlike every other VL family (gemma3_text, qwen3_vl_text, qwen3_5_vl_text), the MoE VL had no registered text-only sibling, so: - registry.get("qwen3_5_moe_text") raised KeyError, and - build(..., text_only=True) on the VL checkpoint failed (no _TEXT_ONLY_MODEL_TYPE entry for qwen3_5_moe_vl). Qwen35MoECausalLMModel already strips the language_model. prefix, drops visual./MTP keys, and unpacks fused experts, so it consumes the VL text weights directly. Register qwen3_5_moe_text -> Qwen35MoECausalLMModel, wire the text-only override (qwen3_5_moe_vl -> qwen3_5_moe_text, idempotent self-map), and add default-id/family map entries. Add an L1 build-graph config (reusing the qwen3_5_moe hybrid MoE tiny config) so registry-completeness passes and the new key builds. Verified against the real Qwen3.6 config that both text_only routing and text_config.model_type now resolve to Qwen35MoECausalLMModel. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: justinchuby <justinchuby@users.noreply.github.com>
1 parent 8a565d7 commit b6d4db0

3 files changed

Lines changed: 39 additions & 0 deletions

File tree

src/mobius/_registry.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -604,6 +604,13 @@ def _detect_fallback_registration(hf_config) -> ModelRegistration | None:
604604
"qwen2_vl_text": ModelRegistration(Qwen25VLTextModel),
605605
"qwen3_5": ModelRegistration(Qwen35VL3ModelCausalLMModel, task="hybrid-qwen-vl"),
606606
"qwen3_5_moe_vl": ModelRegistration(Qwen35MoEVL3ModelCausalLMModel, task="hybrid-qwen-vl"),
607+
# Text-only sibling of ``qwen3_5_moe_vl`` (Qwen3.6-35B-A3B). The MoE
608+
# backbone ``Qwen35MoECausalLMModel`` already strips ``language_model.``
609+
# and drops ``visual.``/MTP keys, so it consumes the VL checkpoint's text
610+
# weights directly; ``build(..., text_only=True)`` routes here via
611+
# ``_TEXT_ONLY_MODEL_TYPE``. It also matches the VL ``text_config``'s own
612+
# ``model_type=qwen3_5_moe_text`` so that config resolves cleanly.
613+
"qwen3_5_moe_text": ModelRegistration(Qwen35MoECausalLMModel),
607614
"qwen3_5_vl": ModelRegistration(Qwen35VL3ModelCausalLMModel, task="hybrid-qwen-vl"),
608615
"qwen3_5_vl_text": ModelRegistration(Qwen35VLTextModel),
609616
"qwen3_vl": ModelRegistration(Qwen3VL3ModelCausalLMModel, task="qwen-vl"),
@@ -791,6 +798,12 @@ def _create_default_registry() -> ModelRegistry:
791798
_TEXT_ONLY_MODEL_TYPE: dict[str, str] = {
792799
"gemma4_unified": "gemma4_unified_text",
793800
"gemma4_unified_text": "gemma4_unified_text",
801+
# Qwen3.5-MoE-VL (Qwen3.6-35B-A3B): export just the hybrid MoE text
802+
# backbone as a standalone decoder-only LLM. The builder overrides
803+
# ``qwen3_5_moe`` -> ``qwen3_5_moe_vl`` when a ``vision_config`` is present,
804+
# so the text-only override keys off the VL type here.
805+
"qwen3_5_moe_vl": "qwen3_5_moe_text",
806+
"qwen3_5_moe_text": "qwen3_5_moe_text",
794807
}
795808

796809

@@ -894,6 +907,7 @@ def _create_default_registry() -> ModelRegistry:
894907
"qwen2_moe": "Qwen/Qwen1.5-MoE-A2.7B-Chat",
895908
"qwen3_moe": "Qwen/Qwen3-30B-A3B",
896909
"qwen3_5_moe": "Qwen/Qwen3.5-MoE-A3B-128K",
910+
"qwen3_5_moe_text": "Qwen/Qwen3.6-35B-A3B",
897911
"qwen3_next": "Qwen/Qwen3-235B-A22B",
898912
"granitemoe": "ibm-granite/granite-3.0-1b-a400m-instruct",
899913
"olmoe": "allenai/OLMoE-1B-7B-0924",
@@ -1141,6 +1155,7 @@ def _create_default_registry() -> ModelRegistry:
11411155
"qwen3_moe": "qwen",
11421156
"qwen3_5_text": "qwen",
11431157
"qwen3_5_moe": "qwen",
1158+
"qwen3_5_moe_text": "qwen",
11441159
"qwen3_next": "qwen",
11451160
"qwen2_vl": "qwen",
11461161
"qwen2_vl_text": "qwen",

tests/_test_configs.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,29 @@ def _base_config(config_cls=None, **overrides) -> ArchitectureConfig:
519519
},
520520
True,
521521
),
522+
# Text-only sibling of the Qwen3.5-MoE-VL (Qwen3.6-35B-A3B) checkpoint,
523+
# exported via ``build(..., text_only=True)``. Same hybrid MoE backbone as
524+
# ``qwen3_5_moe`` above; registered separately so the VL ``text_config``'s
525+
# ``model_type=qwen3_5_moe_text`` and the text-only override both resolve.
526+
(
527+
"qwen3_5_moe_text",
528+
{
529+
"hidden_act": "silu",
530+
"layer_types": ["linear_attention", "full_attention"],
531+
"partial_rotary_factor": 0.25,
532+
"mrope_interleaved": True,
533+
"num_local_experts": 4,
534+
"num_experts_per_tok": 2,
535+
"moe_intermediate_size": 32,
536+
"shared_expert_intermediate_size": 32,
537+
"linear_num_value_heads": 4,
538+
"linear_num_key_heads": 2,
539+
"linear_key_head_dim": 16,
540+
"linear_value_head_dim": 16,
541+
"linear_conv_kernel_dim": 4,
542+
},
543+
True,
544+
),
522545
# === Falcon and Bloom ===
523546
# dual_ln=True: Falcon with new_decoder_architecture uses separate ln_attn + ln_mlp.
524547
# hidden_act="gelu": real Falcon uses GELU (HF FalconConfig.activation default);

tests/build_graph_test.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
"minimax",
8181
"qwen3_5_text",
8282
"qwen3_5_moe",
83+
"qwen3_5_moe_text",
8384
"qwen3_next",
8485
# Models using LinearAttention / CausalConvWithState custom ops
8586
# prevent full shape/type propagation through com.microsoft domain.

0 commit comments

Comments
 (0)