"""MLX runtime for Qwen3.6 ModelOpt hybrid FP8/NVFP4 checkpoints. The converter stores ModelOpt FP8 weights losslessly in MLX's packed MXFP8 carrier with unit E8M0 block scales, then applies the original per-tensor ModelOpt scale to the output. ModelOpt NVFP4 weights and E4M3 block scales are likewise retained bit-for-bit; their FP32 tensor scale is applied after the matrix multiplication. Activations remain in the model dtype. This avoids adding a second lossy activation requantization scheme while still using MLX's native quantized weight kernels. """ from dataclasses import dataclass, field from typing import Dict import mlx.core as mx import mlx.nn as nn from mlx.utils import tree_flatten, tree_unflatten from mlx_lm.models.base import BaseModelArgs from mlx_lm.models.qwen3_5_moe import Model as BaseModel from mlx_lm.models.switch_layers import SwitchLinear @dataclass class ModelArgs(BaseModelArgs): model_type: str text_config: dict mlx_modelopt_quantization: Dict[str, str] = field(default_factory=dict) class ScaledQuantizedLinear(nn.Module): """Weight-quantized dense linear with an additional tensor scale.""" def __init__( self, input_dims: int, output_dims: int, *, group_size: int, bits: int, mode: str, bias: bool = False, ): super().__init__() if input_dims % group_size: raise ValueError( f"input_dims={input_dims} is not divisible by group_size={group_size}" ) if (input_dims * bits) % 32: raise ValueError( f"input_dims={input_dims}, bits={bits} cannot be packed into uint32" ) self.group_size = group_size self.bits = bits self.mode = mode self.weight = mx.zeros( (output_dims, input_dims * bits // 32), dtype=mx.uint32 ) self.scales = mx.zeros( (output_dims, input_dims // group_size), dtype=mx.uint8 ) self.global_scale = mx.ones((), dtype=mx.float32) if bias: self.bias = mx.zeros((output_dims,)) self.freeze() @classmethod def from_linear(cls, linear: nn.Module, kind: str): output_dims, input_dims = linear.weight.shape has_bias = linear.get("bias") is not None if kind == "scaled_mxfp8": params = dict(group_size=32, bits=8, mode="mxfp8") elif kind == "scaled_nvfp4": params = dict(group_size=16, bits=4, mode="nvfp4") else: raise ValueError(f"Unsupported dense quantization kind: {kind}") return cls(input_dims, output_dims, bias=has_bias, **params) def __call__(self, x): y = mx.quantized_matmul( x, self["weight"], self["scales"], transpose=True, group_size=self.group_size, bits=self.bits, mode=self.mode, ) # Avoid promoting the residual stream to float32. y = y * self["global_scale"].astype(y.dtype) if "bias" in self: y = y + self["bias"] return y class ScaledNVFP4SwitchLinear(nn.Module): """Expert linear using MLX gather_qmm and per-expert tensor scales.""" group_size = 16 bits = 4 mode = "nvfp4" def __init__( self, input_dims: int, output_dims: int, num_experts: int, *, bias: bool = False, ): super().__init__() if input_dims % self.group_size: raise ValueError( f"input_dims={input_dims} is not divisible by {self.group_size}" ) self.weight = mx.zeros( (num_experts, output_dims, input_dims * self.bits // 32), dtype=mx.uint32, ) self.scales = mx.zeros( (num_experts, output_dims, input_dims // self.group_size), dtype=mx.uint8, ) self.global_scales = mx.ones((num_experts,), dtype=mx.float32) if bias: self.bias = mx.zeros((num_experts, output_dims)) self.freeze() @classmethod def from_switch_linear(cls, linear: SwitchLinear): num_experts, output_dims, input_dims = linear.weight.shape has_bias = linear.get("bias") is not None return cls( input_dims, output_dims, num_experts, bias=has_bias, ) @property def input_dims(self): return self.scales.shape[2] * self.group_size @property def output_dims(self): return self.weight.shape[1] @property def num_experts(self): return self.weight.shape[0] def __call__(self, x, indices, sorted_indices=False): y = mx.gather_qmm( x, self["weight"], self["scales"], rhs_indices=indices, transpose=True, group_size=self.group_size, bits=self.bits, mode=self.mode, sorted_indices=sorted_indices, ) scale = self["global_scales"][indices].astype(y.dtype)[..., None, None] y = y * scale if "bias" in self: y = y + mx.expand_dims(self["bias"][indices], -2) return y def _replace_quantized_modules(model: nn.Module, quantization: Dict[str, str]): leaves = dict( tree_flatten(model.leaf_modules(), is_leaf=lambda m: isinstance(m, nn.Module)) ) missing = sorted(set(quantization) - set(leaves)) if missing: preview = "\n ".join(missing[:20]) raise ValueError(f"Quantized module paths are absent from the model:\n {preview}") for path, kind in quantization.items(): module = leaves[path] if kind in ("scaled_mxfp8", "scaled_nvfp4"): if not isinstance(module, nn.Linear): raise TypeError(f"{path} is {type(module).__name__}, expected Linear") leaves[path] = ScaledQuantizedLinear.from_linear(module, kind) elif kind == "scaled_nvfp4_switch": if not isinstance(module, SwitchLinear): raise TypeError( f"{path} is {type(module).__name__}, expected SwitchLinear" ) leaves[path] = ScaledNVFP4SwitchLinear.from_switch_linear(module) else: raise ValueError(f"Unknown quantization kind {kind!r} for {path}") model.update_modules(tree_unflatten(list(leaves.items()))) class Model(BaseModel): def __init__(self, args: ModelArgs): super().__init__(args) _replace_quantized_modules(self, args.mlx_modelopt_quantization)