From 392841f47cb7ef214601eeb528906a0abba02471 Mon Sep 17 00:00:00 2001 From: Yash Akhauri Date: Thu, 3 Sep 2026 08:53:16 -0700 Subject: [PATCH] [Bugfix] Support K2 Horizon MoE without MoVA (#37825) Co-authored-by: BBuf <1182563586@qq.com> --- python/sglang/srt/models/xllm.py | 38 +++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/python/sglang/srt/models/xllm.py b/python/sglang/srt/models/xllm.py index 4fa46971a..10cc99332 100644 --- a/python/sglang/srt/models/xllm.py +++ b/python/sglang/srt/models/xllm.py @@ -188,6 +188,17 @@ def _normalize_k2_horizon_config(config: PretrainedConfig) -> None: f"got {mova_num_experts!r}." ) is_mova = mova_num_experts > 0 + num_experts = getattr(config, "num_experts", 0) + if ( + isinstance(num_experts, bool) + or not isinstance(num_experts, int) + or num_experts < 0 + ): + raise ValueError( + "K2Horizon num_experts must be a non-negative integer, " + f"got {num_experts!r}." + ) + has_moe_ffn = num_experts > 0 if is_mova: if _get_xllm_source_router_gemm_partitions(config) is None: @@ -247,14 +258,21 @@ def _normalize_k2_horizon_config(config: PretrainedConfig) -> None: target_name="num_values_per_tok", value=0, ) - for field in ("num_experts", "num_experts_per_tok", "num_shared_experts"): - value = getattr(config, field, 0) - if isinstance(value, bool) or not isinstance(value, int) or value != 0: - raise ValueError(f"Dense K2Horizon requires {field}=0, got {value!r}") - # Some dense exports omit the MoE-only fields. Downstream model - # construction reads them directly, so materialize the validated - # dense defaults instead of relying on getattr fallbacks forever. - setattr(config, field, 0) + if not has_moe_ffn: + for field in ( + "num_experts", + "num_experts_per_tok", + "num_shared_experts", + ): + value = getattr(config, field, 0) + if isinstance(value, bool) or not isinstance(value, int) or value != 0: + raise ValueError( + f"Dense K2Horizon requires {field}=0, got {value!r}" + ) + # Some dense exports omit the MoE-only fields. Downstream model + # construction reads them directly, so materialize the validated + # dense defaults instead of relying on getattr fallbacks forever. + setattr(config, field, 0) if getattr(config, "query_key_norm", False): raise ValueError( "Dense K2Horizon native loading does not support query/key " @@ -489,7 +507,7 @@ def _normalize_k2_horizon_config(config: PretrainedConfig) -> None: "K2Horizon MoVA requires mlp_only_layers to be a contiguous " f"prefix starting at zero, got {list(mlp_only_layers)}" ) - if not is_mova and list(mlp_only_layers) != list( + if not has_moe_ffn and list(mlp_only_layers) != list( range(config.num_hidden_layers) ): raise ValueError( @@ -501,7 +519,7 @@ def _normalize_k2_horizon_config(config: PretrainedConfig) -> None: target_name="num_dense_layers", value=len(mlp_only_layers), ) - elif not is_mova: + elif not has_moe_ffn: raise ValueError( "Dense K2Horizon native loading requires explicit mlp_only_layers" )