Qwen3.6-35B-A3B-AntiLoop-NVFP4 / modeling_mlx_qwen36_modelopt_hybrid.py
N8Programs's picture
Add MLX-VLM runtime, configuration, and model card
c179c12 verified
Raw
History Blame
6.65 kB
"""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)