From 414dbab0a076a62aff76906029e008cfafad86dd Mon Sep 17 00:00:00 2001 From: tuan nguyen Date: Tue, 13 Oct 2026 14:26:24 +0000 Subject: [PATCH 1/6] fix(quark): load FP8 shared experts into MXFP4 fused MoE weights Quark MiniMax checkpoints store routed experts as packed MXFP4 and shared experts as block FP8. Copy is_fp4_experts onto Quark configs and requantize those shared experts into Quark's uint8 scale buffers. Co-authored-by: Cursor --- .../srt/layers/moe/fused_moe_triton/layer.py | 36 +++++++++++++++---- python/sglang/srt/model_loader/loader.py | 7 ++++ 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/python/sglang/srt/layers/moe/fused_moe_triton/layer.py b/python/sglang/srt/layers/moe/fused_moe_triton/layer.py index bc4a3799f3ad..a27c2c805a82 100644 --- a/python/sglang/srt/layers/moe/fused_moe_triton/layer.py +++ b/python/sglang/srt/layers/moe/fused_moe_triton/layer.py @@ -1058,6 +1058,9 @@ def _maybe_load_fp8_shared_expert_as_fp4( ): return False + # Quark MXFP4 checkpoints store block-FP8 scales as raw UE8M0 bytes. + if "weight_scale_inv" in weight_name and loaded_weight.dtype == torch.uint8: + loaded_weight = loaded_weight.view(torch.float8_e8m0fnu) is_weight = ( "weight" in weight_name and "scale" not in weight_name @@ -1071,9 +1074,17 @@ def _maybe_load_fp8_shared_expert_as_fp4( return False weight_param = self.w2_weight if shard_id == "w2" else self.w13_weight - scale_param = ( - self.w2_weight_scale_inv if shard_id == "w2" else self.w13_weight_scale_inv - ) + # ModelOpt FP4 layers register *_weight_scale_inv. Quark MXFP4 registers + # *_weight_scale. getattr returns None when the ModelOpt name is absent; + # a direct attribute read raises AttributeError on nn.Module. + if shard_id == "w2": + scale_param = getattr(self, "w2_weight_scale_inv", None) + if scale_param is None: + scale_param = self.w2_weight_scale + else: + scale_param = getattr(self, "w13_weight_scale_inv", None) + if scale_param is None: + scale_param = self.w13_weight_scale if param is not weight_param and param is not scale_param: return False @@ -1100,13 +1111,24 @@ def _maybe_load_fp8_shared_expert_as_fp4( weight_block_size = getattr(self.quant_config, "weight_block_size", None) if weight_block_size is None: - raise ValueError( - "Loading FP8 shared expert weights into FP4 fused MoE weights " - "requires block-FP8 weight_block_size." - ) + # Quark MXFP4 has no weight_block_size. Shared-expert FP8 scales are + # one UE8M0 value per 32 K elements, so each block is [1, 32]. + if getattr(self.quant_config, "get_name", lambda: "")() == "quark": + weight_block_size = [1, 32] + else: + raise ValueError( + "Loading FP8 shared expert weights into FP4 fused MoE weights " + "requires block-FP8 weight_block_size." + ) fp4_weight, fp4_scale = quantize_block_fp8_weight_to_mxfp4( fp8_weight, fp8_scale, weight_block_size ) + # Quark stores packed MXFP4 weights and UE8M0 scales as uint8. view() + # keeps the bits; a dtype cast would reinterpret them. + if weight_param.dtype == torch.uint8 and fp4_weight.dtype != torch.uint8: + fp4_weight = fp4_weight.view(torch.uint8) + if scale_param.dtype == torch.uint8 and fp4_scale.dtype != torch.uint8: + fp4_scale = fp4_scale.contiguous().view(torch.uint8) weight_data = weight_param.data[expert_id] scale_data = scale_param.data[expert_id] diff --git a/python/sglang/srt/model_loader/loader.py b/python/sglang/srt/model_loader/loader.py index edda2a761822..25f8630e6d79 100644 --- a/python/sglang/srt/model_loader/loader.py +++ b/python/sglang/srt/model_loader/loader.py @@ -250,6 +250,13 @@ def _get_quantization_config( ) elif quant_config.get_name() == "humming": quant_config.is_fp4_experts = model_config.is_fp4_experts + elif quant_config.get_name() == "quark": + # Mixed Quark checkpoints (routed MXFP4, shared experts block FP8) + # set model_config.is_fp4_experts from routed_experts_quant_method. + # Fp8 and Humming already copy that flag; Quark must too, or the + # fused MoE loader copies the FP8 shared expert into the packed + # MXFP4 buffer. + quant_config.is_fp4_experts = model_config.is_fp4_experts if not _is_npu: major, minor = get_device_capability() From 64ce951fea46f54ea92ea7be07cd257a147a5e17 Mon Sep 17 00:00:00 2001 From: tuan nguyen Date: Tue, 13 Oct 2026 14:50:17 +0000 Subject: [PATCH 2/6] fix(quark): keep the FP8 shared-expert path off other quant methods Only Quark checkpoints marked as MXFP4 routed experts take the new scale name, uint8 layout, and group-32 default. FP8 and ModelOpt still use w2_weight_scale_inv. Co-authored-by: Cursor --- .../srt/layers/moe/fused_moe_triton/layer.py | 40 ++++++++++--------- python/sglang/srt/model_loader/loader.py | 11 ++--- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/python/sglang/srt/layers/moe/fused_moe_triton/layer.py b/python/sglang/srt/layers/moe/fused_moe_triton/layer.py index a27c2c805a82..23319b898821 100644 --- a/python/sglang/srt/layers/moe/fused_moe_triton/layer.py +++ b/python/sglang/srt/layers/moe/fused_moe_triton/layer.py @@ -1058,8 +1058,14 @@ def _maybe_load_fp8_shared_expert_as_fp4( ): return False - # Quark MXFP4 checkpoints store block-FP8 scales as raw UE8M0 bytes. - if "weight_scale_inv" in weight_name and loaded_weight.dtype == torch.uint8: + # Quark stores these scales as uint8. Other methods already pass + # float8 or float32, so leave those tensors alone. + is_quark = self.quant_config.get_name() == "quark" + if ( + is_quark + and "weight_scale_inv" in weight_name + and loaded_weight.dtype == torch.uint8 + ): loaded_weight = loaded_weight.view(torch.float8_e8m0fnu) is_weight = ( "weight" in weight_name @@ -1074,17 +1080,17 @@ def _maybe_load_fp8_shared_expert_as_fp4( return False weight_param = self.w2_weight if shard_id == "w2" else self.w13_weight - # ModelOpt FP4 layers register *_weight_scale_inv. Quark MXFP4 registers - # *_weight_scale. getattr returns None when the ModelOpt name is absent; - # a direct attribute read raises AttributeError on nn.Module. + # Quark calls this w2_weight_scale. The *_weight_scale_inv name still + # wins when the layer has it, which is the FP8 and ModelOpt path. if shard_id == "w2": - scale_param = getattr(self, "w2_weight_scale_inv", None) - if scale_param is None: - scale_param = self.w2_weight_scale + scale_name = "w2_weight_scale_inv" + quark_scale_name = "w2_weight_scale" else: - scale_param = getattr(self, "w13_weight_scale_inv", None) - if scale_param is None: - scale_param = self.w13_weight_scale + scale_name = "w13_weight_scale_inv" + quark_scale_name = "w13_weight_scale" + scale_param = getattr(self, scale_name, None) + if scale_param is None: + scale_param = getattr(self, quark_scale_name if is_quark else scale_name) if param is not weight_param and param is not scale_param: return False @@ -1111,9 +1117,8 @@ def _maybe_load_fp8_shared_expert_as_fp4( weight_block_size = getattr(self.quant_config, "weight_block_size", None) if weight_block_size is None: - # Quark MXFP4 has no weight_block_size. Shared-expert FP8 scales are - # one UE8M0 value per 32 K elements, so each block is [1, 32]. - if getattr(self.quant_config, "get_name", lambda: "")() == "quark": + # MXFP4 uses groups of 32. Quark does not store that size. + if is_quark: weight_block_size = [1, 32] else: raise ValueError( @@ -1123,11 +1128,10 @@ def _maybe_load_fp8_shared_expert_as_fp4( fp4_weight, fp4_scale = quantize_block_fp8_weight_to_mxfp4( fp8_weight, fp8_scale, weight_block_size ) - # Quark stores packed MXFP4 weights and UE8M0 scales as uint8. view() - # keeps the bits; a dtype cast would reinterpret them. - if weight_param.dtype == torch.uint8 and fp4_weight.dtype != torch.uint8: + # Quark buffers are uint8. Keep the bits instead of casting them. + if is_quark and weight_param.dtype == torch.uint8: fp4_weight = fp4_weight.view(torch.uint8) - if scale_param.dtype == torch.uint8 and fp4_scale.dtype != torch.uint8: + if is_quark and scale_param.dtype == torch.uint8: fp4_scale = fp4_scale.contiguous().view(torch.uint8) weight_data = weight_param.data[expert_id] diff --git a/python/sglang/srt/model_loader/loader.py b/python/sglang/srt/model_loader/loader.py index 25f8630e6d79..99f4eba46ff5 100644 --- a/python/sglang/srt/model_loader/loader.py +++ b/python/sglang/srt/model_loader/loader.py @@ -250,13 +250,10 @@ def _get_quantization_config( ) elif quant_config.get_name() == "humming": quant_config.is_fp4_experts = model_config.is_fp4_experts - elif quant_config.get_name() == "quark": - # Mixed Quark checkpoints (routed MXFP4, shared experts block FP8) - # set model_config.is_fp4_experts from routed_experts_quant_method. - # Fp8 and Humming already copy that flag; Quark must too, or the - # fused MoE loader copies the FP8 shared expert into the packed - # MXFP4 buffer. - quant_config.is_fp4_experts = model_config.is_fp4_experts + elif quant_config.get_name() == "quark" and model_config.is_fp4_experts: + # FP8 and Humming already do this. Skip Quark checkpoints that + # are not marked as MXFP4 routed experts. + quant_config.is_fp4_experts = True if not _is_npu: major, minor = get_device_capability() From 37fd5ce37e380d1d3b9fcceecf40ee262616ec42 Mon Sep 17 00:00:00 2001 From: tuan nguyen Date: Thu, 15 Oct 2026 10:53:39 +0000 Subject: [PATCH 3/6] fix(quark): decode E8M0 block-FP8 weight scales for Quark MXFP4 Quark's block-FP8 shared-expert weights (scale_type float8_e8m0fnu) store the raw exponent byte as the weight scale. The block kernel expects a float scale, so leaving the byte undecoded corrupts every block-FP8 matmul in the shared experts and the model degenerates into repeating garbage tokens. Thread weight_scale_is_ue8m0 through Fp8Config from quark.py's _get_block_fp8_config, set it whenever the checkpoint's weight spec has scale_type == "float8_e8m0fnu", and decode weight_scale_inv = 2^(stored_byte - 127) once in Fp8LinearMethod.process_weights_after_loading_block_quant. Verified against amd/MiniMax-M3.1-preview2-dspark-private-Quark-MXFP4, disable-shared-experts-fusion: GSM8K (1314 questions, 5-shot) goes from 0.000 to 0.915 with --attention-backend triton and to 0.909 with --attention-backend aiter. Without this decode every GSM8K answer runs to the 4096-token cap on repeated text. Co-authored-by: Cursor --- python/sglang/srt/layers/quantization/fp8.py | 9 +++++++++ python/sglang/srt/layers/quantization/quark/quark.py | 3 +++ 2 files changed, 12 insertions(+) diff --git a/python/sglang/srt/layers/quantization/fp8.py b/python/sglang/srt/layers/quantization/fp8.py index d779091923d1..7f60a9f459f9 100644 --- a/python/sglang/srt/layers/quantization/fp8.py +++ b/python/sglang/srt/layers/quantization/fp8.py @@ -272,6 +272,7 @@ def __init__( is_fp4_experts: bool = False, kv_cache_quant_algo: Optional[str] = None, scale_fmt: Optional[str] = None, + weight_scale_is_ue8m0: bool = False, ) -> None: super().__init__() # DSV4 mxfp4-packed (True) vs converted FP8 (False); injected by @@ -299,6 +300,9 @@ def __init__( self.kv_cache_quant_algo = kv_cache_quant_algo # "ue8m0" checkpoints quantize activations with power-of-two scales. self.scale_fmt = scale_fmt + # Quark's block-FP8 weight scale is the raw E8M0 exponent byte, not the + # float32 value the block kernel expects; decode it after loading. + self.weight_scale_is_ue8m0 = weight_scale_is_ue8m0 if weight_block_size is not None: if not is_checkpoint_fp8_serialized: raise ValueError( @@ -741,6 +745,11 @@ def create_weights( ) def process_weights_after_loading_block_quant(self, layer: Module) -> None: + if getattr(self.quant_config, "weight_scale_is_ue8m0", False): + # Quark stores the raw E8M0 exponent byte. The block kernel wants + # the decoded power-of-two scale, 2^(stored_byte - 127). + scale = layer.weight_scale_inv.data + layer.weight_scale_inv.data = torch.exp2(scale - 127) use_deepgemm_runner = ( getattr(self.w8a8_block_fp8_linear, "func", self.w8a8_block_fp8_linear) is deepgemm_w8a8_block_fp8_linear_with_fallback diff --git a/python/sglang/srt/layers/quantization/quark/quark.py b/python/sglang/srt/layers/quantization/quark/quark.py index ce797984d475..38cf2ab2f5eb 100644 --- a/python/sglang/srt/layers/quantization/quark/quark.py +++ b/python/sglang/srt/layers/quantization/quark/quark.py @@ -454,6 +454,9 @@ def _get_block_fp8_config( activation_scheme="dynamic", weight_block_size=block_size, packed_modules_mapping=packed_modules_mapping, + # Quark's block-FP8 weight scale is the raw E8M0 exponent byte + # when scale_type is float8_e8m0fnu, not a plain float32 value. + weight_scale_is_ue8m0=weight_config.get("scale_type") == "float8_e8m0fnu", ) def is_linear_unquantized(self, prefix: str) -> bool: From de70e2b96c4e1f9863493baaf11fd19059394cc2 Mon Sep 17 00:00:00 2001 From: tuan nguyen Date: Mon, 26 Oct 2026 11:45:17 +0000 Subject: [PATCH 4/6] fix(quark): requantize MiniMax-M3.1 Flash shared experts before fusion The Flash Quark checkpoint marks routed experts as FP4 only in the global weight spec and stores shared-expert scales as weight_scale. Without that detection, omitting --disable-shared-experts-fusion copies the FP8 down projection into the packed MXFP4 buffer and the server crashes. Co-authored-by: Cursor --- python/sglang/srt/configs/model_config.py | 20 ++++++++++- .../srt/layers/moe/fused_moe_triton/layer.py | 33 ++++++++++--------- 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/python/sglang/srt/configs/model_config.py b/python/sglang/srt/configs/model_config.py index aca43047668b..3a92a6583a60 100644 --- a/python/sglang/srt/configs/model_config.py +++ b/python/sglang/srt/configs/model_config.py @@ -719,7 +719,7 @@ def __init__( # Config draft model self._config_draft_model() - # Mixed FP8/MXFP4 ckpts mark mxfp4 routed experts via this key. + # Mixed FP8/MXFP4 ckpts usually mark mxfp4 routed experts via this key. quantization_config = ( _quant_config_to_dict(getattr(self.hf_config, "quantization_config", None)) or {} @@ -732,6 +732,24 @@ def __init__( if self.is_fp4_experts: logger.info("Detected mixed checkpoint layout: routed experts are MXFP4.") + # MiniMax-M3.1-Flash Quark MXFP4 uses the global FP4 specification for + # routed experts and per-layer block-FP8 specifications for shared + # experts, but does not set routed_experts_quant_method. Mark this as + # mixed so FusedMoE requantizes an FP8 shared expert before fusing it + # into the packed MXFP4 routed-expert buffer. + global_weight_config = ( + (quantization_config.get("global_quant_config") or {}).get("weight") or {} + ) + if ( + not self.is_fp4_experts + and getattr(self.hf_config, "model_type", None) == "minimax_m3_vl" + and str(global_weight_config.get("dtype", "")).lower() == "fp4" + ): + self.is_fp4_experts = True + logger.info( + "Detected MiniMax-M3 Quark mixed layout: routed experts are MXFP4." + ) + # MiMo-V2 mxfp4 ckpts declare the routed-expert layout via store_dtype. if ( not self.is_fp4_experts diff --git a/python/sglang/srt/layers/moe/fused_moe_triton/layer.py b/python/sglang/srt/layers/moe/fused_moe_triton/layer.py index 23319b898821..fbea51437ef7 100644 --- a/python/sglang/srt/layers/moe/fused_moe_triton/layer.py +++ b/python/sglang/srt/layers/moe/fused_moe_triton/layer.py @@ -1063,7 +1063,7 @@ def _maybe_load_fp8_shared_expert_as_fp4( is_quark = self.quant_config.get_name() == "quark" if ( is_quark - and "weight_scale_inv" in weight_name + and "weight_scale" in weight_name and loaded_weight.dtype == torch.uint8 ): loaded_weight = loaded_weight.view(torch.float8_e8m0fnu) @@ -1072,25 +1072,26 @@ def _maybe_load_fp8_shared_expert_as_fp4( and "scale" not in weight_name and loaded_weight.dtype == torch.float8_e4m3fn ) - is_scale = "weight_scale_inv" in weight_name and loaded_weight.dtype in ( - torch.float8_e8m0fnu, - torch.float32, + # Quark names the shared-expert scale weight_scale. Other FP8 formats + # use weight_scale_inv. + is_scale = ( + ( + "weight_scale_inv" in weight_name + or (is_quark and "weight_scale" in weight_name) + ) + and loaded_weight.dtype in (torch.float8_e8m0fnu, torch.float32) ) if not is_weight and not is_scale: return False - weight_param = self.w2_weight if shard_id == "w2" else self.w13_weight - # Quark calls this w2_weight_scale. The *_weight_scale_inv name still - # wins when the layer has it, which is the FP8 and ModelOpt path. - if shard_id == "w2": - scale_name = "w2_weight_scale_inv" - quark_scale_name = "w2_weight_scale" - else: - scale_name = "w13_weight_scale_inv" - quark_scale_name = "w13_weight_scale" - scale_param = getattr(self, scale_name, None) - if scale_param is None: - scale_param = getattr(self, quark_scale_name if is_quark else scale_name) + # FP8 and ModelOpt use *_weight_scale_inv. Quark uses *_weight_scale + # only when the inv name is absent. + prefix = "w2" if shard_id == "w2" else "w13" + weight_param = getattr(self, f"{prefix}_weight") + scale_name = f"{prefix}_weight_scale_inv" + if is_quark and not hasattr(self, scale_name): + scale_name = f"{prefix}_weight_scale" + scale_param = getattr(self, scale_name) if param is not weight_param and param is not scale_param: return False From 2c989fe49b0e42f7cd4717556fcc55753dc8b09e Mon Sep 17 00:00:00 2001 From: tuan nguyen Date: Mon, 26 Oct 2026 12:08:35 +0000 Subject: [PATCH 5/6] fix(quark): shorten the shared-expert fusion comments --- python/sglang/srt/configs/model_config.py | 13 +++++------ .../srt/layers/moe/fused_moe_triton/layer.py | 22 +++++++------------ python/sglang/srt/layers/quantization/fp8.py | 6 ++--- .../srt/layers/quantization/quark/quark.py | 3 +-- python/sglang/srt/model_loader/loader.py | 3 +-- 5 files changed, 17 insertions(+), 30 deletions(-) diff --git a/python/sglang/srt/configs/model_config.py b/python/sglang/srt/configs/model_config.py index 3a92a6583a60..4ce6c04549a0 100644 --- a/python/sglang/srt/configs/model_config.py +++ b/python/sglang/srt/configs/model_config.py @@ -719,7 +719,7 @@ def __init__( # Config draft model self._config_draft_model() - # Mixed FP8/MXFP4 ckpts usually mark mxfp4 routed experts via this key. + # Mixed FP8/MXFP4 ckpts mark mxfp4 routed experts via this key. quantization_config = ( _quant_config_to_dict(getattr(self.hf_config, "quantization_config", None)) or {} @@ -732,14 +732,11 @@ def __init__( if self.is_fp4_experts: logger.info("Detected mixed checkpoint layout: routed experts are MXFP4.") - # MiniMax-M3.1-Flash Quark MXFP4 uses the global FP4 specification for - # routed experts and per-layer block-FP8 specifications for shared - # experts, but does not set routed_experts_quant_method. Mark this as - # mixed so FusedMoE requantizes an FP8 shared expert before fusing it - # into the packed MXFP4 routed-expert buffer. + # MiniMax-M3.1 Quark marks routed experts as FP4 here, not with + # routed_experts_quant_method. global_weight_config = ( - (quantization_config.get("global_quant_config") or {}).get("weight") or {} - ) + quantization_config.get("global_quant_config") or {} + ).get("weight") or {} if ( not self.is_fp4_experts and getattr(self.hf_config, "model_type", None) == "minimax_m3_vl" diff --git a/python/sglang/srt/layers/moe/fused_moe_triton/layer.py b/python/sglang/srt/layers/moe/fused_moe_triton/layer.py index fbea51437ef7..57f6757c8131 100644 --- a/python/sglang/srt/layers/moe/fused_moe_triton/layer.py +++ b/python/sglang/srt/layers/moe/fused_moe_triton/layer.py @@ -1058,8 +1058,7 @@ def _maybe_load_fp8_shared_expert_as_fp4( ): return False - # Quark stores these scales as uint8. Other methods already pass - # float8 or float32, so leave those tensors alone. + # Quark scales are raw uint8 E8M0 bytes. is_quark = self.quant_config.get_name() == "quark" if ( is_quark @@ -1072,20 +1071,15 @@ def _maybe_load_fp8_shared_expert_as_fp4( and "scale" not in weight_name and loaded_weight.dtype == torch.float8_e4m3fn ) - # Quark names the shared-expert scale weight_scale. Other FP8 formats - # use weight_scale_inv. + # Quark names this weight_scale. Other formats use weight_scale_inv. is_scale = ( - ( - "weight_scale_inv" in weight_name - or (is_quark and "weight_scale" in weight_name) - ) - and loaded_weight.dtype in (torch.float8_e8m0fnu, torch.float32) - ) + "weight_scale_inv" in weight_name + or (is_quark and "weight_scale" in weight_name) + ) and loaded_weight.dtype in (torch.float8_e8m0fnu, torch.float32) if not is_weight and not is_scale: return False - # FP8 and ModelOpt use *_weight_scale_inv. Quark uses *_weight_scale - # only when the inv name is absent. + # Fall back to Quark's weight_scale name when weight_scale_inv is absent. prefix = "w2" if shard_id == "w2" else "w13" weight_param = getattr(self, f"{prefix}_weight") scale_name = f"{prefix}_weight_scale_inv" @@ -1118,7 +1112,7 @@ def _maybe_load_fp8_shared_expert_as_fp4( weight_block_size = getattr(self.quant_config, "weight_block_size", None) if weight_block_size is None: - # MXFP4 uses groups of 32. Quark does not store that size. + # Quark omits this. MXFP4 groups are 32. if is_quark: weight_block_size = [1, 32] else: @@ -1129,7 +1123,7 @@ def _maybe_load_fp8_shared_expert_as_fp4( fp4_weight, fp4_scale = quantize_block_fp8_weight_to_mxfp4( fp8_weight, fp8_scale, weight_block_size ) - # Quark buffers are uint8. Keep the bits instead of casting them. + # Packed Quark buffers are uint8. if is_quark and weight_param.dtype == torch.uint8: fp4_weight = fp4_weight.view(torch.uint8) if is_quark and scale_param.dtype == torch.uint8: diff --git a/python/sglang/srt/layers/quantization/fp8.py b/python/sglang/srt/layers/quantization/fp8.py index 7f60a9f459f9..01cf7a0a62aa 100644 --- a/python/sglang/srt/layers/quantization/fp8.py +++ b/python/sglang/srt/layers/quantization/fp8.py @@ -300,8 +300,7 @@ def __init__( self.kv_cache_quant_algo = kv_cache_quant_algo # "ue8m0" checkpoints quantize activations with power-of-two scales. self.scale_fmt = scale_fmt - # Quark's block-FP8 weight scale is the raw E8M0 exponent byte, not the - # float32 value the block kernel expects; decode it after loading. + # When set, the block-FP8 scale is a raw E8M0 byte. self.weight_scale_is_ue8m0 = weight_scale_is_ue8m0 if weight_block_size is not None: if not is_checkpoint_fp8_serialized: @@ -746,8 +745,7 @@ def create_weights( def process_weights_after_loading_block_quant(self, layer: Module) -> None: if getattr(self.quant_config, "weight_scale_is_ue8m0", False): - # Quark stores the raw E8M0 exponent byte. The block kernel wants - # the decoded power-of-two scale, 2^(stored_byte - 127). + # Raw E8M0 byte -> 2^(byte - 127). scale = layer.weight_scale_inv.data layer.weight_scale_inv.data = torch.exp2(scale - 127) use_deepgemm_runner = ( diff --git a/python/sglang/srt/layers/quantization/quark/quark.py b/python/sglang/srt/layers/quantization/quark/quark.py index 38cf2ab2f5eb..31b218622368 100644 --- a/python/sglang/srt/layers/quantization/quark/quark.py +++ b/python/sglang/srt/layers/quantization/quark/quark.py @@ -454,8 +454,7 @@ def _get_block_fp8_config( activation_scheme="dynamic", weight_block_size=block_size, packed_modules_mapping=packed_modules_mapping, - # Quark's block-FP8 weight scale is the raw E8M0 exponent byte - # when scale_type is float8_e8m0fnu, not a plain float32 value. + # True when the scale is a raw E8M0 byte. weight_scale_is_ue8m0=weight_config.get("scale_type") == "float8_e8m0fnu", ) diff --git a/python/sglang/srt/model_loader/loader.py b/python/sglang/srt/model_loader/loader.py index 99f4eba46ff5..048ff85e3410 100644 --- a/python/sglang/srt/model_loader/loader.py +++ b/python/sglang/srt/model_loader/loader.py @@ -251,8 +251,7 @@ def _get_quantization_config( elif quant_config.get_name() == "humming": quant_config.is_fp4_experts = model_config.is_fp4_experts elif quant_config.get_name() == "quark" and model_config.is_fp4_experts: - # FP8 and Humming already do this. Skip Quark checkpoints that - # are not marked as MXFP4 routed experts. + # So the fused MoE loader can requantize the FP8 shared expert. quant_config.is_fp4_experts = True if not _is_npu: major, minor = get_device_capability() From 4a2dff95f1e6fc09618fea0062e3bae2b99db79c Mon Sep 17 00:00:00 2001 From: tuan nguyen Date: Tue, 27 Oct 2026 10:48:40 +0000 Subject: [PATCH 6/6] fix(quark): detect MiniMax MXFP4 routed experts on QuarkConfig Keep the MiniMax global-FP4 check with QuarkConfig so ModelConfig stays free of Quark-specific layout detection. Co-authored-by: Cursor --- python/sglang/srt/configs/model_config.py | 15 --------------- .../srt/layers/quantization/quark/quark.py | 16 ++++++++++++++++ python/sglang/srt/model_loader/loader.py | 1 - 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/python/sglang/srt/configs/model_config.py b/python/sglang/srt/configs/model_config.py index 4ce6c04549a0..aca43047668b 100644 --- a/python/sglang/srt/configs/model_config.py +++ b/python/sglang/srt/configs/model_config.py @@ -732,21 +732,6 @@ def __init__( if self.is_fp4_experts: logger.info("Detected mixed checkpoint layout: routed experts are MXFP4.") - # MiniMax-M3.1 Quark marks routed experts as FP4 here, not with - # routed_experts_quant_method. - global_weight_config = ( - quantization_config.get("global_quant_config") or {} - ).get("weight") or {} - if ( - not self.is_fp4_experts - and getattr(self.hf_config, "model_type", None) == "minimax_m3_vl" - and str(global_weight_config.get("dtype", "")).lower() == "fp4" - ): - self.is_fp4_experts = True - logger.info( - "Detected MiniMax-M3 Quark mixed layout: routed experts are MXFP4." - ) - # MiMo-V2 mxfp4 ckpts declare the routed-expert layout via store_dtype. if ( not self.is_fp4_experts diff --git a/python/sglang/srt/layers/quantization/quark/quark.py b/python/sglang/srt/layers/quantization/quark/quark.py index 31b218622368..a4f894b752b8 100644 --- a/python/sglang/srt/layers/quantization/quark/quark.py +++ b/python/sglang/srt/layers/quantization/quark/quark.py @@ -77,6 +77,18 @@ def _parse_nvfp4_excludes(hf_quant_config: Dict[str, Any]) -> List[str]: return [p if p.startswith("re:") else "re:" + fnmatch.translate(p) for p in pats] +def _minimax_quark_routed_experts_are_fp4( + model_type: Optional[str], quant_config: dict[str, Any] +) -> bool: + """MiniMax Quark stores routed experts as global FP4.""" + if model_type != "minimax_m3_vl": + return False + weight = (quant_config.get("global_quant_config") or {}).get("weight") or {} + if not isinstance(weight, dict): + return False + return str(weight.get("dtype", "")).lower() == "fp4" + + def _detect_nvfp4_source(config: Dict[str, Any]) -> Optional["Nvfp4SourceConfig"]: """Return an Nvfp4SourceConfig if `config` (the checkpoint's quantization_config dict) describes a supported NVFP4 source, else None. @@ -364,6 +376,10 @@ def __init__( # Gates the online shared-expert MXFP4 conversion to the architectures # it is supported on; see _ONLINE_SHARED_EXPERT_MXFP4_MODEL_TYPES. self.model_type = getattr(hf_config, "model_type", None) + # MiniMax Quark marks routed experts via the global weight dtype. + self.is_fp4_experts = _minimax_quark_routed_experts_are_fp4( + self.model_type, quant_config + ) self.is_prequantized = is_prequantized self.dequantization_config = dequantization_config # Load-as-is FP8 config for excluded layers of a mixed-precision source diff --git a/python/sglang/srt/model_loader/loader.py b/python/sglang/srt/model_loader/loader.py index 048ff85e3410..d52622f7ecb3 100644 --- a/python/sglang/srt/model_loader/loader.py +++ b/python/sglang/srt/model_loader/loader.py @@ -251,7 +251,6 @@ def _get_quantization_config( elif quant_config.get_name() == "humming": quant_config.is_fp4_experts = model_config.is_fp4_experts elif quant_config.get_name() == "quark" and model_config.is_fp4_experts: - # So the fused MoE loader can requantize the FP8 shared expert. quant_config.is_fp4_experts = True if not _is_npu: major, minor = get_device_capability()