6564 lines
238 KiB
Python
6564 lines
238 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
||
"""oQ: oMLX Universal Dynamic Quantization.
|
||
|
||
Mixed-precision quantization combining GGUF K-quant layer position strategy,
|
||
unsloth Dynamic 2.0 selective non-quantization, and BnB MSE-optimal clipping.
|
||
|
||
Supported levels: oQ2, oQ2.5, oQ2.7, oQ3, oQ3.5, oQ4, oQ5, oQ6, oQ8
|
||
(base bits differ, same predicate). Fractional levels keep the lower level's
|
||
base bits and add targeted routed-expert protection plus a higher bpw budget.
|
||
"""
|
||
|
||
import hashlib
|
||
import json
|
||
import logging
|
||
import re
|
||
import shutil
|
||
import tempfile
|
||
import time as _time
|
||
from dataclasses import dataclass
|
||
from pathlib import Path
|
||
from typing import Any, Callable, Optional, Union
|
||
|
||
import numpy as np
|
||
|
||
try:
|
||
import mlx.core as mx
|
||
import mlx.nn as nn
|
||
from mlx.utils import tree_flatten, tree_unflatten
|
||
from mlx_lm.models.base import create_attention_mask
|
||
|
||
HAS_MLX = True
|
||
except ImportError:
|
||
HAS_MLX = False
|
||
|
||
from omlx.model_discovery import _has_vision_subconfig
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
OQ_LEVELS = {2, 2.5, 2.7, 3, 3.5, 4, 5, 6, 8}
|
||
|
||
OQ_DTYPES: tuple[str, ...] = ("bfloat16", "float16")
|
||
|
||
_OQ_DEFAULT_GROUP_SIZE = 64
|
||
|
||
_MAX_MODEL_RAM_FRACTION = 0.8
|
||
|
||
# Auto-built proxy for sensitivity measurement when the source model
|
||
# exceeds available RAM. Uniform 4-bit affine quant — same shape as a
|
||
# user-supplied --sensitivity-model, but built on demand.
|
||
_PROXY_QUANT_BITS = 4
|
||
_PROXY_QUANT_GROUP_SIZE = 64
|
||
|
||
_LEVEL_BITS: dict[float, int] = {
|
||
2: 2,
|
||
2.5: 2,
|
||
2.7: 2,
|
||
3: 3,
|
||
3.5: 3,
|
||
4: 4,
|
||
5: 5,
|
||
6: 6,
|
||
8: 8,
|
||
}
|
||
|
||
_LEVEL_PROTECTION: dict[float, str] = {
|
||
2: "full",
|
||
2.5: "full",
|
||
2.7: "full",
|
||
3: "full",
|
||
3.5: "full",
|
||
4: "full",
|
||
5: "full",
|
||
6: "full",
|
||
8: "full",
|
||
}
|
||
|
||
# Fractional levels that reserve a blanket Super Weights floor.
|
||
# 3.5 -> routed expert down_proj 4-bit.
|
||
_LEVEL_EXPERT_DOWN_BOOST: dict[float, int] = {3.5: 1}
|
||
|
||
_OQ_BPW_TARGETS: dict[float, tuple[float, float]] = {
|
||
2: (2.8, 3.0),
|
||
2.5: (3.1, 3.3),
|
||
2.7: (3.25, 3.35),
|
||
3: (3.5, 3.7),
|
||
3.5: (3.8, 4.0),
|
||
4: (4.6, 4.7),
|
||
5: (5.5, 5.7),
|
||
6: (6.5, 6.7),
|
||
}
|
||
|
||
_ROUTED_LAYER_BOOST_LEVELS = {2.5, 2.7}
|
||
_VALID_QUANT_BITS = (2, 3, 4, 5, 6, 8)
|
||
|
||
|
||
def _bpw_targets_for_level(oq_level: float) -> tuple[float, float] | None:
|
||
"""Return (target_bpw, hard_cap_bpw) for the given oQ level, or None."""
|
||
return _OQ_BPW_TARGETS.get(oq_level)
|
||
|
||
|
||
def _is_deepseek_v4_config(config: dict) -> bool:
|
||
model_type = str(config.get("model_type", "")).lower()
|
||
if model_type.startswith("deepseek_v4"):
|
||
return True
|
||
|
||
architectures = config.get("architectures") or []
|
||
return any(
|
||
str(arch).lower().replace("_", "") == "deepseekv4forcausallm"
|
||
for arch in architectures
|
||
)
|
||
|
||
|
||
def _validate_oq_dtype_for_model(config: dict, dtype: str) -> None:
|
||
if dtype == "float16" and _is_deepseek_v4_config(config):
|
||
raise ValueError(
|
||
"oQ dtype=float16 is unsupported for deepseek_v4. "
|
||
"DeepSeek V4 fp16 oQ can collapse to repeated BOS tokens during "
|
||
"generation; use dtype='bfloat16' instead."
|
||
)
|
||
|
||
|
||
@dataclass
|
||
class QuantPlan:
|
||
"""Byte-budgeted mixed-precision plan for a single quantization run."""
|
||
|
||
boost_map: dict[str, dict]
|
||
effective_bpw: float
|
||
target_bpw: float
|
||
hard_cap_bpw: float
|
||
|
||
|
||
@dataclass
|
||
class OQImatrixEntry:
|
||
"""Activation-energy statistics for one quantized weight tensor."""
|
||
|
||
in_sum2: np.ndarray
|
||
counts: np.ndarray
|
||
|
||
|
||
@dataclass
|
||
class OQImatrixData:
|
||
"""Loaded oQe imatrix cache and the metadata used to validate it."""
|
||
|
||
entries: dict[str, OQImatrixEntry]
|
||
metadata: dict[str, Any]
|
||
path: str
|
||
reused: bool = False
|
||
|
||
|
||
def universal_quant_predicate(
|
||
path: str, module, config: dict, oq_level: int = 4
|
||
) -> Union[bool, dict]:
|
||
"""Per-tensor quantization decision based on GGUF/unsloth/llama.cpp rules.
|
||
|
||
Protection levels vary by oQ level:
|
||
oQ2: minimal protection (router fp16, lm_head 4-bit only) → ~2.5 bpw
|
||
oQ2.5/oQ2.7: base 2-bit + routed layer boosts selected by layer
|
||
sensitivity; routed w2/down_proj first, then w1/w3 as paired
|
||
layer-wide boosts while staying under the bpw cap
|
||
oQ3.5: base 3-bit with routed expert down_proj protected above base per
|
||
_LEVEL_EXPERT_DOWN_BOOST (Super Weights protection)
|
||
oQ3: base 3-bit + full protection → ~3.5 bpw
|
||
oQ4-oQ6: base N-bit + full protection
|
||
oQ7: base 8-bit + full protection
|
||
oQ8: near-uniform 8-bit (router fp16 only) → ~8.0 bpw
|
||
|
||
Args:
|
||
path: Dot-separated module path (e.g. "model.layers.0.self_attn.v_proj").
|
||
module: The nn.Module being quantized.
|
||
config: Model config.json dict.
|
||
oq_level: oQ quantization level (2-8).
|
||
|
||
Returns:
|
||
False to skip quantization (keep fp16),
|
||
True to use default bits,
|
||
dict with {"bits": N, "group_size": M} for per-layer override.
|
||
"""
|
||
path = _normalize_quant_path(path)
|
||
path_l = path.lower()
|
||
|
||
non_quantizable = config.get("_oq_non_quantizable", set())
|
||
if path in non_quantizable:
|
||
return False
|
||
|
||
tc = config.get("text_config", {})
|
||
num_layers = config.get("num_hidden_layers") or tc.get("num_hidden_layers", 32)
|
||
num_experts = (
|
||
config.get("num_local_experts")
|
||
or tc.get("num_local_experts")
|
||
or config.get("num_experts")
|
||
or tc.get("num_experts", 0)
|
||
or 0
|
||
)
|
||
hidden_size = config.get("hidden_size") or tc.get("hidden_size", 0)
|
||
is_moe = num_experts > 0
|
||
|
||
base_bits = int(_LEVEL_BITS.get(oq_level, oq_level))
|
||
protection = _LEVEL_PROTECTION.get(oq_level, "full")
|
||
full_protection = protection == "full"
|
||
|
||
def gs():
|
||
if _is_moe_router(path):
|
||
return 64
|
||
if num_experts >= 150:
|
||
return 128
|
||
return 64
|
||
|
||
def bits(n):
|
||
effective = int(max(n, base_bits))
|
||
return {
|
||
"bits": effective,
|
||
"group_size": _gs_for_mode(effective, gs()),
|
||
"mode": _mode_for_bits(effective),
|
||
}
|
||
|
||
if _is_moe_router(path):
|
||
return False # fp16 — tiny weights, some models (MoEGate) lack to_quantized()
|
||
|
||
if "shared_expert_gate" in path and "gate_proj" not in path:
|
||
return {"bits": 8, "group_size": 64, "mode": "affine"}
|
||
|
||
if _is_vision_tensor(path):
|
||
return False
|
||
|
||
if _is_audio_tensor(path):
|
||
return False
|
||
|
||
if any(
|
||
p in path_l
|
||
for p in ("ssm_alpha", "ssm_beta", "a_log", "time_decay", "time_faaaa")
|
||
):
|
||
return False
|
||
|
||
if path.endswith(".D"):
|
||
return False
|
||
|
||
# Gated DeltaNet / Mamba-like SSM sensitive params (Qwen3_5 hybrid arch).
|
||
# dt_bias drives the discretization step, keep fp16/fp32 like A_log.
|
||
# conv1d is a small depth-wise causal conv, very sensitive to low bits.
|
||
# linear_attn.out_proj mirrors self_attn.o_proj sensitivity.
|
||
if path_l.endswith("dt_bias"):
|
||
return False
|
||
if "conv1d" in path_l and "linear_attn" in path_l:
|
||
return bits(8)
|
||
if "linear_attn.out_proj" in path_l:
|
||
return bits(5)
|
||
|
||
boost_map = config.get("_oq_boost_map") or {}
|
||
if path in boost_map:
|
||
return dict(boost_map[path])
|
||
|
||
if config.get("_oq_use_budget_plan"):
|
||
if any(p in path for p in ("ssm_output", "ssm_out")):
|
||
return bits(8)
|
||
if "lora.2" in path:
|
||
return bits(8)
|
||
return True
|
||
|
||
if not full_protection:
|
||
if any(p in path for p in ("lm_head", "output.weight", "classifier")):
|
||
return bits(6)
|
||
|
||
if any(p in path for p in ("ssm_output", "ssm_out")):
|
||
return bits(8)
|
||
|
||
if any(p in path for p in ("embed_tokens", "wte", "word_embeddings")):
|
||
return bits(base_bits + 2)
|
||
|
||
if num_experts >= 512 and hidden_size >= 4096:
|
||
if "gate_proj" in path and "shared_expert" not in path:
|
||
return bits(4)
|
||
|
||
layer_idx = _extract_layer_index(path)
|
||
if layer_idx >= 0:
|
||
sensitive = layer_idx < num_layers // 8 or layer_idx >= 7 * num_layers // 8
|
||
is_expert = "switch_mlp" in path or "experts" in path
|
||
if sensitive and not is_expert:
|
||
return bits(base_bits + 1)
|
||
|
||
return True
|
||
|
||
if any(p in path for p in ("ssm_output", "ssm_out")):
|
||
return bits(8)
|
||
|
||
if "lora.2" in path:
|
||
return bits(8)
|
||
|
||
if any(p in path for p in ("lm_head", "output.weight", "classifier")):
|
||
return bits(6)
|
||
|
||
if "cross_attn" in path and "o_proj" in path:
|
||
return bits(6)
|
||
|
||
if any(
|
||
p in path for p in ("kv_a_proj_with_mqa", "kv_b_proj", "q_a_proj", "q_b_proj")
|
||
):
|
||
return bits(6)
|
||
|
||
if "o_proj" in path and "shared_expert" not in path:
|
||
if not is_moe:
|
||
return bits(5)
|
||
|
||
if "shared_expert" in path and not path.endswith("shared_expert_gate"):
|
||
return bits(8)
|
||
|
||
if num_experts >= 512 and hidden_size >= 4096:
|
||
if "gate_proj" in path and "shared_expert" not in path:
|
||
return bits(4)
|
||
if "down_proj" in path and "shared_expert" not in path:
|
||
return bits(3)
|
||
|
||
layer_idx = _extract_layer_index(path)
|
||
|
||
sensitivity_map = config.get("_oq_sensitivity_map")
|
||
if sensitivity_map and layer_idx >= 0:
|
||
scores = list(sensitivity_map.values())
|
||
scores.sort(reverse=True)
|
||
threshold = scores[max(0, len(scores) // 4 - 1)] if scores else 0
|
||
sensitive = sensitivity_map.get(str(layer_idx), 0) >= threshold
|
||
else:
|
||
sensitive = layer_idx >= 0 and (
|
||
layer_idx < num_layers // 8 or layer_idx >= 7 * num_layers // 8
|
||
)
|
||
|
||
if any(p in path for p in ("v_proj", "v_a_proj", "v_b_proj")):
|
||
if sensitive:
|
||
return bits(6)
|
||
return True
|
||
|
||
if any(p in path for p in ("down_proj", "w2", "mlp.fc2", "wo")):
|
||
is_routed_expert = (
|
||
is_moe
|
||
and "shared_expert" not in path
|
||
and ("switch_mlp" in path or "experts" in path)
|
||
)
|
||
if is_routed_expert:
|
||
down_boost = _LEVEL_EXPERT_DOWN_BOOST.get(oq_level)
|
||
if down_boost:
|
||
# Mandatory fractional levels protect routed expert down_proj
|
||
# above the base bits (Super Weights protection).
|
||
return bits(base_bits + down_boost)
|
||
return True
|
||
if sensitive:
|
||
return bits(6)
|
||
return bits(5)
|
||
|
||
if any(p in path for p in ("q_proj", "k_proj")):
|
||
if sensitive:
|
||
return bits(5)
|
||
|
||
if any(p in path for p in ("qkv_proj", "in_proj_qkv", "attn_qkv")):
|
||
if sensitive:
|
||
return bits(5)
|
||
|
||
if any(p in path for p in ("in_proj_z", "in_proj_a", "in_proj_b", "delta_net")):
|
||
return bits(5)
|
||
|
||
if any(p in path for p in ("mixer.in_proj", "mixer.out_proj", "x_proj", "dt_proj")):
|
||
return bits(5)
|
||
|
||
return True
|
||
|
||
|
||
def _is_vision_tensor(name: str) -> bool:
|
||
"""Check if a tensor belongs to the vision encoder/projector."""
|
||
return any(
|
||
p in name
|
||
for p in (
|
||
"visual.",
|
||
"vision_",
|
||
"patch_embed",
|
||
"pos_embed",
|
||
"image_newline",
|
||
"multi_modal_projector",
|
||
"visual.merger",
|
||
"image_norm",
|
||
"temporal_embed",
|
||
)
|
||
)
|
||
|
||
|
||
def _is_audio_tensor(name: str) -> bool:
|
||
"""Check if a tensor belongs to the audio encoder.
|
||
|
||
Mirrors `_is_vision_tensor`: matches `audio_tower.*` only, not
|
||
`embed_audio.*` (the projection from audio output to text hidden size,
|
||
which is quantized like `embed_vision.embedding_projection`).
|
||
"""
|
||
return "audio_tower" in name
|
||
|
||
|
||
def _is_moe_router(path: str) -> bool:
|
||
"""Detect MoE router/gate layers (distinct from gate_proj)."""
|
||
if path.endswith(("mlp.gate", ".router", ".router.layer")):
|
||
return True
|
||
if path.endswith(".gate") and "gate_proj" not in path:
|
||
return True
|
||
if ".gate." in path and "gate_proj" not in path:
|
||
return True
|
||
return False
|
||
|
||
|
||
def _extract_layer_index(path: str) -> int:
|
||
"""Extract transformer layer index from module path. Returns -1 if absent."""
|
||
m = re.search(r"layers\.(\d+)\.", path)
|
||
return int(m.group(1)) if m else -1
|
||
|
||
|
||
def _default_bits(config: dict) -> int:
|
||
"""Read default quantization bits from config."""
|
||
q = config.get("quantization", {})
|
||
return q.get("bits", 4)
|
||
|
||
|
||
def _normalize_quant_path(path: str) -> str:
|
||
"""Normalize tensor/module names to the module path used in configs."""
|
||
if path.endswith(".weight"):
|
||
return path[:-7]
|
||
if path.endswith(".scales"):
|
||
return path[:-7]
|
||
if path.endswith(".biases"):
|
||
return path[:-7]
|
||
return path
|
||
|
||
|
||
def _base_bits_for_level(oq_level: int) -> int:
|
||
return int(_LEVEL_BITS.get(oq_level, oq_level))
|
||
|
||
|
||
def _bytes_per_group(mode: str) -> int:
|
||
if mode == "mxfp4":
|
||
return 1
|
||
if mode == "mxfp8":
|
||
return 2
|
||
return 4
|
||
|
||
|
||
def _tensor_quantized_bytes(shape: tuple, bits: int, group_size: int, mode: str) -> int:
|
||
"""Estimate serialized bytes for a quantized tensor."""
|
||
n_elements = 1
|
||
for dim in shape:
|
||
n_elements *= dim
|
||
if len(shape) < 2:
|
||
return n_elements * 2
|
||
if shape[-1] % group_size != 0:
|
||
return n_elements * 2
|
||
rows = n_elements // max(shape[-1], 1)
|
||
n_groups = shape[-1] // group_size
|
||
weight_bytes = (n_elements * bits + 7) // 8
|
||
overhead_bytes = rows * n_groups * _bytes_per_group(mode)
|
||
return weight_bytes + overhead_bytes
|
||
|
||
|
||
def _estimate_effective_bpw(
|
||
named_shapes: dict[str, tuple],
|
||
base_bits: int,
|
||
base_group_size: int,
|
||
base_mode: str,
|
||
overrides: dict[str, dict] | None = None,
|
||
) -> float:
|
||
"""Estimate effective bpw for quantizable weights only."""
|
||
overrides = overrides or {}
|
||
total_bits = 0
|
||
total_params = 0
|
||
|
||
for path, shape in named_shapes.items():
|
||
n_elements = 1
|
||
for dim in shape:
|
||
n_elements *= dim
|
||
total_params += n_elements
|
||
|
||
override = overrides.get(path)
|
||
if override is None:
|
||
bits = base_bits
|
||
gs = base_group_size
|
||
mode = base_mode
|
||
else:
|
||
bits = int(override.get("bits", base_bits))
|
||
gs = int(override.get("group_size", base_group_size))
|
||
mode = override.get("mode", _mode_for_bits(bits))
|
||
|
||
total_bits += 8 * _tensor_quantized_bytes(shape, bits, gs, mode)
|
||
|
||
return total_bits / max(total_params, 1)
|
||
|
||
|
||
def _collect_named_weight_shapes_from_model(model) -> dict[str, tuple]:
|
||
"""Collect quantizable weight shapes from the in-memory model."""
|
||
named_shapes = {}
|
||
for path, module in tree_flatten(model.leaf_modules(), is_leaf=nn.Module.is_module):
|
||
if not hasattr(module, "weight") or not hasattr(module, "to_quantized"):
|
||
continue
|
||
if getattr(module.weight, "ndim", 0) < 2:
|
||
continue
|
||
named_shapes[_normalize_quant_path(path)] = tuple(module.weight.shape)
|
||
return named_shapes
|
||
|
||
|
||
def _collect_named_weight_shapes_from_weights(
|
||
weights: dict[str, Any],
|
||
) -> dict[str, tuple]:
|
||
"""Collect quantizable weight shapes from sanitized weight tensors."""
|
||
named_shapes = {}
|
||
for name, tensor in weights.items():
|
||
norm_name = _normalize_quant_path(name)
|
||
if name != f"{norm_name}.weight":
|
||
continue
|
||
if getattr(tensor, "ndim", 0) < 2:
|
||
continue
|
||
named_shapes[norm_name] = tuple(tensor.shape)
|
||
return named_shapes
|
||
|
||
|
||
def _is_routed_expert(path: str) -> bool:
|
||
"""Check if a tensor belongs to routed MoE experts (93-98% of params)."""
|
||
if "switch_mlp" in path:
|
||
return True
|
||
if "experts" in path and "shared_expert" not in path:
|
||
return True
|
||
if "block_sparse_moe" in path and "shared_expert" not in path:
|
||
return True
|
||
return False
|
||
|
||
|
||
def _routed_expert_projection(path: str) -> str | None:
|
||
"""Return the routed expert projection family for a module/tensor path."""
|
||
if not _is_routed_expert(path):
|
||
return None
|
||
if any(p in path for p in ("down_proj", ".w2", "mlp.fc2", ".fc2")):
|
||
return "down"
|
||
if any(p in path for p in ("gate_proj", ".w1", "mlp.fc1", ".fc1")):
|
||
return "gate"
|
||
if any(p in path for p in ("up_proj", ".w3")):
|
||
return "up"
|
||
return None
|
||
|
||
|
||
_MANDATORY_BOOST_PATTERNS = {
|
||
"lm_head": {"bits": 8, "group_size": 64, "mode": "affine"},
|
||
"embeddings": {"bits": 8, "group_size": 64, "mode": "affine"},
|
||
"embed_tokens": {"bits": 8, "group_size": 64, "mode": "affine"},
|
||
"wte": {"bits": 8, "group_size": 64, "mode": "affine"},
|
||
}
|
||
|
||
|
||
def _sensitivity_tier(layer_score: float, max_score: float) -> int:
|
||
"""Map sensitivity score to boost tier: +4 (top), +2 (high), +1 (moderate).
|
||
|
||
Greedy allocator will fallback to lower tiers if budget can't fit the
|
||
requested bits (e.g., 8-bit → try 6-bit → try 5-bit).
|
||
"""
|
||
if max_score <= 0:
|
||
return 1
|
||
ratio = layer_score / max_score
|
||
if ratio >= 0.5:
|
||
return 4
|
||
if ratio >= 0.2:
|
||
return 2
|
||
return 1
|
||
|
||
|
||
def _apply_routed_layer_boosts(
|
||
named_shapes: dict[str, tuple],
|
||
config: dict,
|
||
oq_level: float,
|
||
boost_map: dict[str, dict],
|
||
fixed_overrides: dict[str, dict],
|
||
base_bits: int,
|
||
base_group_size: int,
|
||
base_mode: str,
|
||
total_bits_f: float,
|
||
total_params: int,
|
||
current_bpw: float,
|
||
target_bpw: float,
|
||
hard_cap_bpw: float,
|
||
) -> tuple[float, float]:
|
||
"""Boost routed expert modules by layer while staying MLX-loader portable.
|
||
|
||
MLX's QuantizedSwitchLinear stores one bit-width per fused expert projection,
|
||
not per expert. For oQ2.5/oQ2.7 we therefore rank layers by sensitivity
|
||
and boost whole routed projection modules: down/w2 first, then gate+up as
|
||
a pair.
|
||
"""
|
||
if oq_level not in _ROUTED_LAYER_BOOST_LEVELS or base_bits >= 3:
|
||
return total_bits_f, current_bpw
|
||
|
||
from collections import defaultdict
|
||
|
||
layer_scores = config.get("_oq_sensitivity_map") or {}
|
||
grouped: dict[tuple[int, str], list[tuple[str, tuple]]] = defaultdict(list)
|
||
for path, shape in named_shapes.items():
|
||
if path in fixed_overrides:
|
||
continue
|
||
projection = _routed_expert_projection(path)
|
||
if projection is None:
|
||
continue
|
||
layer_idx = _extract_layer_index(path)
|
||
if layer_idx < 0:
|
||
continue
|
||
phase = "down" if projection == "down" else "gate_up"
|
||
grouped[(layer_idx, phase)].append((path, shape))
|
||
|
||
def group_score(layer_idx: int) -> float:
|
||
return float(layer_scores.get(str(layer_idx), 0.0))
|
||
|
||
def try_boost_group(items: list[tuple[str, tuple]]) -> bool:
|
||
nonlocal total_bits_f, current_bpw
|
||
delta = 0
|
||
updates = []
|
||
cand_bits = 3
|
||
cand_gs = _gs_for_mode(cand_bits, _OQ_DEFAULT_GROUP_SIZE)
|
||
cand_mode = _mode_for_bits(cand_bits)
|
||
for path, shape in items:
|
||
cur = boost_map.get(path)
|
||
cur_bits = int(cur["bits"]) if cur else base_bits
|
||
if cur_bits >= cand_bits:
|
||
continue
|
||
cur_gs = (
|
||
int(cur.get("group_size", base_group_size)) if cur else base_group_size
|
||
)
|
||
cur_mode = cur.get("mode", base_mode) if cur else base_mode
|
||
old_cost = _tensor_quantized_bytes(shape, cur_bits, cur_gs, cur_mode)
|
||
new_cost = _tensor_quantized_bytes(shape, cand_bits, cand_gs, cand_mode)
|
||
item_delta = 8 * (new_cost - old_cost)
|
||
if item_delta <= 0:
|
||
continue
|
||
delta += item_delta
|
||
updates.append(path)
|
||
if not updates:
|
||
return False
|
||
next_bpw = (total_bits_f + delta) / total_params
|
||
if next_bpw > hard_cap_bpw:
|
||
return False
|
||
for path in updates:
|
||
boost_map[path] = {
|
||
"bits": cand_bits,
|
||
"group_size": cand_gs,
|
||
"mode": cand_mode,
|
||
}
|
||
total_bits_f += delta
|
||
current_bpw = next_bpw
|
||
return True
|
||
|
||
for phase in ("down", "gate_up"):
|
||
candidates = [
|
||
(layer_idx, items)
|
||
for (layer_idx, group_phase), items in grouped.items()
|
||
if group_phase == phase
|
||
]
|
||
candidates.sort(key=lambda item: (-group_score(item[0]), item[0]))
|
||
for _layer_idx, items in candidates:
|
||
if current_bpw >= target_bpw:
|
||
break
|
||
try_boost_group(items)
|
||
|
||
return total_bits_f, current_bpw
|
||
|
||
|
||
def _build_quant_plan(
|
||
named_shapes: dict[str, tuple],
|
||
config: dict,
|
||
oq_level: int,
|
||
target_bpw: float = 4.6,
|
||
hard_cap_bpw: float = 4.7,
|
||
fixed_overrides: dict[str, dict] | None = None,
|
||
) -> QuantPlan:
|
||
"""Allocate byte-budgeted boosts using sensitivity-driven allocation.
|
||
|
||
Strategy:
|
||
1. Mandatory pre-allocation: consensus-critical tensors (lm_head → 8-bit)
|
||
2. Data-driven: all non-expert tensors compete equally, ranked by
|
||
layer sensitivity score. Higher sensitivity → more bits.
|
||
3. Routed experts stay at base bits except explicit fractional floors and
|
||
the oQ2.5/oQ2.7 fallback routed-layer boost.
|
||
|
||
fixed_overrides marks tensors whose output format is fixed up front
|
||
(pre-quantized source tensors passed through as mxfp4/mxfp8). They are
|
||
priced into the baseline bpw at their true cost and excluded from every
|
||
boost decision.
|
||
"""
|
||
base_bits = _base_bits_for_level(oq_level)
|
||
base_mode = _mode_for_bits(base_bits)
|
||
base_group_size = _gs_for_mode(base_bits, _OQ_DEFAULT_GROUP_SIZE)
|
||
boost_map: dict[str, dict] = {}
|
||
fixed_overrides = fixed_overrides or {}
|
||
|
||
layer_scores = config.get("_oq_sensitivity_map") or {}
|
||
max_layer_score = max(layer_scores.values(), default=0.0)
|
||
|
||
total_params = 0
|
||
expert_params = 0
|
||
for path, shape in named_shapes.items():
|
||
n = 1
|
||
for dim in shape:
|
||
n *= dim
|
||
total_params += n
|
||
if _is_routed_expert(path):
|
||
expert_params += n
|
||
|
||
current_bpw = _estimate_effective_bpw(
|
||
named_shapes,
|
||
base_bits,
|
||
base_group_size,
|
||
base_mode,
|
||
overrides=fixed_overrides,
|
||
)
|
||
total_bits_f = current_bpw * total_params
|
||
|
||
module = None
|
||
for path, shape in named_shapes.items():
|
||
if path in fixed_overrides:
|
||
continue
|
||
pred = universal_quant_predicate(
|
||
path, module, {**config, "_oq_boost_map": {}}, oq_level
|
||
)
|
||
if pred is False:
|
||
continue
|
||
for pattern, boost in _MANDATORY_BOOST_PATTERNS.items():
|
||
if pattern in path:
|
||
cand_bits = int(boost["bits"])
|
||
if cand_bits <= base_bits:
|
||
break
|
||
cand_gs = int(boost.get("group_size", base_group_size))
|
||
cand_mode = boost.get("mode", _mode_for_bits(cand_bits))
|
||
base_cost = _tensor_quantized_bytes(
|
||
shape, base_bits, base_group_size, base_mode
|
||
)
|
||
cand_cost = _tensor_quantized_bytes(
|
||
shape, cand_bits, cand_gs, cand_mode
|
||
)
|
||
delta = 8 * (cand_cost - base_cost)
|
||
next_bpw = (total_bits_f + delta) / total_params
|
||
if delta > 0 and next_bpw <= hard_cap_bpw:
|
||
boost_map[path] = dict(boost)
|
||
total_bits_f += delta
|
||
current_bpw = next_bpw
|
||
break
|
||
|
||
# Fractional levels with a blanket Super Weights floor: mandatory expert
|
||
# down_proj boost above base bits.
|
||
_down_boost = _LEVEL_EXPERT_DOWN_BOOST.get(oq_level)
|
||
if _down_boost:
|
||
for path, shape in named_shapes.items():
|
||
if path in boost_map or path in fixed_overrides:
|
||
continue
|
||
if not _is_routed_expert(path):
|
||
continue
|
||
if not any(p in path for p in ("down_proj", "w2")):
|
||
continue
|
||
cand_bits = base_bits + _down_boost
|
||
if cand_bits not in _VALID_QUANT_BITS:
|
||
continue
|
||
cand_gs = _gs_for_mode(cand_bits, _OQ_DEFAULT_GROUP_SIZE)
|
||
cand_mode = _mode_for_bits(cand_bits)
|
||
base_cost = _tensor_quantized_bytes(
|
||
shape, base_bits, base_group_size, base_mode
|
||
)
|
||
cand_cost = _tensor_quantized_bytes(shape, cand_bits, cand_gs, cand_mode)
|
||
delta = 8 * (cand_cost - base_cost)
|
||
if delta > 0:
|
||
boost_map[path] = {
|
||
"bits": cand_bits,
|
||
"group_size": cand_gs,
|
||
"mode": cand_mode,
|
||
}
|
||
total_bits_f += delta
|
||
current_bpw = total_bits_f / total_params
|
||
|
||
# Protection floor: apply full protection rules as minimum bits for
|
||
# non-expert tensors. This ensures attention, shared experts, etc. get
|
||
# adequate precision even at aggressive base bits (e.g. oQ2 base=2).
|
||
# Each floor boost is checked against hard_cap to avoid overshooting.
|
||
floor_config = {**config, "_oq_use_budget_plan": False, "_oq_boost_map": {}}
|
||
for path, shape in named_shapes.items():
|
||
if path in boost_map or path in fixed_overrides:
|
||
continue
|
||
if _is_routed_expert(path):
|
||
continue
|
||
floor_pred = universal_quant_predicate(path, module, floor_config, oq_level)
|
||
if not isinstance(floor_pred, dict):
|
||
continue
|
||
floor_bits = int(floor_pred["bits"])
|
||
if floor_bits <= base_bits:
|
||
continue
|
||
floor_gs = int(
|
||
floor_pred.get(
|
||
"group_size", _gs_for_mode(floor_bits, _OQ_DEFAULT_GROUP_SIZE)
|
||
)
|
||
)
|
||
floor_mode = floor_pred.get("mode", _mode_for_bits(floor_bits))
|
||
old_cost = _tensor_quantized_bytes(shape, base_bits, base_group_size, base_mode)
|
||
new_cost = _tensor_quantized_bytes(shape, floor_bits, floor_gs, floor_mode)
|
||
delta = 8 * (new_cost - old_cost)
|
||
if delta <= 0:
|
||
continue
|
||
next_bpw = (total_bits_f + delta) / total_params
|
||
if next_bpw > hard_cap_bpw:
|
||
continue
|
||
boost_map[path] = {
|
||
"bits": floor_bits,
|
||
"group_size": floor_gs,
|
||
"mode": floor_mode,
|
||
}
|
||
total_bits_f += delta
|
||
current_bpw = next_bpw
|
||
|
||
# Sensitivity-based greedy boost: boost tensors from their current bits
|
||
# (which may already be elevated by the protection floor) using remaining
|
||
# budget up to hard_cap_bpw.
|
||
candidates = []
|
||
for path, shape in named_shapes.items():
|
||
if _is_routed_expert(path) or path in fixed_overrides:
|
||
continue
|
||
pred = universal_quant_predicate(
|
||
path, module, {**config, "_oq_boost_map": {}}, oq_level
|
||
)
|
||
if pred is False:
|
||
continue
|
||
layer_idx = _extract_layer_index(path)
|
||
if layer_idx < 0:
|
||
# MTP-head tensors carry no ``layers.<n>`` index but should
|
||
# compete like any other non-expert tensor (the stated contract
|
||
# is backbone-equal treatment for the head's internal block);
|
||
# score 0 seeds them at the bottom tier and the under-target
|
||
# fallback lifts them with the rest.
|
||
if not _is_mtp_tensor(path):
|
||
continue
|
||
layer_score = 0.0
|
||
else:
|
||
layer_score = float(layer_scores.get(str(layer_idx), 0.0))
|
||
# Current bits (floor or base)
|
||
cur_bits = boost_map[path]["bits"] if path in boost_map else base_bits
|
||
cur_gs = _gs_for_mode(cur_bits, _OQ_DEFAULT_GROUP_SIZE)
|
||
cur_mode = _mode_for_bits(cur_bits)
|
||
cur_cost = _tensor_quantized_bytes(shape, cur_bits, cur_gs, cur_mode)
|
||
# Max target based on sensitivity
|
||
ratio = layer_score / max_layer_score if max_layer_score > 0 else 0
|
||
if ratio >= 0.5:
|
||
max_target = 8
|
||
elif ratio >= 0.2:
|
||
max_target = min(cur_bits + 2, 8)
|
||
else:
|
||
max_target = min(cur_bits + 1, 8)
|
||
if max_target <= cur_bits:
|
||
continue
|
||
candidates.append((layer_score, path, shape, cur_bits, cur_cost, max_target))
|
||
|
||
for _score, path, shape, cur_bits, cur_cost, max_target in sorted(
|
||
candidates, key=lambda x: x[0], reverse=True
|
||
):
|
||
for cand_bits in range(max_target, cur_bits, -1):
|
||
if cand_bits not in _VALID_QUANT_BITS or cand_bits <= cur_bits:
|
||
continue
|
||
cand_gs = _gs_for_mode(cand_bits, _OQ_DEFAULT_GROUP_SIZE)
|
||
cand_mode = _mode_for_bits(cand_bits)
|
||
cand_cost = _tensor_quantized_bytes(shape, cand_bits, cand_gs, cand_mode)
|
||
delta = 8 * (cand_cost - cur_cost)
|
||
if delta <= 0:
|
||
continue
|
||
next_bpw = (total_bits_f + delta) / total_params
|
||
if next_bpw > hard_cap_bpw:
|
||
continue
|
||
boost_map[path] = {
|
||
"bits": cand_bits,
|
||
"group_size": cand_gs,
|
||
"mode": cand_mode,
|
||
}
|
||
total_bits_f += delta
|
||
current_bpw = next_bpw
|
||
break
|
||
|
||
# Fallback: if still under target, boost non-expert tensors toward 8-bit
|
||
# regardless of sensitivity tier. On large MoE models, non-expert weights
|
||
# are <6% of params so every bit counts to reach the target bpw.
|
||
if current_bpw < target_bpw:
|
||
fallback_candidates = []
|
||
for path, shape in named_shapes.items():
|
||
if _is_routed_expert(path) or path in fixed_overrides:
|
||
continue
|
||
cur = boost_map.get(path)
|
||
if cur is None:
|
||
continue
|
||
cur_bits = cur["bits"]
|
||
if cur_bits >= 8:
|
||
continue
|
||
cur_gs = _gs_for_mode(cur_bits, _OQ_DEFAULT_GROUP_SIZE)
|
||
cur_mode = _mode_for_bits(cur_bits)
|
||
cur_cost = _tensor_quantized_bytes(shape, cur_bits, cur_gs, cur_mode)
|
||
layer_idx = _extract_layer_index(path)
|
||
layer_score = float(layer_scores.get(str(layer_idx), 0.0))
|
||
fallback_candidates.append((layer_score, path, shape, cur_bits, cur_cost))
|
||
|
||
for _score, path, shape, cur_bits, cur_cost in sorted(
|
||
fallback_candidates, key=lambda x: x[0], reverse=True
|
||
):
|
||
for cand_bits in (8, 6, 5, 4, 3):
|
||
if cand_bits <= cur_bits:
|
||
continue
|
||
cand_gs = _gs_for_mode(cand_bits, _OQ_DEFAULT_GROUP_SIZE)
|
||
cand_mode = _mode_for_bits(cand_bits)
|
||
cand_cost = _tensor_quantized_bytes(
|
||
shape, cand_bits, cand_gs, cand_mode
|
||
)
|
||
delta = 8 * (cand_cost - cur_cost)
|
||
if delta <= 0:
|
||
continue
|
||
next_bpw = (total_bits_f + delta) / total_params
|
||
if next_bpw > hard_cap_bpw:
|
||
continue
|
||
boost_map[path] = {
|
||
"bits": cand_bits,
|
||
"group_size": cand_gs,
|
||
"mode": cand_mode,
|
||
}
|
||
total_bits_f += delta
|
||
current_bpw = next_bpw
|
||
break
|
||
if current_bpw >= target_bpw:
|
||
break
|
||
|
||
if current_bpw < target_bpw:
|
||
total_bits_f, current_bpw = _apply_routed_layer_boosts(
|
||
named_shapes,
|
||
config,
|
||
oq_level,
|
||
boost_map,
|
||
fixed_overrides,
|
||
base_bits,
|
||
base_group_size,
|
||
base_mode,
|
||
total_bits_f,
|
||
total_params,
|
||
current_bpw,
|
||
target_bpw,
|
||
hard_cap_bpw,
|
||
)
|
||
|
||
if boost_map:
|
||
from collections import Counter
|
||
|
||
bits_dist = Counter(v["bits"] for v in boost_map.values())
|
||
route_dist = Counter()
|
||
layer_bits = {}
|
||
for k, v in boost_map.items():
|
||
projection = _routed_expert_projection(k)
|
||
if projection is not None:
|
||
route_dist[f"{projection}:{v['bits']}bit"] += 1
|
||
elif _is_routed_expert(k):
|
||
route_dist[f"routed_other:{v['bits']}bit"] += 1
|
||
else:
|
||
route_dist[f"non_expert:{v['bits']}bit"] += 1
|
||
idx = _extract_layer_index(k)
|
||
label = f"L{idx}" if idx >= 0 else k.split(".")[-1]
|
||
if label not in layer_bits:
|
||
layer_bits[label] = v["bits"]
|
||
else:
|
||
layer_bits[label] = max(layer_bits[label], v["bits"])
|
||
bits_summary = ", ".join(
|
||
f"{b}bit×{c}" for b, c in sorted(bits_dist.items(), reverse=True)
|
||
)
|
||
top_layers = sorted(layer_bits.items(), key=lambda x: -x[1])[:8]
|
||
top_str = ", ".join(f"{l}={b}b" for l, b in top_layers)
|
||
route_summary = ", ".join(
|
||
f"{name}×{count}" for name, count in sorted(route_dist.items())
|
||
)
|
||
logger.info(
|
||
f" plan detail: {bits_summary} | routes: {route_summary} | "
|
||
f"top: {top_str}"
|
||
)
|
||
|
||
return QuantPlan(
|
||
boost_map=boost_map,
|
||
effective_bpw=current_bpw,
|
||
target_bpw=target_bpw,
|
||
hard_cap_bpw=hard_cap_bpw,
|
||
)
|
||
|
||
|
||
def resolve_output_name(
|
||
model_name: str,
|
||
oq_level: int,
|
||
dtype: str = "bfloat16",
|
||
preserve_mtp: bool = False,
|
||
enhanced: bool = False,
|
||
) -> str:
|
||
"""Generate output model name: strip existing quant suffixes, append oQ tag.
|
||
|
||
Appends `-fp16` suffix when dtype is float16. bfloat16 is the default and
|
||
produces no dtype suffix (backwards compatible). When preserve_mtp is True,
|
||
appends `-mtp` so the resulting name reflects that mtp.* tensors and
|
||
config fields were preserved through quantization.
|
||
|
||
Examples:
|
||
"Qwen3.5-122B-A10B" + 4 + bfloat16 -> "Qwen3.5-122B-A10B-oQ4"
|
||
"Qwen3.5-122B-A10B" + 4 + enhanced -> "Qwen3.5-122B-A10B-oQ4e"
|
||
"Qwen3.5-122B-A10B" + 4 + float16 -> "Qwen3.5-122B-A10B-oQ4-fp16"
|
||
"Qwen3.5-122B-A10B-oQ6-fp16" + 2 + bfloat16 -> "Qwen3.5-122B-A10B-oQ2"
|
||
"Qwen3.5-27B" + 4 + bfloat16 + preserve_mtp -> "Qwen3.5-27B-oQ4-mtp"
|
||
"""
|
||
pattern = re.compile(
|
||
r"-(oQ[\d.]+e?|[0-9]+[_-]?bit|fp\d+|bf\d+|mtp)$",
|
||
flags=re.IGNORECASE,
|
||
)
|
||
base = model_name
|
||
while True:
|
||
new = pattern.sub("", base)
|
||
if new == base:
|
||
break
|
||
base = new
|
||
level_str = f"{oq_level:g}"
|
||
suffix = f"-oQ{level_str}{'e' if enhanced else ''}"
|
||
if dtype == "float16":
|
||
suffix += "-fp16"
|
||
if preserve_mtp:
|
||
suffix += "-mtp"
|
||
return f"{base}{suffix}"
|
||
|
||
|
||
# ── Auto-discovery streaming sanitizer ──────────────────────────────────
|
||
|
||
|
||
class _TrackedTensor:
|
||
"""Fake tensor proxy that records shape, dtype, lineage, and transforms
|
||
applied during a sanitize() dry run. Holds no GPU data."""
|
||
|
||
__slots__ = (
|
||
"shape",
|
||
"ndim",
|
||
"dtype",
|
||
"sources",
|
||
"transform",
|
||
"axis",
|
||
"recipe",
|
||
"expr",
|
||
)
|
||
|
||
def __init__(
|
||
self,
|
||
shape,
|
||
dtype,
|
||
sources=None,
|
||
transform="passthrough",
|
||
axis=None,
|
||
recipe=None,
|
||
expr=None,
|
||
):
|
||
self.shape = tuple(shape)
|
||
self.ndim = len(self.shape)
|
||
self.dtype = dtype
|
||
self.sources = sources or []
|
||
self.transform = transform
|
||
self.axis = axis
|
||
self.recipe = list(recipe or [])
|
||
if expr is None and transform == "passthrough" and len(self.sources) == 1:
|
||
expr = ("source", self.sources[0])
|
||
self.expr = expr
|
||
|
||
def _clone(self, shape=None, dtype=None, transform=None):
|
||
new_transform = transform if transform is not None else self.transform
|
||
return _TrackedTensor(
|
||
shape if shape is not None else self.shape,
|
||
dtype if dtype is not None else self.dtype,
|
||
list(self.sources),
|
||
new_transform,
|
||
recipe=list(self.recipe),
|
||
expr=self.expr if new_transform == self.transform else None,
|
||
)
|
||
|
||
# Arithmetic — recipe is "fp8_dequant" for the whole sanitize block if weight came from FP8
|
||
def __add__(self, other):
|
||
return self._clone(transform="add")
|
||
|
||
def __radd__(self, other):
|
||
return self.__add__(other)
|
||
|
||
def __sub__(self, other):
|
||
return self._clone(transform="sub")
|
||
|
||
def __mul__(self, other):
|
||
return self._clone(transform="mul")
|
||
|
||
def __rmul__(self, other):
|
||
return self.__mul__(other)
|
||
|
||
def __truediv__(self, other):
|
||
return self._clone(transform="div")
|
||
|
||
@staticmethod
|
||
def _slice_length(dim, sl):
|
||
start, stop, step = sl.indices(dim)
|
||
return len(range(start, stop, step))
|
||
|
||
@staticmethod
|
||
def _detect_half_split(dim, sl):
|
||
start, stop, step = sl.indices(dim)
|
||
if step != 1 or dim <= 0 or dim % 2 != 0:
|
||
return None
|
||
length = len(range(start, stop, step))
|
||
if length != dim // 2:
|
||
return None
|
||
if start == 0:
|
||
return 0
|
||
if start == dim // 2:
|
||
return 1
|
||
return None
|
||
|
||
@staticmethod
|
||
def _expand_index(idx, rank):
|
||
if not isinstance(idx, tuple):
|
||
return idx
|
||
if Ellipsis not in idx:
|
||
return idx
|
||
explicit = sum(1 for p in idx if p is not Ellipsis and p is not None)
|
||
pad = max(0, rank - explicit)
|
||
expanded: list = []
|
||
seen = False
|
||
for part in idx:
|
||
if part is Ellipsis:
|
||
if seen:
|
||
raise ValueError("only one Ellipsis allowed in index")
|
||
seen = True
|
||
expanded.extend([slice(None)] * pad)
|
||
else:
|
||
expanded.append(part)
|
||
return tuple(expanded)
|
||
|
||
def _with_recipe(self, shape, transform, op, axis=None):
|
||
expr = self.as_expr()
|
||
if expr is not None:
|
||
expr = self._wrap_expr_op(expr, op)
|
||
return _TrackedTensor(
|
||
shape,
|
||
self.dtype,
|
||
list(self.sources),
|
||
transform,
|
||
axis=axis,
|
||
recipe=list(self.recipe) + [op],
|
||
expr=expr,
|
||
)
|
||
|
||
@staticmethod
|
||
def _wrap_expr_op(expr, op):
|
||
kind = op[0]
|
||
if kind == "reshape":
|
||
return ("reshape", op[1], expr)
|
||
if kind == "slice":
|
||
return ("slice", op[1], expr)
|
||
if kind == "transpose":
|
||
return ("transpose", op[1], expr)
|
||
if kind == "moveaxis":
|
||
return ("moveaxis", op[1], op[2], expr)
|
||
if kind == "astype":
|
||
return ("astype", op[1], expr)
|
||
if kind == "expand_dims":
|
||
return ("expand_dims", op[1], expr)
|
||
return None
|
||
|
||
def as_expr(self):
|
||
if self.expr is not None:
|
||
return self.expr
|
||
if self.recipe and len(self.sources) == 1:
|
||
expr = ("source", self.sources[0])
|
||
for op in self.recipe:
|
||
expr = self._wrap_expr_op(expr, op)
|
||
if expr is None:
|
||
return None
|
||
return expr
|
||
if self.transform == "passthrough" and len(self.sources) == 1:
|
||
return ("source", self.sources[0])
|
||
if self.transform == "stack":
|
||
axis = self.axis if self.axis is not None else 0
|
||
return ("stack", axis, [("source", src) for src in self.sources])
|
||
if self.transform == "concatenate":
|
||
axis = self.axis if self.axis is not None else 0
|
||
return ("concatenate", axis, [("source", src) for src in self.sources])
|
||
return None
|
||
|
||
@staticmethod
|
||
def _normalize_expand_axes(axis, ndim):
|
||
axes = (axis,) if isinstance(axis, int) else tuple(axis)
|
||
out_ndim = ndim + len(axes)
|
||
normalized = []
|
||
for ax in axes:
|
||
ax = ax + out_ndim if ax < 0 else ax
|
||
if ax < 0 or ax >= out_ndim:
|
||
raise ValueError(f"axis {ax} is out of bounds for expand_dims")
|
||
normalized.append(ax)
|
||
if len(set(normalized)) != len(normalized):
|
||
raise ValueError("repeated axis in expand_dims")
|
||
return tuple(sorted(normalized))
|
||
|
||
def expand_dims(self, axis):
|
||
axes = self._normalize_expand_axes(axis, self.ndim)
|
||
axis_set = set(axes)
|
||
src_i = 0
|
||
new_shape = []
|
||
for i in range(self.ndim + len(axes)):
|
||
if i in axis_set:
|
||
new_shape.append(1)
|
||
else:
|
||
new_shape.append(self.shape[src_i])
|
||
src_i += 1
|
||
stored_axis = axes[0] if len(axes) == 1 else axes
|
||
return self._with_recipe(
|
||
tuple(new_shape),
|
||
"expand_dims",
|
||
("expand_dims", axes),
|
||
axis=stored_axis,
|
||
)
|
||
|
||
def __getitem__(self, idx):
|
||
new_shape = list(self.shape)
|
||
idx = self._expand_index(idx, len(new_shape))
|
||
if isinstance(idx, tuple):
|
||
result_shape = []
|
||
axis = 0
|
||
split_info = None
|
||
for part in idx:
|
||
if part is None:
|
||
result_shape.append(1)
|
||
elif isinstance(part, slice):
|
||
if axis < len(new_shape):
|
||
dim = new_shape[axis]
|
||
length = self._slice_length(dim, part)
|
||
result_shape.append(length)
|
||
half = self._detect_half_split(dim, part)
|
||
if half is not None:
|
||
split_info = (axis, half, 2)
|
||
axis += 1
|
||
else:
|
||
result_shape.append(1)
|
||
else:
|
||
if axis < len(new_shape):
|
||
axis += 1
|
||
while axis < len(new_shape):
|
||
result_shape.append(new_shape[axis])
|
||
axis += 1
|
||
if split_info is not None:
|
||
ax, idx_n, total = split_info
|
||
return _TrackedTensor(
|
||
result_shape,
|
||
self.dtype,
|
||
list(self.sources),
|
||
f"split_{idx_n}_{total}",
|
||
axis=ax,
|
||
recipe=list(self.recipe) + [("slice", idx)],
|
||
)
|
||
return self._with_recipe(result_shape, "slice", ("slice", idx))
|
||
if isinstance(idx, slice):
|
||
dim = new_shape[0] if new_shape else 0
|
||
length = self._slice_length(dim, idx) if dim > 0 else 0
|
||
half = self._detect_half_split(dim, idx) if dim > 0 else None
|
||
if half is not None:
|
||
return _TrackedTensor(
|
||
[length] + new_shape[1:],
|
||
self.dtype,
|
||
list(self.sources),
|
||
f"split_{half}_2",
|
||
axis=0,
|
||
recipe=list(self.recipe) + [("slice", idx)],
|
||
)
|
||
result = list(new_shape)
|
||
if result:
|
||
result[0] = length
|
||
return self._with_recipe(result, "slice", ("slice", idx))
|
||
# int or other
|
||
if new_shape:
|
||
return self._with_recipe(new_shape[1:], "slice", ("slice", idx))
|
||
return self._with_recipe(self.shape, "slice", ("slice", idx))
|
||
|
||
def reshape(self, *new_shape):
|
||
if len(new_shape) == 1 and isinstance(new_shape[0], (tuple, list)):
|
||
new_shape = tuple(new_shape[0])
|
||
# Resolve any -1 using total element count
|
||
total = 1
|
||
for d in self.shape:
|
||
total *= d
|
||
resolved = []
|
||
unknown_idx = -1
|
||
known_prod = 1
|
||
for i, d in enumerate(new_shape):
|
||
if d == -1:
|
||
unknown_idx = i
|
||
resolved.append(-1)
|
||
else:
|
||
resolved.append(d)
|
||
known_prod *= d
|
||
if unknown_idx >= 0 and known_prod > 0:
|
||
resolved[unknown_idx] = total // known_prod
|
||
shape = tuple(resolved)
|
||
return _TrackedTensor(
|
||
shape,
|
||
self.dtype,
|
||
list(self.sources),
|
||
"reshape",
|
||
recipe=list(self.recipe) + [("reshape", shape)],
|
||
)
|
||
|
||
def astype(self, dtype):
|
||
return _TrackedTensor(
|
||
self.shape,
|
||
dtype,
|
||
list(self.sources),
|
||
"astype",
|
||
recipe=list(self.recipe) + [("astype", dtype)],
|
||
)
|
||
|
||
def moveaxis(self, src_ax, dst_ax):
|
||
src_ax = src_ax % self.ndim if src_ax < 0 else src_ax
|
||
dst_ax = dst_ax % self.ndim if dst_ax < 0 else dst_ax
|
||
dims = list(range(self.ndim))
|
||
dims.insert(dst_ax, dims.pop(src_ax))
|
||
new_shape = tuple(self.shape[d] for d in dims)
|
||
return _TrackedTensor(
|
||
new_shape,
|
||
self.dtype,
|
||
list(self.sources),
|
||
f"moveaxis_{src_ax}_{dst_ax}",
|
||
recipe=list(self.recipe) + [("moveaxis", src_ax, dst_ax)],
|
||
)
|
||
|
||
def transpose(self, *axes):
|
||
if not axes:
|
||
axes_list = list(reversed(range(self.ndim)))
|
||
elif len(axes) == 1 and isinstance(axes[0], (list, tuple)):
|
||
axes_list = list(axes[0])
|
||
else:
|
||
axes_list = list(axes)
|
||
axes_list = [a % self.ndim if a < 0 else a for a in axes_list]
|
||
new_shape = tuple(self.shape[a] for a in axes_list)
|
||
return _TrackedTensor(
|
||
new_shape,
|
||
self.dtype,
|
||
list(self.sources),
|
||
"transpose_" + "_".join(str(a) for a in axes_list),
|
||
recipe=list(self.recipe) + [("transpose", tuple(axes_list))],
|
||
)
|
||
|
||
def swapaxes(self, axis1, axis2):
|
||
axes_list = list(range(self.ndim))
|
||
axis1 = axis1 % self.ndim if axis1 < 0 else axis1
|
||
axis2 = axis2 % self.ndim if axis2 < 0 else axis2
|
||
axes_list[axis1], axes_list[axis2] = axes_list[axis2], axes_list[axis1]
|
||
return self.transpose(axes_list)
|
||
|
||
@property
|
||
def T(self):
|
||
axes = tuple(reversed(range(self.ndim)))
|
||
return _TrackedTensor(
|
||
tuple(reversed(self.shape)),
|
||
self.dtype,
|
||
list(self.sources),
|
||
"transpose",
|
||
recipe=list(self.recipe) + [("transpose", axes)],
|
||
)
|
||
|
||
@property
|
||
def size(self):
|
||
r = 1
|
||
for d in self.shape:
|
||
r *= d
|
||
return r
|
||
|
||
|
||
_FP8_WEIGHT_DTYPES = frozenset(("F8_E4M3", "F8_E5M2", "I8"))
|
||
|
||
|
||
def _block_dequant_fp8(weight_raw, scale_raw, w_dtype, s_dtype):
|
||
"""Block-scaled dequant of a single FP8/I8 weight+scale pair to BF16."""
|
||
# MXFP8 checkpoints (e.g. MiniMax-M3) save the e8m0 shared exponents
|
||
# with safetensors dtype U8: fp8 weight, one scale byte per 32 columns.
|
||
# Those bytes are exponents, not linear scales. Float-typed scales
|
||
# (DeepSeek block-128 weight_scale_inv) stay linear.
|
||
e8m0 = s_dtype == "F8_E8M0" or (
|
||
s_dtype in ("U8", "UINT8")
|
||
and w_dtype in ("F8_E4M3", "F8_E5M2")
|
||
and scale_raw.ndim == 2
|
||
and weight_raw.shape[-1] == scale_raw.shape[-1] * 32
|
||
)
|
||
if e8m0:
|
||
scale = mx.power(mx.array(2.0), scale_raw.astype(mx.float32) - 127.0)
|
||
else:
|
||
scale = scale_raw
|
||
|
||
if w_dtype in ("F8_E4M3", "F8_E5M2"):
|
||
weight = mx.from_fp8(weight_raw, dtype=mx.bfloat16)
|
||
else:
|
||
weight = weight_raw.astype(mx.bfloat16)
|
||
|
||
m, n = weight.shape
|
||
sm, sn = scale.shape
|
||
if sm == 0 or sn == 0:
|
||
raise ValueError(f"degenerate scale shape {scale.shape}")
|
||
|
||
def _infer_block(dim: int, blocks: int) -> int | None:
|
||
if dim % blocks == 0:
|
||
return dim // blocks
|
||
for block in (128, 64, 32, 256, 16, 8):
|
||
if (dim + block - 1) // block == blocks:
|
||
return block
|
||
return None
|
||
|
||
bs_row = _infer_block(m, sm)
|
||
bs_col = _infer_block(n, sn)
|
||
if bs_row is None or bs_col is None:
|
||
raise ValueError(
|
||
f"weight shape ({m},{n}) not divisible by scale shape ({sm},{sn})"
|
||
)
|
||
|
||
if bs_row > 1:
|
||
target_m = sm * bs_row
|
||
target_n = sn * bs_col
|
||
pad_bottom = max(0, target_m - m)
|
||
pad_side = max(0, target_n - n)
|
||
if pad_bottom or pad_side:
|
||
weight = mx.pad(weight, ((0, pad_bottom), (0, pad_side)))
|
||
weight = weight.reshape(sm, bs_row, sn, bs_col)
|
||
weight = (weight * scale[:, None, :, None]).reshape(
|
||
m + pad_bottom, n + pad_side
|
||
)
|
||
if pad_bottom or pad_side:
|
||
weight = weight[:m, :n]
|
||
else:
|
||
weight = weight.reshape(m, sn, bs_col)
|
||
weight = (weight * scale[:, :, None]).reshape(m, n)
|
||
|
||
weight = weight.astype(mx.bfloat16)
|
||
mx.eval(weight)
|
||
return weight
|
||
|
||
|
||
def _discover_sanitize_plan(sanitize_fn, lazy_index):
|
||
"""Run sanitize on _TrackedTensors to discover the key mapping and
|
||
transforms without materializing any real data.
|
||
|
||
Returns a dict: output_key -> {sources, transform, shape, axis}
|
||
or None if discovery fails.
|
||
"""
|
||
import mlx.core as mx
|
||
|
||
# Build tracked dict mirroring the lazy index (logical view hides scale
|
||
# keys and reports FP8 weights as BF16 so sanitize won't call from_fp8)
|
||
tracked = {}
|
||
initial_meta = {}
|
||
if hasattr(lazy_index, "logical_metadata"):
|
||
logical = lazy_index.logical_metadata()
|
||
for k, (shape, dtype) in logical.items():
|
||
tracked[k] = _TrackedTensor(shape, dtype, sources=[k])
|
||
initial_meta[k] = (tuple(shape), dtype)
|
||
else:
|
||
for k in lazy_index._index:
|
||
meta = lazy_index._index[k]
|
||
shape, dtype = meta[4], meta[5]
|
||
tracked[k] = _TrackedTensor(shape, dtype, sources=[k])
|
||
initial_meta[k] = (tuple(shape), dtype)
|
||
|
||
# Monkey-patch mx ops to work on tracked tensors
|
||
_orig = {
|
||
"stack": mx.stack,
|
||
"concatenate": mx.concatenate,
|
||
"split": mx.split,
|
||
"eval": mx.eval,
|
||
"clear_cache": mx.clear_cache,
|
||
"synchronize": mx.synchronize,
|
||
"moveaxis": mx.moveaxis,
|
||
"transpose": mx.transpose,
|
||
"swapaxes": getattr(mx, "swapaxes", None),
|
||
"expand_dims": mx.expand_dims,
|
||
"contiguous": getattr(mx, "contiguous", None),
|
||
"from_fp8": getattr(mx, "from_fp8", None),
|
||
"pad": getattr(mx, "pad", None),
|
||
}
|
||
|
||
def _is_plain_source(tensor):
|
||
return (
|
||
isinstance(tensor, _TrackedTensor)
|
||
and tensor.transform == "passthrough"
|
||
and not tensor.recipe
|
||
and len(tensor.sources) == 1
|
||
)
|
||
|
||
def _fake_stack(tensors, axis=0):
|
||
if tensors and isinstance(tensors[0], _TrackedTensor):
|
||
n = len(tensors)
|
||
base = list(tensors[0].shape)
|
||
axis = axis + len(base) + 1 if axis < 0 else axis
|
||
new_shape = base[:axis] + [n] + base[axis:]
|
||
all_src = []
|
||
for t in tensors:
|
||
all_src.extend(t.sources)
|
||
if not all(_is_plain_source(t) for t in tensors):
|
||
exprs = [t.as_expr() for t in tensors]
|
||
if any(expr is None for expr in exprs):
|
||
return _TrackedTensor(
|
||
new_shape,
|
||
tensors[0].dtype,
|
||
all_src,
|
||
"nested_unreplayable",
|
||
)
|
||
return _TrackedTensor(
|
||
new_shape,
|
||
tensors[0].dtype,
|
||
all_src,
|
||
"expr",
|
||
axis=axis,
|
||
expr=("stack", axis, exprs),
|
||
)
|
||
return _TrackedTensor(
|
||
new_shape, tensors[0].dtype, all_src, "stack", axis=axis
|
||
)
|
||
return _orig["stack"](tensors, axis=axis)
|
||
|
||
def _fake_concatenate(tensors, axis=0):
|
||
if tensors and isinstance(tensors[0], _TrackedTensor):
|
||
all_src = []
|
||
for t in tensors:
|
||
all_src.extend(t.sources)
|
||
base = list(tensors[0].shape)
|
||
axis = axis + len(base) if axis < 0 else axis
|
||
base[axis] = sum(t.shape[axis] for t in tensors)
|
||
if not all(_is_plain_source(t) for t in tensors):
|
||
exprs = [t.as_expr() for t in tensors]
|
||
if any(expr is None for expr in exprs):
|
||
return _TrackedTensor(
|
||
base,
|
||
tensors[0].dtype,
|
||
all_src,
|
||
"nested_unreplayable",
|
||
)
|
||
return _TrackedTensor(
|
||
base,
|
||
tensors[0].dtype,
|
||
all_src,
|
||
"expr",
|
||
axis=axis,
|
||
expr=("concatenate", axis, exprs),
|
||
)
|
||
return _TrackedTensor(
|
||
base, tensors[0].dtype, all_src, "concatenate", axis=axis
|
||
)
|
||
return _orig["concatenate"](tensors, axis=axis)
|
||
|
||
def _fake_split(tensor, indices_or_sections, axis=0):
|
||
if isinstance(tensor, _TrackedTensor):
|
||
if isinstance(indices_or_sections, int):
|
||
n = indices_or_sections
|
||
sz = tensor.shape[axis] // n
|
||
parts = []
|
||
for i in range(n):
|
||
sh = list(tensor.shape)
|
||
sh[axis] = sz
|
||
parts.append(
|
||
_TrackedTensor(
|
||
sh,
|
||
tensor.dtype,
|
||
list(tensor.sources),
|
||
f"split_{i}_{n}",
|
||
axis=axis,
|
||
)
|
||
)
|
||
return parts
|
||
# list of indices
|
||
parts = []
|
||
prev = 0
|
||
idxs = list(indices_or_sections) + [tensor.shape[axis]]
|
||
for i, idx in enumerate(idxs):
|
||
sh = list(tensor.shape)
|
||
sh[axis] = idx - prev
|
||
parts.append(
|
||
_TrackedTensor(
|
||
sh, tensor.dtype, list(tensor.sources), f"split_{i}", axis=axis
|
||
)
|
||
)
|
||
prev = idx
|
||
return parts
|
||
return _orig["split"](tensor, indices_or_sections, axis=axis)
|
||
|
||
def _fake_moveaxis(tensor, src_ax, dst_ax):
|
||
if isinstance(tensor, _TrackedTensor):
|
||
return tensor.moveaxis(src_ax, dst_ax)
|
||
return _orig["moveaxis"](tensor, src_ax, dst_ax)
|
||
|
||
def _fake_transpose(tensor, axes=None):
|
||
if isinstance(tensor, _TrackedTensor):
|
||
if axes is None:
|
||
axes = list(reversed(range(tensor.ndim)))
|
||
return tensor.transpose(tuple(axes))
|
||
return _orig["transpose"](tensor, axes=axes)
|
||
|
||
def _fake_swapaxes(tensor, axis1, axis2):
|
||
if isinstance(tensor, _TrackedTensor):
|
||
return tensor.swapaxes(axis1, axis2)
|
||
return _orig["swapaxes"](tensor, axis1, axis2)
|
||
|
||
def _fake_expand_dims(tensor, axis, **kwargs):
|
||
if isinstance(tensor, _TrackedTensor):
|
||
return tensor.expand_dims(axis)
|
||
return _orig["expand_dims"](tensor, axis=axis, **kwargs)
|
||
|
||
def _fake_contiguous(tensor, *args, **kwargs):
|
||
if isinstance(tensor, _TrackedTensor):
|
||
return tensor
|
||
return _orig["contiguous"](tensor, *args, **kwargs)
|
||
|
||
def _noop(*a, **kw):
|
||
pass
|
||
|
||
mx.stack = _fake_stack
|
||
mx.concatenate = _fake_concatenate
|
||
mx.split = _fake_split
|
||
mx.eval = _noop
|
||
mx.clear_cache = _noop
|
||
mx.synchronize = _noop
|
||
mx.moveaxis = _fake_moveaxis
|
||
mx.transpose = _fake_transpose
|
||
mx.expand_dims = _fake_expand_dims
|
||
if _orig["swapaxes"] is not None:
|
||
mx.swapaxes = _fake_swapaxes
|
||
if _orig["contiguous"] is not None:
|
||
mx.contiguous = _fake_contiguous
|
||
|
||
def _fake_from_fp8(x, dtype=None, **kw):
|
||
if isinstance(x, _TrackedTensor):
|
||
return _TrackedTensor(
|
||
x.shape, dtype or x.dtype, list(x.sources), "from_fp8"
|
||
)
|
||
return _orig["from_fp8"](x, dtype=dtype, **kw) if _orig["from_fp8"] else x
|
||
|
||
def _fake_pad(x, pad_width, **kw):
|
||
if isinstance(x, _TrackedTensor):
|
||
new_shape = []
|
||
for i, d in enumerate(x.shape):
|
||
if i < len(pad_width):
|
||
lo, hi = (
|
||
pad_width[i]
|
||
if isinstance(pad_width[i], (tuple, list))
|
||
else (pad_width[i], pad_width[i])
|
||
)
|
||
new_shape.append(d + lo + hi)
|
||
else:
|
||
new_shape.append(d)
|
||
return _TrackedTensor(new_shape, x.dtype, list(x.sources), "pad")
|
||
return _orig["pad"](x, pad_width, **kw) if _orig["pad"] else x
|
||
|
||
if _orig["from_fp8"] is not None:
|
||
mx.from_fp8 = _fake_from_fp8
|
||
if _orig["pad"] is not None:
|
||
mx.pad = _fake_pad
|
||
|
||
try:
|
||
result = sanitize_fn(tracked)
|
||
finally:
|
||
for name, fn in _orig.items():
|
||
if fn is not None:
|
||
setattr(mx, name, fn)
|
||
|
||
# Extract plan
|
||
_REPLAYABLE_PREFIXES = (
|
||
"passthrough",
|
||
"literal",
|
||
"stack",
|
||
"concatenate",
|
||
"add",
|
||
"add_if_mean_lt_0_5",
|
||
"transpose_",
|
||
"moveaxis_",
|
||
"split_",
|
||
"slice",
|
||
"reshape",
|
||
"astype",
|
||
"expand_dims",
|
||
"expr",
|
||
)
|
||
plan = {}
|
||
for k, v in result.items():
|
||
if isinstance(v, _TrackedTensor):
|
||
t = v.transform
|
||
if not any(t == p or t.startswith(p) for p in _REPLAYABLE_PREFIXES):
|
||
raise ValueError(
|
||
f"non-replayable transform {t!r} for {k!r} — "
|
||
"falling back to eager sanitize"
|
||
)
|
||
plan[k] = {
|
||
"sources": v.sources,
|
||
"transform": t,
|
||
"shape": v.shape,
|
||
"axis": v.axis,
|
||
"recipe": list(v.recipe),
|
||
}
|
||
if v.transform == "expr":
|
||
if v.expr is None:
|
||
raise ValueError(
|
||
f"missing replay expression for {k!r} — "
|
||
"falling back to eager sanitize"
|
||
)
|
||
plan[k]["expr"] = v.expr
|
||
if v.recipe:
|
||
if len(v.sources) != 1:
|
||
raise ValueError(
|
||
f"recipe with non-trivial sources for {k!r} — "
|
||
"falling back to eager sanitize"
|
||
)
|
||
elif t in ("reshape", "astype"):
|
||
# Only the LAST transform is tracked, so replay is sound
|
||
# only when nothing else touched the tensor: an astype must
|
||
# keep the source shape and a reshape must keep the source
|
||
# dtype. Chains (e.g. reshape-then-astype) fall back to
|
||
# eager sanitize, matching the pre-replay behavior.
|
||
src_meta = (
|
||
initial_meta.get(v.sources[0]) if len(v.sources) == 1 else None
|
||
)
|
||
if src_meta is None:
|
||
raise ValueError(
|
||
f"{t} with non-trivial sources for {k!r} — "
|
||
"falling back to eager sanitize"
|
||
)
|
||
if t == "astype" and tuple(v.shape) != src_meta[0]:
|
||
raise ValueError(
|
||
f"astype after a shape-changing op for {k!r} — "
|
||
"falling back to eager sanitize"
|
||
)
|
||
if t == "reshape" and v.dtype != src_meta[1]:
|
||
raise ValueError(
|
||
f"reshape after a dtype-changing op for {k!r} — "
|
||
"falling back to eager sanitize"
|
||
)
|
||
if t == "astype":
|
||
# _TrackedTensor.astype records the target mx dtype.
|
||
plan[k]["dtype"] = v.dtype
|
||
else:
|
||
plan[k] = {
|
||
"sources": [],
|
||
"transform": "literal",
|
||
"shape": getattr(v, "shape", ()),
|
||
"axis": None,
|
||
"value": v,
|
||
}
|
||
|
||
return plan
|
||
|
||
|
||
class _DiscoveredPlan:
|
||
"""Dict-like wrapper that materializes tensors one at a time using
|
||
a plan discovered by _discover_sanitize_plan. Supports chunked
|
||
stacking for huge MoE expert tensors."""
|
||
|
||
_STACK_CHUNK = 16 # experts per chunk during materialization
|
||
|
||
def __init__(self, plan, lazy_index):
|
||
self._plan = plan # output_key -> {sources, transform, ...}
|
||
self._lazy = lazy_index
|
||
self._cache = {} # output_key -> mx.array (for multi-consumer sources)
|
||
|
||
def keys(self):
|
||
return self._plan.keys()
|
||
|
||
def __len__(self):
|
||
return len(self._plan)
|
||
|
||
def __contains__(self, k):
|
||
return k in self._plan
|
||
|
||
def __iter__(self):
|
||
return iter(self._plan)
|
||
|
||
def items(self):
|
||
# Yield (key, shape_proxy) for the quantize loop shape inspection
|
||
class _SP:
|
||
__slots__ = ("shape", "ndim")
|
||
|
||
def __init__(self, sh):
|
||
self.shape = tuple(sh)
|
||
self.ndim = len(self.shape)
|
||
|
||
return ((k, _SP(info["shape"])) for k, info in self._plan.items())
|
||
|
||
def nbytes(self):
|
||
return self._lazy.nbytes()
|
||
|
||
def plan_shape(self, key):
|
||
"""Logical output shape for a planned key without materializing."""
|
||
return tuple(self._plan[key]["shape"])
|
||
|
||
def source_quant_info(self, key):
|
||
"""Common pre-quantized source metadata for an output key, or None.
|
||
|
||
Only meaningful for transforms that preserve the packed layout
|
||
(passthrough, stack, single-source reshape) where every source is
|
||
the same passthrough-capable format.
|
||
"""
|
||
info = self._plan.get(key)
|
||
if info is None or not hasattr(self._lazy, "source_quant_info"):
|
||
return None
|
||
transform = info["transform"]
|
||
sources = info["sources"]
|
||
recipe = info.get("recipe") or []
|
||
if not sources:
|
||
return None
|
||
if recipe and not (
|
||
transform == "reshape" and len(recipe) == 1 and recipe[0][0] == "reshape"
|
||
):
|
||
return None
|
||
if transform not in ("passthrough", "stack") and not (
|
||
transform == "reshape" and len(sources) == 1
|
||
):
|
||
return None
|
||
first = self._lazy.source_quant_info(sources[0])
|
||
if first is None:
|
||
return None
|
||
for src in sources[1:]:
|
||
if self._lazy.source_quant_info(src) != first:
|
||
return None
|
||
return first
|
||
|
||
def pop_packed(self, key):
|
||
"""Materialize a pre-quantized output tensor in mlx packed form.
|
||
|
||
Returns (weight, scales). Only valid when source_quant_info(key)
|
||
returned a dict; consumes the plan entry like pop().
|
||
"""
|
||
info = self._plan.pop(key)
|
||
transform = info["transform"]
|
||
sources = info["sources"]
|
||
|
||
if transform == "passthrough":
|
||
return self._lazy._load_packed(sources[0])
|
||
|
||
if transform == "reshape":
|
||
w, s = self._lazy._load_packed(sources[0])
|
||
lead = tuple(info["shape"][:-1])
|
||
return mx.reshape(w, lead + (-1,)), mx.reshape(s, lead + (-1,))
|
||
|
||
if transform == "stack":
|
||
axis = info.get("axis", 0)
|
||
chunk = self._STACK_CHUNK
|
||
w_parts, s_parts = [], []
|
||
for base in range(0, len(sources), chunk):
|
||
w_piece, s_piece = [], []
|
||
for src in sources[base : base + chunk]:
|
||
w, s = self._lazy._load_packed(src)
|
||
w_piece.append(w)
|
||
s_piece.append(s)
|
||
w_stk = mx.stack(w_piece, axis=axis)
|
||
s_stk = mx.stack(s_piece, axis=axis)
|
||
mx.eval(w_stk, s_stk)
|
||
del w_piece, s_piece
|
||
mx.clear_cache()
|
||
w_parts.append(w_stk)
|
||
s_parts.append(s_stk)
|
||
if len(w_parts) == 1:
|
||
return w_parts[0], s_parts[0]
|
||
w_res = mx.concatenate(w_parts, axis=axis)
|
||
s_res = mx.concatenate(s_parts, axis=axis)
|
||
mx.eval(w_res, s_res)
|
||
del w_parts, s_parts
|
||
mx.clear_cache()
|
||
return w_res, s_res
|
||
|
||
raise ValueError(f"cannot materialize packed {key!r}: transform={transform}")
|
||
|
||
def _materialize_source(self, src_key):
|
||
"""Load a single source tensor from the lazy index."""
|
||
if hasattr(self._lazy, "_fp8_pairs") and src_key in self._lazy._fp8_pairs:
|
||
return self._lazy._dequant_one(src_key)
|
||
meta = self._lazy._index.get(src_key)
|
||
if meta is None:
|
||
raise KeyError(f"source tensor {src_key!r} not in lazy index")
|
||
sf_path, data_offset, start, end, shape, dtype = meta
|
||
if len(shape) == 0:
|
||
import numpy as _np
|
||
|
||
with open(sf_path, "rb") as f:
|
||
f.seek(data_offset + start)
|
||
raw = f.read(end - start)
|
||
lt_tmp = _LazyTensor(sf_path, data_offset, start, end, (1,), dtype)
|
||
np_view = _np.frombuffer(raw, dtype=lt_tmp._np_view_dtype())
|
||
arr = mx.array(np_view).view(lt_tmp._mlx_dtype()).reshape(())
|
||
mx.eval(arr)
|
||
return arr
|
||
lt = _LazyTensor(sf_path, data_offset, start, end, shape, dtype)
|
||
arr = lt[:]
|
||
mx.eval(arr)
|
||
return arr
|
||
|
||
@staticmethod
|
||
def _apply_recipe(arr, recipe):
|
||
for op in recipe:
|
||
kind = op[0]
|
||
if kind == "reshape":
|
||
arr = mx.reshape(arr, op[1])
|
||
elif kind == "slice":
|
||
arr = arr[op[1]]
|
||
elif kind == "transpose":
|
||
arr = mx.transpose(arr, axes=op[1])
|
||
elif kind == "moveaxis":
|
||
arr = mx.moveaxis(arr, op[1], op[2])
|
||
elif kind == "astype":
|
||
arr = arr.astype(op[1])
|
||
elif kind == "expand_dims":
|
||
arr = mx.expand_dims(arr, axis=op[1])
|
||
else:
|
||
raise ValueError(f"unsupported replay recipe op: {kind}")
|
||
mx.eval(arr)
|
||
return arr
|
||
|
||
def _materialize_expr(self, expr):
|
||
kind = expr[0]
|
||
|
||
if kind == "source":
|
||
return self._materialize_source(expr[1])
|
||
|
||
if kind == "stack":
|
||
axis = expr[1]
|
||
children = expr[2]
|
||
chunk = self._STACK_CHUNK
|
||
partials = []
|
||
for base in range(0, len(children), chunk):
|
||
piece = [
|
||
self._materialize_expr(c) for c in children[base : base + chunk]
|
||
]
|
||
stk = mx.stack(piece, axis=axis)
|
||
mx.eval(stk)
|
||
del piece
|
||
mx.clear_cache()
|
||
partials.append(stk)
|
||
if len(partials) == 1:
|
||
return partials[0]
|
||
result = mx.concatenate(partials, axis=axis)
|
||
mx.eval(result)
|
||
del partials
|
||
mx.clear_cache()
|
||
return result
|
||
|
||
child_kinds = {"reshape", "slice", "transpose", "expand_dims", "astype"}
|
||
if kind in child_kinds:
|
||
arr = self._materialize_expr(expr[2])
|
||
if kind == "reshape":
|
||
result = mx.reshape(arr, expr[1])
|
||
elif kind == "slice":
|
||
result = arr[expr[1]]
|
||
elif kind == "transpose":
|
||
result = mx.transpose(arr, axes=expr[1])
|
||
elif kind == "expand_dims":
|
||
result = mx.expand_dims(arr, axis=expr[1])
|
||
else:
|
||
result = arr.astype(expr[1])
|
||
mx.eval(result)
|
||
return result
|
||
|
||
if kind == "moveaxis":
|
||
arr = self._materialize_expr(expr[3])
|
||
result = mx.moveaxis(arr, expr[1], expr[2])
|
||
mx.eval(result)
|
||
return result
|
||
|
||
if kind == "concatenate":
|
||
axis = expr[1]
|
||
parts = [self._materialize_expr(c) for c in expr[2]]
|
||
result = mx.concatenate(parts, axis=axis)
|
||
mx.eval(result)
|
||
del parts
|
||
mx.clear_cache()
|
||
return result
|
||
|
||
raise ValueError(f"unsupported replay expression op: {kind}")
|
||
|
||
def pop(self, key, *default):
|
||
if key not in self._plan:
|
||
if default:
|
||
return default[0]
|
||
raise KeyError(key)
|
||
|
||
info = self._plan.pop(key)
|
||
transform = info["transform"]
|
||
sources = info["sources"]
|
||
recipe = info.get("recipe") or []
|
||
|
||
if transform == "literal":
|
||
return info["value"]
|
||
|
||
if transform == "expr":
|
||
return self._materialize_expr(info["expr"])
|
||
|
||
if recipe and len(sources) == 1:
|
||
arr = self._materialize_source(sources[0])
|
||
return self._apply_recipe(arr, recipe)
|
||
|
||
if transform == "passthrough" and len(sources) == 1:
|
||
arr = self._materialize_source(sources[0])
|
||
return arr
|
||
|
||
if transform == "stack":
|
||
# Chunked stacking to bound peak memory
|
||
axis = info.get("axis", 0)
|
||
chunk = self._STACK_CHUNK
|
||
partials = []
|
||
for base in range(0, len(sources), chunk):
|
||
piece = []
|
||
for src in sources[base : base + chunk]:
|
||
piece.append(self._materialize_source(src))
|
||
stk = mx.stack(piece, axis=axis)
|
||
mx.eval(stk)
|
||
del piece
|
||
mx.clear_cache()
|
||
partials.append(stk)
|
||
if len(partials) == 1:
|
||
return partials[0]
|
||
result = mx.concatenate(partials, axis=axis)
|
||
mx.eval(result)
|
||
del partials
|
||
mx.clear_cache()
|
||
return result
|
||
|
||
if transform == "concatenate":
|
||
axis = info.get("axis", 0)
|
||
parts = [self._materialize_source(src) for src in sources]
|
||
result = mx.concatenate(parts, axis=axis)
|
||
mx.eval(result)
|
||
del parts
|
||
mx.clear_cache()
|
||
return result
|
||
|
||
if transform == "add":
|
||
arr = self._materialize_source(sources[0])
|
||
return arr + 1.0 # norm weight += 1.0 pattern
|
||
|
||
if transform == "add_if_mean_lt_0_5":
|
||
arr = self._materialize_source(sources[0])
|
||
mean = float(mx.mean(arr.astype(mx.float32)).item())
|
||
if mean < 0.5:
|
||
return arr + 1.0
|
||
return arr
|
||
|
||
if transform == "reshape":
|
||
arr = self._materialize_source(sources[0])
|
||
return mx.reshape(arr, info["shape"])
|
||
|
||
if transform == "astype":
|
||
arr = self._materialize_source(sources[0])
|
||
return arr.astype(info["dtype"])
|
||
|
||
if transform.startswith("transpose_"):
|
||
axes = [int(a) for a in transform.split("_")[1:]]
|
||
arr = self._materialize_source(sources[0])
|
||
return mx.transpose(arr, axes=axes)
|
||
|
||
if transform.startswith("moveaxis_"):
|
||
parts = transform.split("_")
|
||
src_ax, dst_ax = int(parts[1]), int(parts[2])
|
||
arr = self._materialize_source(sources[0])
|
||
return mx.moveaxis(arr, src_ax, dst_ax)
|
||
|
||
if "split_" in transform:
|
||
# split_N_M means take part N of M
|
||
parts = transform.split("_")
|
||
arr = self._materialize_source(sources[0])
|
||
axis = info.get("axis", 0)
|
||
if len(parts) == 3: # split_idx_total
|
||
idx, total = int(parts[1]), int(parts[2])
|
||
chunks = mx.split(arr, total, axis=axis)
|
||
result = chunks[idx]
|
||
mx.eval(result)
|
||
del arr, chunks
|
||
mx.clear_cache()
|
||
return result
|
||
# split_idx (index-based split) — less common
|
||
return arr
|
||
|
||
if transform == "slice":
|
||
raise ValueError(
|
||
f"cannot replay arbitrary slice for {key!r} — "
|
||
"discovery should fall back to eager sanitize"
|
||
)
|
||
|
||
# Fallback: passthrough (identity) — load first source unchanged
|
||
if transform == "passthrough" and sources:
|
||
return self._materialize_source(sources[0])
|
||
|
||
raise ValueError(
|
||
f"cannot materialize {key!r}: transform={transform}, no sources"
|
||
)
|
||
|
||
|
||
def _is_qat_unquantized_config(qc) -> bool:
|
||
"""Return True if qc is a QAT training config with full-precision weights.
|
||
|
||
Gemma 4 QAT configs carry quant_type (e.g. "q4_0") recording the training
|
||
regime but store weights in bfloat16 — no quant_method means no actual
|
||
weight quantization has been applied.
|
||
"""
|
||
return (
|
||
isinstance(qc, dict)
|
||
and qc.get("quant_type") == "q4_0"
|
||
and "quant_method" not in qc
|
||
)
|
||
|
||
|
||
def validate_quantizable(config: dict) -> bool:
|
||
"""Check if a model config indicates it can be quantized.
|
||
|
||
Models with 'quantization' key (mlx-lm quantized) are excluded.
|
||
Models with 'quantization_config' are excluded UNLESS they are native FP8
|
||
(e.g. MiniMax, DeepSeek) which are full-precision models stored in FP8 format,
|
||
or QAT-trained models (e.g. Google Gemma 4 QAT variants) whose
|
||
quantization_config records training-time settings but whose weights are
|
||
stored in full precision (bfloat16/float16).
|
||
"""
|
||
if "quantization" in config:
|
||
return False
|
||
if "quantization_config" in config:
|
||
qc = config["quantization_config"]
|
||
if isinstance(qc, dict):
|
||
quant_method = qc.get("quant_method", "")
|
||
# FP8 models are full-precision weights stored in FP8 format
|
||
if quant_method == "fp8":
|
||
return True
|
||
# QAT models record training-time quant_type but weights are fp16/bf16
|
||
if _is_qat_unquantized_config(qc):
|
||
return True
|
||
return False
|
||
return True
|
||
|
||
|
||
def _sensitivity_lm_config_override(config: dict) -> dict | None:
|
||
"""Return a model_config override for mlx_lm.load when the model has a
|
||
QAT quantization_config that mlx-lm cannot process (missing quant_method).
|
||
|
||
mlx-lm does ``quantization_config["quant_method"]`` without a fallback, so
|
||
QAT configs (e.g. Google Gemma 4 QAT) raise KeyError and abort the load.
|
||
Passing ``{"quantization_config": None}`` via model_config causes
|
||
config.update() to replace the offending key before that branch runs.
|
||
"""
|
||
for qc in (
|
||
config.get("quantization_config"),
|
||
config.get("text_config", {}).get("quantization_config"),
|
||
):
|
||
if _is_qat_unquantized_config(qc):
|
||
return {"quantization_config": None}
|
||
return None
|
||
|
||
|
||
def make_predicate(config: dict, oq_level: int = 4) -> Callable:
|
||
"""Create a quant_predicate closure for mlx-lm's quantize_model."""
|
||
|
||
def predicate(path: str, module) -> Union[bool, dict]:
|
||
return universal_quant_predicate(path, module, config, oq_level)
|
||
|
||
return predicate
|
||
|
||
|
||
def estimate_bpw_and_size(
|
||
model_path: str,
|
||
oq_level: int,
|
||
group_size: int = 64,
|
||
preserve_mtp: bool = False,
|
||
) -> dict:
|
||
"""Calculate precise effective bpw and output size by scanning actual tensors.
|
||
|
||
Applies the universal predicate to each tensor to determine its bit width,
|
||
then computes weighted average bpw and estimated output size.
|
||
|
||
Args:
|
||
model_path: Path to source model directory.
|
||
preserve_mtp: When True, mtp.* tensors are kept (counted toward
|
||
output size) instead of being skipped. Mirrors the matching
|
||
argument in ``quantize_oq_streaming``.
|
||
oq_level: Target oQ level (base bits).
|
||
group_size: Quantization group size.
|
||
|
||
Returns:
|
||
Dict with effective_bpw, output_size_bytes, output_size_formatted.
|
||
"""
|
||
source = Path(model_path)
|
||
config_path = source / "config.json"
|
||
with open(config_path) as f:
|
||
config = json.load(f)
|
||
|
||
weight_files = sorted(source.glob("*.safetensors"))
|
||
if not weight_files:
|
||
return {
|
||
"effective_bpw": float(oq_level),
|
||
"output_size_bytes": 0,
|
||
"output_size_formatted": "?",
|
||
}
|
||
|
||
if preserve_mtp:
|
||
from omlx.utils.model_loading import _checkpoint_has_mtp_weights
|
||
|
||
if not _checkpoint_has_mtp_weights(source):
|
||
preserve_mtp = False
|
||
|
||
# Header-only scan: shapes/dtypes come from the safetensors headers, so
|
||
# checkpoints with dtypes mx.load rejects (F8_E8M0 block scales) still
|
||
# estimate. The logical view hides .scale companions and reports
|
||
# pre-quantized weights at their unpacked logical shape.
|
||
idx = _LazyTensorIndex(weight_files)
|
||
logical = idx.logical_metadata()
|
||
|
||
named_shapes = {}
|
||
for name, (shape, _dtype) in logical.items():
|
||
norm = _normalize_quant_path(name)
|
||
if name == f"{norm}.weight" and len(shape) >= 2:
|
||
named_shapes[norm] = tuple(shape)
|
||
|
||
# Match quantize_oq_streaming: the budget-plan flag must be set BEFORE
|
||
# any predicate evaluation so the fixed-override floors here agree with
|
||
# the per-tensor pricing loop below (the flag changes which predicate
|
||
# branch answers).
|
||
config["_oq_use_budget_plan"] = oq_level in _OQ_BPW_TARGETS
|
||
|
||
# Pre-quantized tensors that pass through in source precision (mirrors
|
||
# the decision in quantize_oq_streaming, evaluated pre-boost).
|
||
fixed_overrides = {}
|
||
_pre_boost_config = {**config, "_oq_boost_map": {}}
|
||
for _path in named_shapes:
|
||
_info = idx.source_quant_info(f"{_path}.weight")
|
||
if _info is None:
|
||
continue
|
||
_floor_bits, _, _ = _get_predicate_bits(
|
||
f"{_path}.weight", _pre_boost_config, oq_level, group_size
|
||
)
|
||
if _floor_bits is not None and _floor_bits >= _info["bits"]:
|
||
fixed_overrides[_path] = {
|
||
"bits": _info["bits"],
|
||
"group_size": _info["group_size"],
|
||
"mode": _info["mode"],
|
||
}
|
||
|
||
# Build budget plan for accurate estimate (position-based sensitivity)
|
||
_level_targets = _bpw_targets_for_level(oq_level)
|
||
if _level_targets is not None:
|
||
tc = config.get("text_config", {})
|
||
num_layers = config.get("num_hidden_layers") or tc.get("num_hidden_layers", 32)
|
||
pos_sens = {}
|
||
for i in range(num_layers):
|
||
if i < num_layers // 8 or i >= 7 * num_layers // 8:
|
||
pos_sens[str(i)] = 0.05
|
||
elif i < num_layers // 4 or i >= 3 * num_layers // 4:
|
||
pos_sens[str(i)] = 0.02
|
||
else:
|
||
pos_sens[str(i)] = 0.01
|
||
config["_oq_sensitivity_map"] = pos_sens
|
||
|
||
plan = _build_quant_plan(
|
||
named_shapes,
|
||
config,
|
||
oq_level,
|
||
target_bpw=_level_targets[0],
|
||
hard_cap_bpw=_level_targets[1],
|
||
fixed_overrides=fixed_overrides,
|
||
)
|
||
config["_oq_boost_map"] = plan.boost_map
|
||
else:
|
||
config["_oq_boost_map"] = {}
|
||
|
||
total_params = 0
|
||
total_weighted_bits = 0
|
||
total_output_bytes = 0
|
||
|
||
for name, (shape, _dtype) in logical.items():
|
||
n_elements = 1
|
||
for d in shape:
|
||
n_elements *= d
|
||
|
||
if _should_skip_tensor(name, preserve_mtp=preserve_mtp):
|
||
continue
|
||
|
||
if not _should_quantize_tensor(name, shape):
|
||
total_params += n_elements
|
||
total_weighted_bits += n_elements * 16
|
||
total_output_bytes += n_elements * 2
|
||
continue
|
||
|
||
bits, gs, _mode = _get_predicate_bits(name, config, oq_level, group_size)
|
||
if bits is None:
|
||
total_params += n_elements
|
||
total_weighted_bits += n_elements * 16
|
||
total_output_bytes += n_elements * 2
|
||
continue
|
||
|
||
total_params += n_elements
|
||
src_info = idx.source_quant_info(name)
|
||
if src_info is not None and bits >= src_info["bits"]:
|
||
# Passthrough: packed weight at source bits plus one e8m0
|
||
# uint8 scale byte per group.
|
||
rows = n_elements // max(shape[-1], 1)
|
||
n_groups = shape[-1] // src_info["group_size"]
|
||
tensor_bytes = (n_elements * src_info["bits"] + 7) // 8
|
||
tensor_bytes += rows * n_groups
|
||
total_output_bytes += tensor_bytes
|
||
total_weighted_bits += tensor_bytes * 8
|
||
elif len(shape) >= 2:
|
||
n_groups = (shape[-1] + gs - 1) // gs
|
||
rows = n_elements // max(shape[-1], 1)
|
||
weight_bytes = (n_elements * bits + 7) // 8
|
||
if _mode == "mxfp4":
|
||
bytes_per_group = 1
|
||
elif _mode == "mxfp8":
|
||
bytes_per_group = 2
|
||
else:
|
||
bytes_per_group = 4
|
||
overhead_bytes = rows * n_groups * bytes_per_group
|
||
tensor_bytes = weight_bytes + overhead_bytes
|
||
total_output_bytes += tensor_bytes
|
||
total_weighted_bits += tensor_bytes * 8
|
||
else:
|
||
total_output_bytes += n_elements * 2
|
||
total_weighted_bits += n_elements * 16
|
||
|
||
for k in ("_oq_use_budget_plan", "_oq_boost_map", "_oq_sensitivity_map"):
|
||
config.pop(k, None)
|
||
|
||
effective_bpw = total_weighted_bits / max(total_params, 1)
|
||
|
||
# Fractional-level correction: the expert down_proj boost is not
|
||
# visible in pre-sanitize scans of fused layouts (gate_up_proj-style
|
||
# tensors don't have a .weight suffix). After sanitize, down_proj is
|
||
# ~31% of routed expert params, so each boost bit adds roughly this
|
||
# much effective bpw. When the scan DID see the down tensors the boost
|
||
# is already priced by the plan, so the correction would double-count.
|
||
_down_boost = _LEVEL_EXPERT_DOWN_BOOST.get(oq_level)
|
||
if _down_boost:
|
||
_down_visible = any(
|
||
_is_routed_expert(p) and any(s in p for s in ("down_proj", "w2"))
|
||
for p in named_shapes
|
||
)
|
||
if not _down_visible:
|
||
effective_bpw += 0.3 * _down_boost
|
||
total_output_bytes = int(effective_bpw * total_params / 8)
|
||
|
||
source_total = sum(sf.stat().st_size for sf in source.glob("*.safetensors"))
|
||
streaming_peak = int(source_total * 1.5) + 5 * 1024**3
|
||
|
||
return {
|
||
"effective_bpw": round(effective_bpw, 2),
|
||
"output_size_bytes": total_output_bytes,
|
||
"output_size_formatted": _format_size(total_output_bytes),
|
||
"memory_streaming_bytes": streaming_peak,
|
||
"memory_streaming_formatted": _format_size(streaming_peak),
|
||
}
|
||
|
||
|
||
def estimate_memory(source_size_bytes: int) -> dict:
|
||
"""Estimate peak memory for quantization.
|
||
|
||
This is a rough estimate used before precise calculation is available.
|
||
The /api/oq/estimate endpoint provides precise values per tensor.
|
||
|
||
Streaming: source (mmap) + 5GB output buffer + sanitize overhead
|
||
"""
|
||
peak = source_size_bytes + 6 * 1024**3
|
||
return {"peak_bytes": peak, "peak_formatted": _format_size(peak)}
|
||
|
||
|
||
def _format_size(size_bytes: int) -> str:
|
||
"""Format byte count as human-readable string."""
|
||
if size_bytes < 1024:
|
||
return f"{size_bytes} B"
|
||
elif size_bytes < 1024**2:
|
||
return f"{size_bytes / 1024:.1f} KB"
|
||
elif size_bytes < 1024**3:
|
||
return f"{size_bytes / 1024**2:.1f} MB"
|
||
else:
|
||
return f"{size_bytes / 1024**3:.1f} GB"
|
||
|
||
|
||
def _emit_progress(
|
||
callback,
|
||
phase: str,
|
||
pct: float,
|
||
detail: str = "",
|
||
meta: dict[str, Any] | None = None,
|
||
) -> None:
|
||
"""Emit progress while preserving the older two-argument callback API."""
|
||
if callback is None:
|
||
return
|
||
try:
|
||
callback(phase, pct, detail, meta or {})
|
||
except TypeError:
|
||
callback(phase, pct)
|
||
|
||
|
||
def _system_available_memory_bytes() -> int:
|
||
try:
|
||
import psutil
|
||
|
||
return int(psutil.virtual_memory().available)
|
||
except Exception:
|
||
return 0
|
||
|
||
|
||
def _metal_available_memory_bytes() -> int:
|
||
try:
|
||
info = mx.device_info()
|
||
max_working_set = int(info.get("max_recommended_working_set_size", 0) or 0)
|
||
except Exception:
|
||
max_working_set = 0
|
||
if max_working_set <= 0:
|
||
return 0
|
||
try:
|
||
active = int(mx.get_active_memory()) + int(mx.get_cache_memory())
|
||
except Exception:
|
||
active = 0
|
||
return max(0, max_working_set - active)
|
||
|
||
|
||
def _nested_config_int(config: dict, keys: tuple[str, ...], default: int = 0) -> int:
|
||
text_config = config.get("text_config", {})
|
||
for source in (config, text_config if isinstance(text_config, dict) else {}):
|
||
for key in keys:
|
||
value = source.get(key)
|
||
if value is None:
|
||
continue
|
||
try:
|
||
return int(value)
|
||
except (TypeError, ValueError):
|
||
continue
|
||
return int(default)
|
||
|
||
|
||
def _oqe_calibration_batch_plan(
|
||
config: dict,
|
||
*,
|
||
requested_samples: int,
|
||
seq_length: int,
|
||
) -> dict[str, Any]:
|
||
"""Choose an oQe calibration micro-batch from live memory and model shape."""
|
||
hidden_size = _nested_config_int(
|
||
config,
|
||
("hidden_size", "model_dim", "n_embd", "d_model"),
|
||
default=4096,
|
||
)
|
||
num_experts = _nested_config_int(
|
||
config,
|
||
("num_local_experts", "num_experts", "n_routed_experts"),
|
||
default=0,
|
||
)
|
||
top_k = _nested_config_int(
|
||
config,
|
||
("top_k_experts", "num_experts_per_tok", "moe_top_k", "top_k"),
|
||
default=1,
|
||
)
|
||
route_factor = max(1, top_k if num_experts > 0 else 1)
|
||
sample_bytes = max(1, int(seq_length) * max(1, hidden_size) * 4 * route_factor)
|
||
|
||
system_available = _system_available_memory_bytes()
|
||
metal_available = _metal_available_memory_bytes()
|
||
live_available = (
|
||
min(b for b in (system_available, metal_available) if b > 0)
|
||
if system_available > 0 or metal_available > 0
|
||
else 0
|
||
)
|
||
|
||
if live_available > 0:
|
||
# Cap activation materialization even on very large-memory machines:
|
||
# MoE capture creates large temporary NumPy arrays per module.
|
||
capture_budget = min(768 * 1024**2, max(128 * 1024**2, live_available // 100))
|
||
else:
|
||
capture_budget = 256 * 1024**2
|
||
|
||
micro_batch_size = max(
|
||
1, min(int(requested_samples), capture_budget // sample_bytes)
|
||
)
|
||
hard_cap = 16 if num_experts > 0 else 32
|
||
micro_batch_size = max(1, min(micro_batch_size, hard_cap))
|
||
return {
|
||
"micro_batch_size": int(micro_batch_size),
|
||
"estimated_sample_bytes": int(sample_bytes),
|
||
"capture_budget_bytes": int(capture_budget),
|
||
"system_available_bytes": int(system_available),
|
||
"metal_available_bytes": int(metal_available),
|
||
"live_available_bytes": int(live_available),
|
||
"hidden_size": int(hidden_size),
|
||
"num_experts": int(num_experts),
|
||
"top_k": int(top_k),
|
||
}
|
||
|
||
|
||
_MAX_SHARD_BYTES = 5_000_000_000
|
||
|
||
_SKIP_QUANT_PATTERNS = (
|
||
"layernorm",
|
||
"rmsnorm",
|
||
"norm.weight",
|
||
"norm.bias",
|
||
"ln_",
|
||
"layer_norm",
|
||
)
|
||
|
||
|
||
def _should_skip_tensor(name: str, preserve_mtp: bool = False) -> bool:
|
||
"""Check if a tensor should be completely excluded from output.
|
||
|
||
By default mtp.* tensors are stripped because mlx-lm's stock sanitize()
|
||
removes them when the model has no MTP head. When ``preserve_mtp`` is
|
||
True the caller has stashed mtp.* tensors around the sanitize call and
|
||
re-merged them, so we must keep them in the output shards.
|
||
"""
|
||
if ".mtp." in name or name.startswith("mtp."):
|
||
return not preserve_mtp
|
||
return False
|
||
|
||
|
||
def _is_mtp_tensor(name: str) -> bool:
|
||
"""Return True iff the tensor key belongs to an MTP head."""
|
||
return name.startswith("mtp.") or ".mtp." in name
|
||
|
||
|
||
def _source_has_nextn_tensors(keys, config: dict) -> bool:
|
||
"""True iff the checkpoint stores its MTP head as extra decoder layers.
|
||
|
||
DeepSeek-V3-style checkpoints (GLM-5.2 among them) keep the MTP layers
|
||
as ``model.layers.<num_hidden_layers + i>.*`` rather than ``mtp.*``;
|
||
the model patch's sanitize remaps them, so for preservation purposes
|
||
they count as MTP tensors even though ``_is_mtp_tensor`` (which sees
|
||
post-sanitize names) doesn't match them.
|
||
"""
|
||
cfgs = (config, config.get("text_config") or {})
|
||
n_mtp = max(int(c.get("num_nextn_predict_layers", 0) or 0) for c in cfgs)
|
||
if n_mtp <= 0:
|
||
return False
|
||
n_main = 0
|
||
for c in cfgs:
|
||
n_main = max(n_main, int(c.get("num_hidden_layers", 0) or 0))
|
||
if n_main <= 0:
|
||
return False
|
||
prefixes = tuple(f"model.layers.{n_main + i}." for i in range(n_mtp))
|
||
return any(k.startswith(prefixes) for k in keys)
|
||
|
||
|
||
def _normalize_mtp_in_config(config: dict) -> None:
|
||
"""Zero out MTP layer counts in the output config (in place).
|
||
|
||
Used when preserve_mtp is False so the resulting quantized model
|
||
presents itself as MTP-free. Without this, the source config's
|
||
mtp_num_hidden_layers / num_nextn_predict_layers values would survive
|
||
while the actual mtp.* tensors are stripped, producing the
|
||
"Missing N parameters" load error we hit on Qwen3.5-27B.
|
||
"""
|
||
for key in ("mtp_num_hidden_layers", "num_nextn_predict_layers"):
|
||
if key in config and config[key]:
|
||
config[key] = 0
|
||
text_cfg = config.get("text_config")
|
||
if isinstance(text_cfg, dict):
|
||
for key in ("mtp_num_hidden_layers", "num_nextn_predict_layers"):
|
||
if key in text_cfg and text_cfg[key]:
|
||
text_cfg[key] = 0
|
||
|
||
|
||
def _should_quantize_tensor(name: str, shape: tuple) -> bool:
|
||
"""Check if a tensor should be quantized based on name and shape."""
|
||
if not name.endswith(".weight"):
|
||
# Only module weights are quantizable. 2D plain parameters (e.g.
|
||
# DeepSeek V4 hyper-connection fn/base tables, compressor.ape)
|
||
# must pass through untouched — emitting weight/scales pairs for
|
||
# them would corrupt the checkpoint.
|
||
return False
|
||
if len(shape) < 2:
|
||
return False
|
||
name_lower = name.lower()
|
||
if any(p in name_lower for p in _SKIP_QUANT_PATTERNS):
|
||
return False
|
||
if name.endswith(".bias"):
|
||
return False
|
||
return True
|
||
|
||
|
||
def _cast_passthrough_tensor(tensor_name: str, w_mx, target_dtype):
|
||
"""Cast an unquantized output tensor to its storage dtype."""
|
||
if not mx.issubdtype(w_mx.dtype, mx.floating):
|
||
return w_mx
|
||
|
||
if target_dtype == mx.float16 and (
|
||
_is_vision_tensor(tensor_name) or _is_audio_tensor(tensor_name)
|
||
):
|
||
if w_mx.dtype != mx.float32:
|
||
return w_mx.astype(mx.float32)
|
||
return w_mx
|
||
|
||
if w_mx.dtype != target_dtype:
|
||
return w_mx.astype(target_dtype)
|
||
return w_mx
|
||
|
||
|
||
def _build_model_sanitizer(config: dict, text_only: bool = False):
|
||
"""Build a sanitize function from the model class.
|
||
|
||
For VLM models, uses mlx-vlm's model class (preserves vision weights).
|
||
For LLM models, uses mlx-lm's model class.
|
||
When text_only is True, always uses the LLM path even for VLM
|
||
architectures so that mlx_lm_mtp patches (which handle MTP sanitize
|
||
for both dense and MoE) are used instead of the VLM path whose
|
||
_Proxy-based sanitize drops the MTP head.
|
||
|
||
Returns:
|
||
A function that takes a dict of weights and returns sanitized weights,
|
||
or None if the model class can't be loaded.
|
||
"""
|
||
architectures = config.get("architectures", [])
|
||
is_vlm = (
|
||
any("ForConditionalGeneration" in a for a in architectures) and not text_only
|
||
)
|
||
|
||
if is_vlm:
|
||
try:
|
||
try:
|
||
model_type = config.get("model_type")
|
||
text_config = config.get("text_config")
|
||
text_model_type = (
|
||
text_config.get("model_type")
|
||
if isinstance(text_config, dict)
|
||
else None
|
||
)
|
||
if model_type in ("minimax_m3", "minimax_m3_vl") or (
|
||
text_model_type in ("minimax_m3", "minimax_m3_vl")
|
||
):
|
||
from omlx.patches.mlx_vlm_minimax_m3_compat import (
|
||
apply_mlx_vlm_minimax_m3_compat_patch,
|
||
)
|
||
|
||
apply_mlx_vlm_minimax_m3_compat_patch()
|
||
except Exception as patch_err:
|
||
logger.debug(f"MiniMax M3 mlx-vlm patch not applied: {patch_err}")
|
||
|
||
from mlx_vlm.utils import get_model_and_args, sanitize_weights
|
||
|
||
# Apply mlx-vlm MTP sanitize patch so qwen3_5/qwen3_5_moe Model
|
||
# classes keep ``mtp.*`` weights and shift the MTP-specific
|
||
# RMSNorm tensors by +1 (matching mlx_lm_mtp/qwen35_model.py).
|
||
# Without this, oQ output ships raw MTP norm weights, the
|
||
# mlx-lm patched sanitize on load doesn't re-shift (it guards on
|
||
# the unsanitized conv1d marker, which is False after oQ), and
|
||
# the MTP head produces garbage logits — 0% accept rate.
|
||
try:
|
||
from omlx.patches.mlx_vlm_mtp import apply_mlx_vlm_mtp_patch
|
||
|
||
apply_mlx_vlm_mtp_patch()
|
||
except Exception as patch_err:
|
||
logger.debug(f"mlx-vlm MTP patch not applied: {patch_err}")
|
||
|
||
# Remap language_model.model.visual.* -> vision_tower.* for
|
||
# Qwen3.6-35B-A3B's nested ViT layout. Wraps whichever
|
||
# Model.sanitize is current; no-op when already installed or
|
||
# when upstream mlx-vlm grows the rule itself.
|
||
try:
|
||
from omlx.patches.qwen3_6_nested_visual import (
|
||
apply_qwen3_6_nested_visual_patch,
|
||
)
|
||
|
||
apply_qwen3_6_nested_visual_patch()
|
||
except Exception as patch_err:
|
||
logger.debug(f"qwen3_6 nested-visual patch not applied: {patch_err}")
|
||
|
||
model_module, _ = get_model_and_args(config)
|
||
model_config_cls = model_module.ModelConfig
|
||
model_config = model_config_cls.from_dict(config)
|
||
|
||
vision_config = model_config.vision_config
|
||
if isinstance(vision_config, dict):
|
||
vision_config = model_module.VisionConfig.from_dict(vision_config)
|
||
text_config = model_config.text_config
|
||
if isinstance(text_config, dict):
|
||
text_config = model_module.TextConfig.from_dict(text_config)
|
||
|
||
model_config.vision_config = vision_config
|
||
model_config.text_config = text_config
|
||
|
||
# Some VLM Model.sanitize implementations (e.g. Gemma 4) drop
|
||
# `audio_tower.*` / `embed_audio.*` weights when `self.audio_tower`
|
||
# is None. Set a truthy sentinel iff the source config carries an
|
||
# `audio_config` so the audio modality survives sanitize and stays
|
||
# in the quantization pipeline.
|
||
has_audio = config.get("audio_config") is not None
|
||
_AUDIO_SENTINEL = object() if has_audio else None
|
||
|
||
def _vlm_sanitize(weights):
|
||
class _Proxy:
|
||
# The audio-presence guard differs by arch: gemma4 checks
|
||
# ``self.audio_tower``; gemma4_unified checks
|
||
# ``self.embed_audio``. Expose BOTH (sentinel iff the source
|
||
# config carries audio) so sanitize keeps the audio modality
|
||
# for either. Missing ``embed_audio`` made gemma4_unified's
|
||
# sanitize raise AttributeError, silently dropping the whole
|
||
# sanitize pass → oQ shipped raw ``model.``-prefixed keys
|
||
# that omlx could not load.
|
||
audio_tower = _AUDIO_SENTINEL
|
||
embed_audio = _AUDIO_SENTINEL
|
||
|
||
proxy = _Proxy()
|
||
proxy.config = model_config
|
||
# Nested-VLM sanitizes (e.g. MiniMax-M3 minimax_m3_vl) read
|
||
# self.language_model.args.{num_hidden_layers,num_local_experts}
|
||
# for MoE expert stacking; expose text_config so proxy-based
|
||
# discovery works without instantiating the full model.
|
||
_lm_proxy = type("_LMProxy", (), {})()
|
||
_lm_proxy.args = text_config
|
||
proxy.language_model = _lm_proxy
|
||
w = model_module.Model.sanitize(proxy, weights)
|
||
|
||
w = sanitize_weights(model_module.VisionModel, w, vision_config)
|
||
w = sanitize_weights(model_module.LanguageModel, w, text_config)
|
||
return w
|
||
|
||
logger.info(
|
||
f"Using mlx-vlm full sanitize chain for "
|
||
f"{model_module.Model.__name__} "
|
||
f"(preserves vision{', audio' if has_audio else ''} weights)"
|
||
)
|
||
return _vlm_sanitize
|
||
except Exception as e:
|
||
logger.debug(f"mlx-vlm sanitizer not available: {e}")
|
||
|
||
try:
|
||
from mlx_lm.utils import _get_classes
|
||
|
||
if config.get("model_type") == "glm_moe_dsa":
|
||
try:
|
||
from omlx.patches.glm_moe_dsa import apply_glm_moe_dsa_patch
|
||
|
||
apply_glm_moe_dsa_patch()
|
||
except Exception as patch_err:
|
||
logger.debug(f"glm_moe_dsa patch not applied: {patch_err}")
|
||
|
||
# DeepSeek-V4 isn't in stock mlx-lm — its model class is injected
|
||
# into ``sys.modules`` by oMLX's base patch. Trigger that here so
|
||
# ``_get_classes(config)`` for deepseek_v4* model types succeeds.
|
||
# No-op for other model types.
|
||
if str(config.get("model_type", "")).startswith("deepseek_v4"):
|
||
try:
|
||
from omlx.patches.deepseek_v4 import apply_deepseek_v4_patch
|
||
|
||
apply_deepseek_v4_patch()
|
||
except Exception as patch_err:
|
||
logger.debug(f"deepseek_v4 base patch not applied: {patch_err}")
|
||
|
||
# Apply mlx-lm MTP patch so the patched __init__/sanitize handle
|
||
# mtp.* tensors correctly. Idempotent — apply() is a no-op once
|
||
# patched.
|
||
try:
|
||
from omlx.patches.mlx_lm_mtp import (
|
||
apply_mlx_lm_mtp_patch,
|
||
is_mtp_active,
|
||
set_mtp_active,
|
||
)
|
||
|
||
apply_mlx_lm_mtp_patch()
|
||
_have_mtp_patch = True
|
||
except Exception as patch_err:
|
||
logger.debug(f"mlx-lm MTP patch not applied: {patch_err}")
|
||
_have_mtp_patch = False
|
||
|
||
model_class, model_args_class = _get_classes(config)
|
||
args = model_args_class.from_dict(config)
|
||
|
||
# Force MTP active during model instantiation so the patched
|
||
# ``__init__`` attaches ``self.mtp``. With ``self.mtp`` attached,
|
||
# the patched ``Model.sanitize`` keeps ``mtp.*`` weights and applies
|
||
# the +1 RMSNorm shift to MTP norms (matching backbone). Without
|
||
# this, mtp.* would be stripped and MTP norms would never receive
|
||
# the shift, producing 0% accept rate after quantization.
|
||
if _have_mtp_patch:
|
||
prev_active = is_mtp_active()
|
||
try:
|
||
set_mtp_active(True)
|
||
model = model_class(args)
|
||
finally:
|
||
set_mtp_active(prev_active)
|
||
else:
|
||
model = model_class(args)
|
||
|
||
if hasattr(model, "sanitize"):
|
||
logger.info(
|
||
f"Using mlx-lm {model_class.__name__}.sanitize() "
|
||
f"for weight transformation"
|
||
)
|
||
bound_sanitize = model.sanitize
|
||
|
||
def _sanitize(weights):
|
||
return bound_sanitize(weights)
|
||
|
||
# Expose the model's cast predicate (key -> bool, False = keep
|
||
# the source dtype) so the streaming loop can skip the target
|
||
# dtype cast for tensors the model declares non-castable
|
||
# (e.g. DeepSeek V4 attn_sink / hyper-connection tables).
|
||
_sanitize._omlx_cast_predicate = getattr(model, "cast_predicate", None)
|
||
return _sanitize
|
||
except Exception as e:
|
||
logger.warning(f"Could not build model sanitizer: {e}")
|
||
|
||
return None
|
||
|
||
|
||
def _copy_model_sidecars(source: Path, output: Path) -> None:
|
||
"""Copy tokenizer/processor sidecar files needed to load the output."""
|
||
for pattern in (
|
||
"tokenizer.json",
|
||
"tokenizer_config.json",
|
||
"special_tokens_map.json",
|
||
"tokenizer.model",
|
||
"generation_config.json",
|
||
"chat_template.json",
|
||
"chat_template.jinja",
|
||
"preprocessor_config.json",
|
||
"processor_config.json",
|
||
"added_tokens.json",
|
||
"merges.txt",
|
||
"vocab.json",
|
||
):
|
||
for src_file in source.glob(pattern):
|
||
shutil.copy2(src_file, output / src_file.name)
|
||
|
||
for py_file in source.glob("*.py"):
|
||
shutil.copy2(py_file, output / py_file.name)
|
||
|
||
|
||
def _build_non_quantizable_set(config: dict) -> set:
|
||
"""Find module paths with 2D weights that lack to_quantized() support.
|
||
|
||
Loads the model class (without real weights) and walks the module tree.
|
||
Modules like ScaledLinear in Gemma 4 have a weight attribute but no
|
||
to_quantized(), so they cannot be loaded as QuantizedLinear after
|
||
quantization. Returns empty set if the model class cannot be loaded.
|
||
"""
|
||
try:
|
||
from mlx_lm.utils import _get_classes
|
||
|
||
model_class, model_args_class = _get_classes(config)
|
||
args = model_args_class.from_dict(config)
|
||
model = model_class(args)
|
||
|
||
result = set()
|
||
for path, module in tree_flatten(
|
||
model.leaf_modules(), is_leaf=nn.Module.is_module
|
||
):
|
||
if hasattr(module, "weight") and not hasattr(module, "to_quantized"):
|
||
if getattr(module.weight, "ndim", 0) >= 2:
|
||
result.add(_normalize_quant_path(path))
|
||
|
||
if result:
|
||
logger.info(
|
||
"Non-quantizable modules (no to_quantized): "
|
||
+ ", ".join(sorted(result))
|
||
)
|
||
return result
|
||
except Exception as e:
|
||
logger.debug(f"Could not build non-quantizable set: {e}")
|
||
return set()
|
||
|
||
|
||
def _is_mtp_protected_tensor(name: str) -> bool:
|
||
"""Tensors inside the MTP head that must stay in full precision.
|
||
|
||
Aggressive quantization of the MTP head's fusion projection or final
|
||
hyper-head collapses draft acceptance to ~0% (oQ4 of an MTP-preserved
|
||
Qwen3.5-27B accepted 0/157 cycles). PR 990 protects ``mtp.fc`` for
|
||
Qwen3.5/3.6; PR 15's DeepSeek-V4 ``MTPBlock`` exposes the same
|
||
semantics under different names (``e_proj`` + ``h_proj`` for the
|
||
embedding/hidden fusion; ``hc_head.*`` for the final projection);
|
||
GLM-5.2's ``GlmMTPBlock`` calls it ``eh_proj``. All of these stay in
|
||
full precision; the MTP block's internal decoder block (attn/ffn)
|
||
gets the same quantization as the backbone's other layers.
|
||
"""
|
||
if not (name.startswith("mtp.") or ".mtp." in name):
|
||
return False
|
||
# Qwen3.5/3.6 fusion projection
|
||
if name.endswith("mtp.fc.weight") or ".mtp.fc.weight" in name:
|
||
return True
|
||
# DeepSeek-V4 / GLM-5.2 MTP block fusion projections
|
||
if name.endswith((".e_proj.weight", ".h_proj.weight", ".eh_proj.weight")):
|
||
return True
|
||
# DeepSeek-V4 HyperHead final projection (sanitized form has the dot;
|
||
# the raw-HF form arrives as ``hc_head_<param>`` and we cover both).
|
||
if ".hc_head." in name:
|
||
return True
|
||
if (
|
||
name.endswith(".hc_head_fn")
|
||
or name.endswith(".hc_head_base")
|
||
or name.endswith(".hc_head_scale")
|
||
):
|
||
return True
|
||
return False
|
||
|
||
|
||
def _get_predicate_bits(
|
||
tensor_name: str, config: dict, oq_level: int, group_size: int
|
||
) -> tuple:
|
||
"""Get quantization bits, group_size, and mode for a tensor.
|
||
|
||
Returns:
|
||
(bits, group_size, mode) or (None, None, None) if not quantized.
|
||
"""
|
||
# See _is_mtp_protected_tensor for why these tensors stay full precision.
|
||
if _is_mtp_protected_tensor(tensor_name):
|
||
return None, None, None
|
||
|
||
base_bits = _base_bits_for_level(oq_level)
|
||
|
||
result = universal_quant_predicate(tensor_name, None, config, oq_level)
|
||
if result is False:
|
||
return None, None, None
|
||
if isinstance(result, dict):
|
||
bits = result.get("bits", base_bits)
|
||
gs = result.get("group_size", group_size)
|
||
mode = result.get("mode", _mode_for_bits(bits))
|
||
if _is_mtp_tensor(tensor_name):
|
||
raised = _mtp_bits_override(bits)
|
||
if raised != bits:
|
||
# Floor lift re-derives mode and group size; unchanged bits
|
||
# keep the predicate's choice (e.g. mxfp8 head projections).
|
||
bits, mode = raised, _mode_for_bits(raised)
|
||
gs = _gs_for_mode(raised, group_size)
|
||
return bits, gs, mode
|
||
bits = base_bits
|
||
if _is_mtp_tensor(tensor_name):
|
||
bits = _mtp_bits_override(bits)
|
||
return bits, _gs_for_mode(bits, group_size), _mode_for_bits(bits)
|
||
|
||
|
||
# Minimum bits for quantized MTP-head tensors. Sub-4-bit oQ levels drag the
|
||
# head down with the trunk (DeepSeek-V4 oQ2.5e stored its head's routed
|
||
# experts and attention at 2-bit gs64), but the head only shapes drafts —
|
||
# every emitted token is trunk-verified — so its footprint (~2 GB on
|
||
# DeepSeek-V4-Flash, ~15 MB on Qwen3.6) buys draft acceptance directly and
|
||
# costs almost nothing relative to the model.
|
||
_MTP_MIN_BITS = 4
|
||
|
||
|
||
def _mtp_bits_override(bits: int) -> int:
|
||
if bits and bits < _MTP_MIN_BITS:
|
||
return _MTP_MIN_BITS
|
||
return bits
|
||
|
||
|
||
def _mode_for_bits(bits: int) -> str:
|
||
"""Select quantization mode. Always affine to minimize kernel combos."""
|
||
return "affine"
|
||
|
||
|
||
def _gs_for_mode(bits: int, default_gs: int) -> int:
|
||
"""Get group_size. Always default to minimize kernel combos."""
|
||
return default_gs
|
||
|
||
|
||
# --- chunked-quantize helpers (added for Qwen3.5-397B) ---------------------
|
||
import struct as _struct
|
||
|
||
import numpy as _np
|
||
|
||
|
||
def _metal_max_buffer_bytes() -> int:
|
||
try:
|
||
info = mx.device_info()
|
||
except AttributeError:
|
||
try:
|
||
info = mx.metal.device_info()
|
||
except Exception:
|
||
return 1 << 30
|
||
except Exception:
|
||
return 1 << 30
|
||
return int(info.get("max_buffer_length", 1 << 30))
|
||
|
||
|
||
_METAL_MAX_BUFFER = _metal_max_buffer_bytes()
|
||
_QUANTIZE_CHUNK_BYTES = max(1 << 20, _METAL_MAX_BUFFER // 4)
|
||
_LOAD_CHUNK_BYTES = max(1 << 20, _METAL_MAX_BUFFER // 2)
|
||
|
||
|
||
class _LazyTensorIndex:
|
||
_DTYPE_BYTES = {
|
||
"BF16": 2,
|
||
"F16": 2,
|
||
"F32": 4,
|
||
"F64": 8,
|
||
"I8": 1,
|
||
"U8": 1,
|
||
"I16": 2,
|
||
"U16": 2,
|
||
"I32": 4,
|
||
"U32": 4,
|
||
"I64": 8,
|
||
"U64": 8,
|
||
"BOOL": 1,
|
||
"F8_E4M3": 1,
|
||
"F8_E5M2": 1,
|
||
"F8_E8M0": 1,
|
||
}
|
||
|
||
def __init__(self, weight_files):
|
||
self._index = {}
|
||
for sf_path in weight_files:
|
||
with open(sf_path, "rb") as f:
|
||
hlen = _struct.unpack("<Q", f.read(8))[0]
|
||
header = json.loads(f.read(hlen))
|
||
data_offset = 8 + hlen
|
||
for k, meta in header.items():
|
||
if k == "__metadata__":
|
||
continue
|
||
self._index[k] = (
|
||
sf_path,
|
||
data_offset,
|
||
meta["data_offsets"][0],
|
||
meta["data_offsets"][1],
|
||
tuple(meta["shape"]),
|
||
meta["dtype"],
|
||
)
|
||
self._fp8_pairs = {}
|
||
self._fp8_scale_keys = set()
|
||
self._src_quant = {}
|
||
self._discover_fp8_pairs()
|
||
|
||
def _discover_fp8_pairs(self):
|
||
seen = set()
|
||
for k in list(self._index):
|
||
if k.endswith("_scale_inv"):
|
||
wk = k[: -len("_scale_inv")]
|
||
if (
|
||
wk in self._index
|
||
and wk not in seen
|
||
and self._index[wk][5] in _FP8_WEIGHT_DTYPES
|
||
):
|
||
self._fp8_pairs[wk] = k
|
||
seen.add(wk)
|
||
elif k.endswith(".scale"):
|
||
wk = k[: -len(".scale")] + ".weight"
|
||
if (
|
||
wk in self._index
|
||
and wk not in seen
|
||
and self._index[wk][5] in _FP8_WEIGHT_DTYPES
|
||
):
|
||
self._fp8_pairs[wk] = k
|
||
seen.add(wk)
|
||
self._fp8_scale_keys = set(self._fp8_pairs.values())
|
||
for wk, sk in self._fp8_pairs.items():
|
||
info = self._classify_pair(wk, sk)
|
||
if info is not None:
|
||
self._src_quant[wk] = info
|
||
if self._fp8_pairs:
|
||
logger.info(
|
||
f"FP8 on-the-fly dequant: {len(self._fp8_pairs)} weight+scale pairs detected"
|
||
)
|
||
|
||
def _classify_pair(self, wk, sk):
|
||
"""Classify a weight+scale pair into an mlx-native quantized format.
|
||
|
||
Returns a dict {kind, bits, group_size, mode} when the pair can be
|
||
passed through to the output in mlx packed form (mxfp4/mxfp8), or
|
||
None for layouts we only support via dequantization (E5M2, float
|
||
scales, plain int8 block quant, _scale_inv pairs, ...).
|
||
"""
|
||
w_shape, w_dtype = self._index[wk][4], self._index[wk][5]
|
||
s_shape, s_dtype = self._index[sk][4], self._index[sk][5]
|
||
if len(w_shape) != 2 or len(s_shape) != 2 or not sk.endswith(".scale"):
|
||
return None
|
||
rows, cols = w_shape
|
||
# FP4-packed experts (DeepSeek V4): int8 bytes carry 2 fp4 values
|
||
# each, e8m0 scale per 32 logical values -> 16 bytes per group.
|
||
if (
|
||
w_dtype == "I8"
|
||
and s_dtype == "F8_E8M0"
|
||
and cols % 16 == 0
|
||
and tuple(s_shape) == (rows, cols // 16)
|
||
):
|
||
return {"kind": "mxfp4", "bits": 4, "group_size": 32, "mode": "mxfp4"}
|
||
# FP8 block quant (e4m3 weight, e8m0 block scale): representable as
|
||
# mxfp8 g32 after expanding the block scale per 32-column group.
|
||
if (
|
||
w_dtype == "F8_E4M3"
|
||
and s_dtype == "F8_E8M0"
|
||
and s_shape[0] > 0
|
||
and s_shape[1] > 0
|
||
and rows % s_shape[0] == 0
|
||
and cols % s_shape[1] == 0
|
||
and (cols // s_shape[1]) % 32 == 0
|
||
and cols % 4 == 0
|
||
):
|
||
return {"kind": "fp8_block", "bits": 8, "group_size": 32, "mode": "mxfp8"}
|
||
return None
|
||
|
||
def _dequant_one(self, wk):
|
||
sk = self._fp8_pairs[wk]
|
||
w_meta = self._index[wk]
|
||
s_meta = self._index[sk]
|
||
w_lt = _LazyTensor(
|
||
w_meta[0], w_meta[1], w_meta[2], w_meta[3], w_meta[4], w_meta[5]
|
||
)
|
||
s_lt = _LazyTensor(
|
||
s_meta[0], s_meta[1], s_meta[2], s_meta[3], s_meta[4], s_meta[5]
|
||
)
|
||
weight_raw = w_lt[:]
|
||
scale_raw = s_lt[:]
|
||
mx.eval(weight_raw, scale_raw)
|
||
info = self._src_quant.get(wk)
|
||
if info is not None and info["kind"] == "mxfp4":
|
||
# FP4-packed: reinterpret bytes as the mlx mxfp4 packed layout
|
||
# and let mx.dequantize unpack (e8m0 uint8 scales, group 32).
|
||
weight = mx.dequantize(
|
||
weight_raw.view(mx.uint32),
|
||
scale_raw,
|
||
None,
|
||
group_size=32,
|
||
bits=4,
|
||
mode="mxfp4",
|
||
).astype(mx.bfloat16)
|
||
else:
|
||
weight = _block_dequant_fp8(weight_raw, scale_raw, w_meta[5], s_meta[5])
|
||
del weight_raw, scale_raw
|
||
mx.clear_cache()
|
||
return weight
|
||
|
||
def _load_packed(self, wk):
|
||
"""Load a passthrough-capable pair in mlx packed quantized form.
|
||
|
||
Returns (weight, scales): uint32-packed weight plus uint8 e8m0
|
||
scales matching the pair's {bits, group_size, mode} from
|
||
source_quant_info. fp8_block scales are expanded from per-block to
|
||
per-32-column-group, mirroring the model sanitize's repeat expansion.
|
||
"""
|
||
sk = self._fp8_pairs[wk]
|
||
info = self._src_quant[wk]
|
||
weight_raw = self._load_raw(wk)
|
||
scale_raw = self._load_raw(sk)
|
||
packed = weight_raw.view(mx.uint32)
|
||
if info["kind"] == "fp8_block":
|
||
rows, cols = self._index[wk][4]
|
||
sm, sn = self._index[sk][4]
|
||
row_rep = rows // sm
|
||
col_rep = (cols // 32) // sn
|
||
if col_rep > 1:
|
||
scale_raw = mx.repeat(scale_raw, col_rep, -1)
|
||
if row_rep > 1:
|
||
scale_raw = mx.repeat(scale_raw, row_rep, 0)
|
||
mx.eval(packed, scale_raw)
|
||
return packed, scale_raw
|
||
|
||
def source_quant_info(self, key):
|
||
"""Pre-quantized source metadata for a weight key, or None."""
|
||
return self._src_quant.get(key)
|
||
|
||
def _is_visible(self, k):
|
||
return k not in self._fp8_scale_keys
|
||
|
||
def logical_metadata(self):
|
||
"""Metadata for plan discovery: FP8 weights report as BF16, scale keys hidden."""
|
||
result = {}
|
||
for k, meta in self._index.items():
|
||
if k in self._fp8_scale_keys:
|
||
continue
|
||
shape, dtype = meta[4], meta[5]
|
||
if k in self._fp8_pairs:
|
||
dtype = "BF16"
|
||
info = self._src_quant.get(k)
|
||
if info is not None and info["kind"] == "mxfp4":
|
||
# FP4-packed bytes: logical width is 2 values per byte.
|
||
shape = (shape[0], shape[1] * 2)
|
||
result[k] = (shape, dtype)
|
||
return result
|
||
|
||
def keys(self):
|
||
base = [k for k in self._index if self._is_visible(k)]
|
||
if hasattr(self, "_overrides"):
|
||
base.extend(self._overrides.keys())
|
||
return base
|
||
|
||
def __len__(self):
|
||
n = sum(1 for k in self._index if self._is_visible(k))
|
||
if hasattr(self, "_overrides"):
|
||
n += len(self._overrides)
|
||
return n
|
||
|
||
def __contains__(self, k):
|
||
if k in self._index and self._is_visible(k):
|
||
return True
|
||
return hasattr(self, "_overrides") and k in self._overrides
|
||
|
||
def __iter__(self):
|
||
for k in self._index:
|
||
if self._is_visible(k):
|
||
yield k
|
||
if hasattr(self, "_overrides"):
|
||
for k in self._overrides:
|
||
if k not in self._index:
|
||
yield k
|
||
|
||
def nbytes(self):
|
||
return sum(
|
||
e - s
|
||
for k, (_, _, s, e, _, _) in self._index.items()
|
||
if self._is_visible(k)
|
||
)
|
||
|
||
def _load_raw(self, key):
|
||
sf_path, data_offset, start, end, shape, dtype = self._index[key]
|
||
lt = _LazyTensor(sf_path, data_offset, start, end, shape, dtype)
|
||
return lt[:]
|
||
|
||
def __getitem__(self, key):
|
||
if hasattr(self, "_overrides") and key in self._overrides:
|
||
return self._overrides[key]
|
||
if key not in self._index:
|
||
raise KeyError(key)
|
||
if key in self._fp8_pairs:
|
||
return self._dequant_one(key)
|
||
return self._load_raw(key)
|
||
|
||
def items(self):
|
||
for k in list(self._index.keys()):
|
||
if not self._is_visible(k):
|
||
continue
|
||
yield k, self[k]
|
||
mx.clear_cache()
|
||
if hasattr(self, "_overrides"):
|
||
for k, v in self._overrides.items():
|
||
yield k, v
|
||
|
||
def get(self, key, default=None):
|
||
if key in self:
|
||
return self[key]
|
||
return default
|
||
|
||
def __setitem__(self, key, value):
|
||
if not hasattr(self, "_overrides"):
|
||
self._overrides = {}
|
||
self._overrides[key] = value
|
||
self._index.pop(key, None)
|
||
self._fp8_pairs.pop(key, None)
|
||
self._src_quant.pop(key, None)
|
||
|
||
def __delitem__(self, key):
|
||
if key in self._fp8_pairs:
|
||
sk = self._fp8_pairs.pop(key)
|
||
self._fp8_scale_keys.discard(sk)
|
||
self._index.pop(sk, None)
|
||
self._index.pop(key, None)
|
||
self._src_quant.pop(key, None)
|
||
if hasattr(self, "_overrides"):
|
||
self._overrides.pop(key, None)
|
||
|
||
def update(self, other):
|
||
if hasattr(other, "items"):
|
||
for k, v in other.items():
|
||
self[k] = v
|
||
else:
|
||
for k, v in other:
|
||
self[k] = v
|
||
|
||
def pop(self, key, *default):
|
||
if hasattr(self, "_overrides") and key in self._overrides:
|
||
return self._overrides.pop(key)
|
||
if key not in self._index:
|
||
if default:
|
||
return default[0]
|
||
raise KeyError(key)
|
||
if key in self._fp8_pairs:
|
||
result = self._dequant_one(key)
|
||
sk = self._fp8_pairs.pop(key)
|
||
self._fp8_scale_keys.discard(sk)
|
||
self._src_quant.pop(key, None)
|
||
self._index.pop(key, None)
|
||
self._index.pop(sk, None)
|
||
return result
|
||
sf_path, data_offset, start, end, shape, dtype = self._index.pop(key)
|
||
lt = _LazyTensor(sf_path, data_offset, start, end, shape, dtype)
|
||
arr = lt[:]
|
||
mx.eval(arr)
|
||
return arr
|
||
|
||
|
||
class _LazyTensor:
|
||
def __init__(self, sf_path, data_offset, start, end, shape, dtype):
|
||
self.shape = tuple(shape)
|
||
self.ndim = len(self.shape)
|
||
self._sf_path = sf_path
|
||
self._data_offset = data_offset
|
||
self._start = start
|
||
self._end = end
|
||
self._dtype = dtype
|
||
self._bpe = _LazyTensorIndex._DTYPE_BYTES.get(dtype, 2)
|
||
self._epr = 1
|
||
for d in self.shape[1:]:
|
||
self._epr *= d
|
||
self._bpr = self._epr * self._bpe
|
||
|
||
@property
|
||
def size(self):
|
||
s = 1
|
||
for d in self.shape:
|
||
s *= d
|
||
return s
|
||
|
||
@property
|
||
def nbytes(self):
|
||
return self._end - self._start
|
||
|
||
_SF_TO_MLX = {
|
||
"BF16": mx.bfloat16,
|
||
"F16": mx.float16,
|
||
"F32": mx.float32,
|
||
"I8": mx.int8,
|
||
"U8": mx.uint8,
|
||
"I16": mx.int16,
|
||
"U16": mx.uint16,
|
||
"I32": mx.int32,
|
||
"U32": mx.uint32,
|
||
"I64": mx.int64,
|
||
"U64": mx.uint64,
|
||
"F8_E4M3": mx.uint8,
|
||
"F8_E5M2": mx.uint8,
|
||
"F8_E8M0": mx.uint8,
|
||
"BOOL": mx.bool_,
|
||
}
|
||
|
||
_SF_TO_NP = {
|
||
"BF16": _np.uint16,
|
||
"F16": _np.float16,
|
||
"F32": _np.float32,
|
||
"F64": _np.float64,
|
||
"I8": _np.int8,
|
||
"U8": _np.uint8,
|
||
"I16": _np.int16,
|
||
"U16": _np.uint16,
|
||
"I32": _np.int32,
|
||
"U32": _np.uint32,
|
||
"I64": _np.int64,
|
||
"U64": _np.uint64,
|
||
"F8_E4M3": _np.uint8,
|
||
"F8_E5M2": _np.uint8,
|
||
"F8_E8M0": _np.uint8,
|
||
"BOOL": _np.bool_,
|
||
}
|
||
|
||
def _mlx_dtype(self):
|
||
return self._SF_TO_MLX.get(self._dtype, mx.bfloat16)
|
||
|
||
def _np_view_dtype(self):
|
||
return self._SF_TO_NP.get(self._dtype, _np.uint16)
|
||
|
||
def _load_rows(self, r0, r1):
|
||
n = r1 - r0
|
||
if n <= 0:
|
||
return mx.zeros((0, *self.shape[1:]), dtype=self._mlx_dtype())
|
||
b0 = self._start + r0 * self._bpr
|
||
b1 = self._start + r1 * self._bpr
|
||
with open(self._sf_path, "rb") as f:
|
||
f.seek(self._data_offset + b0)
|
||
raw = f.read(b1 - b0)
|
||
arr = _np.frombuffer(raw, dtype=self._np_view_dtype())
|
||
chunk_shape = (n, *self.shape[1:])
|
||
# Two ceilings: device buffer bytes, and MLX's int32 element count.
|
||
_MLX_MAX_ELEMS = 1 << 30
|
||
max_rows_bytes = max(1, _LOAD_CHUNK_BYTES // max(self._bpr, 1))
|
||
max_rows_elems = max(1, _MLX_MAX_ELEMS // max(self._epr, 1))
|
||
max_rows = min(max_rows_bytes, max_rows_elems)
|
||
dt = self._mlx_dtype()
|
||
if n <= max_rows:
|
||
t = mx.array(arr).view(dt).reshape(chunk_shape)
|
||
mx.eval(t)
|
||
return t
|
||
parts = []
|
||
epc = max_rows * self._epr
|
||
for s in range(0, arr.size, epc):
|
||
sub = arr[s : s + epc]
|
||
sr = sub.size // self._epr
|
||
t = mx.array(sub).view(dt).reshape((sr, *self.shape[1:]))
|
||
mx.eval(t)
|
||
parts.append(t)
|
||
mx.clear_cache()
|
||
result = mx.concatenate(parts, axis=0)
|
||
mx.eval(result)
|
||
return result
|
||
|
||
def __getitem__(self, idx):
|
||
if len(self.shape) == 0:
|
||
raise IndexError(
|
||
"0-dim _LazyTensor cannot be indexed; caller should use "
|
||
"_materialize_source scalar path"
|
||
)
|
||
if isinstance(idx, tuple):
|
||
return self._load_rows(0, self.shape[0])[idx]
|
||
if isinstance(idx, slice):
|
||
start = idx.start or 0
|
||
stop = self.shape[0] if idx.stop is None else idx.stop
|
||
return self._load_rows(start, stop)
|
||
return self._load_rows(idx, idx + 1)
|
||
|
||
|
||
def _tensor_shape_nbytes(shape, bytes_per_element: int) -> int:
|
||
n = 1
|
||
for dim in shape:
|
||
n *= int(dim)
|
||
return n * bytes_per_element
|
||
|
||
|
||
def _progress_total_bytes(all_weights, source: Path) -> int:
|
||
"""Conservative denominator for streaming quantization progress.
|
||
|
||
Packed or transformed checkpoints can expose a logical tensor view that
|
||
is larger than the physical source shards. Using only source file sizes
|
||
lets progress exceed 100% and makes ETA negative.
|
||
"""
|
||
candidates = [
|
||
sum(sf.stat().st_size for sf in source.glob("*.safetensors")),
|
||
]
|
||
|
||
if hasattr(all_weights, "nbytes"):
|
||
try:
|
||
candidates.append(int(all_weights.nbytes()))
|
||
except Exception:
|
||
pass
|
||
|
||
if hasattr(all_weights, "logical_metadata"):
|
||
try:
|
||
logical_total = 0
|
||
for shape, dtype in all_weights.logical_metadata().values():
|
||
logical_total += _tensor_shape_nbytes(
|
||
shape, _LazyTensorIndex._DTYPE_BYTES.get(dtype, 2)
|
||
)
|
||
candidates.append(logical_total)
|
||
except Exception:
|
||
pass
|
||
|
||
plan = getattr(all_weights, "_plan", None)
|
||
if isinstance(plan, dict):
|
||
try:
|
||
# _DiscoveredPlan entries no longer carry dtype. Two bytes per
|
||
# element matches the BF16 logical view used for packed sources,
|
||
# while source-file bytes remain a lower bound for wider tensors.
|
||
candidates.append(
|
||
sum(_tensor_shape_nbytes(info["shape"], 2) for info in plan.values())
|
||
)
|
||
except Exception:
|
||
pass
|
||
|
||
return max(1, *candidates)
|
||
|
||
|
||
def _imatrix_metadata_path(path: Path) -> Path:
|
||
return path.with_suffix(path.suffix + ".json")
|
||
|
||
|
||
def _source_imatrix_signature(
|
||
source: Path,
|
||
config: dict,
|
||
*,
|
||
num_samples: int,
|
||
seq_length: int,
|
||
calib_dataset: str,
|
||
) -> dict[str, Any]:
|
||
"""Build a cheap invalidation signature for an oQe imatrix cache."""
|
||
stable_config = {k: v for k, v in config.items() if not str(k).startswith("_oq_")}
|
||
cfg_bytes = json.dumps(stable_config, sort_keys=True, default=str).encode("utf-8")
|
||
h = hashlib.sha256(cfg_bytes)
|
||
for sf in sorted(source.glob("*.safetensors")):
|
||
st = sf.stat()
|
||
h.update(sf.name.encode("utf-8"))
|
||
h.update(str(st.st_size).encode("ascii"))
|
||
h.update(str(int(st.st_mtime_ns)).encode("ascii"))
|
||
calib_hash = ""
|
||
if calib_dataset == _OQE_CALIB_DATASET:
|
||
data_path = Path(__file__).parent / "oqe_calibration_data.json"
|
||
elif calib_dataset in ("code_multilingual", "code", "multilingual"):
|
||
data_path = Path(__file__).parent / "oq_calibration_data.json"
|
||
else:
|
||
data_path = None
|
||
if data_path is not None and data_path.exists():
|
||
ch = hashlib.sha256()
|
||
with open(data_path, "rb") as f:
|
||
for block in iter(lambda: f.read(1024 * 1024), b""):
|
||
ch.update(block)
|
||
calib_hash = ch.hexdigest()
|
||
return {
|
||
"format": _OQE_IMATRIX_FORMAT,
|
||
"model_name": source.name,
|
||
"source_hash": h.hexdigest(),
|
||
"calib_dataset": calib_dataset,
|
||
"calib_data_hash": calib_hash,
|
||
"num_samples": int(num_samples),
|
||
"seq_length": int(seq_length),
|
||
}
|
||
|
||
|
||
def _save_oqe_imatrix(
|
||
path: Path,
|
||
entries: dict[str, OQImatrixEntry],
|
||
metadata: dict[str, Any],
|
||
) -> None:
|
||
path.parent.mkdir(parents=True, exist_ok=True)
|
||
arrays = {}
|
||
for name, entry in sorted(entries.items()):
|
||
arrays[f"{name}.in_sum2"] = np.asarray(entry.in_sum2, dtype=np.float32)
|
||
arrays[f"{name}.counts"] = np.asarray(entry.counts, dtype=np.int64)
|
||
np.savez_compressed(path, **arrays)
|
||
_imatrix_metadata_path(path).write_text(
|
||
json.dumps(metadata, indent=2, ensure_ascii=False),
|
||
encoding="utf-8",
|
||
)
|
||
|
||
|
||
def _load_oqe_imatrix(path: Path) -> OQImatrixData:
|
||
metadata_path = _imatrix_metadata_path(path)
|
||
metadata = {}
|
||
if metadata_path.exists():
|
||
metadata = json.loads(metadata_path.read_text(encoding="utf-8"))
|
||
entries: dict[str, OQImatrixEntry] = {}
|
||
with np.load(path, allow_pickle=False) as data:
|
||
names = {
|
||
key[: -len(".in_sum2")] for key in data.files if key.endswith(".in_sum2")
|
||
}
|
||
for name in names:
|
||
counts_key = f"{name}.counts"
|
||
if counts_key not in data.files:
|
||
continue
|
||
entries[name] = OQImatrixEntry(
|
||
in_sum2=np.asarray(data[f"{name}.in_sum2"], dtype=np.float32),
|
||
counts=np.asarray(data[counts_key], dtype=np.int64),
|
||
)
|
||
return OQImatrixData(entries=entries, metadata=metadata, path=str(path))
|
||
|
||
|
||
def _oqe_cache_matches(cache: OQImatrixData, expected: dict[str, Any]) -> bool:
|
||
return all(cache.metadata.get(k) == v for k, v in expected.items())
|
||
|
||
|
||
def _oqe_cache_has_required_expert_coverage(
|
||
cache: OQImatrixData,
|
||
) -> bool:
|
||
collection = cache.metadata.get("collection")
|
||
require_expert_counts = bool(cache.metadata.get("requires_expert_counts", False))
|
||
if isinstance(collection, dict):
|
||
require_expert_counts = bool(
|
||
collection.get("requires_expert_counts", require_expert_counts)
|
||
)
|
||
if not require_expert_counts:
|
||
return True
|
||
coverage = cache.metadata.get("expert_coverage")
|
||
if not isinstance(coverage, dict):
|
||
if isinstance(collection, dict):
|
||
coverage = collection.get("coverage")
|
||
return isinstance(coverage, dict) and bool(coverage.get("has_expert_counts", False))
|
||
|
||
|
||
def _normalised_imatrix_values(entry: OQImatrixEntry) -> np.ndarray:
|
||
sums = np.asarray(entry.in_sum2, dtype=np.float32)
|
||
counts = np.asarray(entry.counts, dtype=np.float32)
|
||
if counts.ndim == 0:
|
||
count = float(counts.item())
|
||
return sums / count if count > 0 else np.ones_like(sums, dtype=np.float32)
|
||
if counts.size == 1:
|
||
count = float(counts.reshape(-1)[0])
|
||
return sums / count if count > 0 else np.ones_like(sums, dtype=np.float32)
|
||
flat = sums.reshape(counts.size, -1)
|
||
denom = counts.reshape(-1, 1)
|
||
values = np.divide(
|
||
flat,
|
||
np.maximum(denom, 1.0),
|
||
out=np.ones_like(flat, dtype=np.float32),
|
||
where=denom > 0,
|
||
)
|
||
return values.reshape(sums.shape)
|
||
|
||
|
||
def _imatrix_expert_coverage_stats(
|
||
entries: dict[str, OQImatrixEntry],
|
||
) -> dict[str, Any]:
|
||
"""Summarise expert coverage across collected SwitchLinear entries."""
|
||
expert_counts = []
|
||
expert_modules = 0
|
||
for entry in entries.values():
|
||
counts = np.asarray(entry.counts, dtype=np.int64).reshape(-1)
|
||
if counts.size <= 1:
|
||
continue
|
||
expert_modules += 1
|
||
expert_counts.append(counts)
|
||
|
||
if not expert_counts:
|
||
return {
|
||
"has_expert_counts": False,
|
||
"expert_modules": 0,
|
||
"total_experts": 0,
|
||
"active_experts": 0,
|
||
"zero_count_experts": 0,
|
||
"active_ratio": 1.0,
|
||
"min_count": 0,
|
||
"p05_count": 0.0,
|
||
"p10_count": 0.0,
|
||
"median_count": 0.0,
|
||
"max_count": 0,
|
||
"min_required_count": _OQE_MIN_EXPERT_COUNT,
|
||
"required_percentile": _OQE_MIN_EXPERT_COUNT_PERCENTILE,
|
||
}
|
||
|
||
counts = np.concatenate(expert_counts)
|
||
total = int(counts.size)
|
||
zero = int((counts <= 0).sum())
|
||
return {
|
||
"has_expert_counts": True,
|
||
"expert_modules": int(expert_modules),
|
||
"total_experts": total,
|
||
"active_experts": total - zero,
|
||
"zero_count_experts": zero,
|
||
"active_ratio": float((total - zero) / max(total, 1)),
|
||
"min_count": int(counts.min()) if total else 0,
|
||
"p05_count": float(np.percentile(counts, 5)) if total else 0.0,
|
||
"p10_count": float(np.percentile(counts, 10)) if total else 0.0,
|
||
"median_count": float(np.percentile(counts, 50)) if total else 0.0,
|
||
"max_count": int(counts.max()) if total else 0,
|
||
"min_required_count": _OQE_MIN_EXPERT_COUNT,
|
||
"required_percentile": _OQE_MIN_EXPERT_COUNT_PERCENTILE,
|
||
}
|
||
|
||
|
||
def _config_expects_moe_expert_counts(config: dict) -> bool:
|
||
"""Return True when the model config describes routed MoE experts."""
|
||
configs = [config]
|
||
text_config = config.get("text_config")
|
||
if isinstance(text_config, dict):
|
||
configs.append(text_config)
|
||
ffn_config = config.get("ffn_config")
|
||
if isinstance(ffn_config, dict):
|
||
configs.append(ffn_config)
|
||
|
||
for cfg in configs:
|
||
for key in (
|
||
"n_routed_experts",
|
||
"num_experts",
|
||
"num_local_experts",
|
||
"moe_num_experts",
|
||
):
|
||
try:
|
||
if int(cfg.get(key) or 0) > 1:
|
||
return True
|
||
except (TypeError, ValueError):
|
||
continue
|
||
return False
|
||
|
||
|
||
def _imatrix_requires_expert_counts(config: dict, switch_capture_modules: int) -> bool:
|
||
return switch_capture_modules > 0 and _config_expects_moe_expert_counts(config)
|
||
|
||
|
||
def _imatrix_expert_coverage_sufficient(
|
||
stats: dict[str, Any],
|
||
*,
|
||
require_expert_counts: bool = False,
|
||
) -> bool:
|
||
if not stats.get("has_expert_counts", False):
|
||
return not require_expert_counts
|
||
required_key = f"p{_OQE_MIN_EXPERT_COUNT_PERCENTILE:02d}_count"
|
||
return (
|
||
int(stats.get("zero_count_experts", 0)) == 0
|
||
and float(stats.get(required_key, 0.0)) >= _OQE_MIN_EXPERT_COUNT
|
||
)
|
||
|
||
|
||
def _lookup_imatrix_importance(
|
||
imatrix: OQImatrixData | None,
|
||
tensor_name: str,
|
||
shape: tuple[int, ...],
|
||
*,
|
||
strict: bool,
|
||
report: dict[str, Any] | None,
|
||
):
|
||
if imatrix is None or not tensor_name.endswith(".weight"):
|
||
return None
|
||
|
||
base = tensor_name[: -len(".weight")]
|
||
entry = imatrix.entries.get(base)
|
||
if entry is None:
|
||
if report is not None:
|
||
report["missing"].append(base)
|
||
if strict:
|
||
raise RuntimeError(f"oQe imatrix missing entry for {base}")
|
||
return None
|
||
|
||
values = _normalised_imatrix_values(entry)
|
||
in_dim = int(shape[-1])
|
||
if values.ndim == 1 and values.shape[0] == in_dim:
|
||
if report is not None:
|
||
report["applied"].append(base)
|
||
return mx.array(values)
|
||
|
||
if values.ndim == 2 and values.shape[-1] == in_dim:
|
||
if len(shape) >= 3 and int(shape[0]) == int(values.shape[0]):
|
||
if report is not None:
|
||
report["applied"].append(base)
|
||
zero_count = int((np.asarray(entry.counts) <= 0).sum())
|
||
report["zero_count_experts"] += zero_count
|
||
return mx.array(values)
|
||
|
||
if report is not None:
|
||
report["mismatched"].append(
|
||
{
|
||
"tensor": base,
|
||
"weight_shape": list(shape),
|
||
"imatrix_shape": list(values.shape),
|
||
}
|
||
)
|
||
if strict:
|
||
raise RuntimeError(
|
||
f"oQe imatrix shape mismatch for {base}: "
|
||
f"weight={shape}, imatrix={values.shape}"
|
||
)
|
||
return None
|
||
|
||
|
||
def _affine_minmax_params(grouped, bits: int):
|
||
n_bins = mx.array((1 << bits) - 1, mx.float32)
|
||
eps = mx.array(1e-7, mx.float32)
|
||
zero = mx.array(0.0, mx.float32)
|
||
w_max = mx.max(grouped, axis=-1, keepdims=True).astype(mx.float32)
|
||
w_min = mx.min(grouped, axis=-1, keepdims=True).astype(mx.float32)
|
||
mask = mx.abs(w_min) > mx.abs(w_max)
|
||
scales = mx.maximum((w_max - w_min) / n_bins, eps)
|
||
scales = mx.where(mask, scales, -scales)
|
||
edge = mx.where(mask, w_min, w_max)
|
||
q0 = mx.round(edge / scales)
|
||
scales = mx.where(q0 != zero, edge / q0, scales)
|
||
biases = mx.where(q0 == zero, zero, edge)
|
||
return scales, biases
|
||
|
||
|
||
def _pack_affine_codes(w, scales, biases, group_size: int, bits: int):
|
||
orig = tuple(w.shape)
|
||
grouped = w.reshape(-1, orig[-1] // group_size, group_size)
|
||
n_bins = mx.array((1 << bits) - 1, mx.float32)
|
||
codes = mx.clip(mx.round((grouped - biases) / scales), 0, n_bins).astype(mx.uint32)
|
||
|
||
el_per_int = 32 // bits
|
||
if bits in (2, 4, 8):
|
||
shifts = mx.power(
|
||
mx.array(2, mx.uint32),
|
||
mx.arange(0, 32, bits, dtype=mx.uint32),
|
||
)
|
||
packed = codes.reshape(codes.shape[0], -1, el_per_int)
|
||
packed = mx.sum(packed * shifts, axis=2)
|
||
else:
|
||
bits_arange = mx.arange(bits, dtype=mx.uint32)
|
||
bit_values = mx.bitwise_and(mx.right_shift(codes[..., None], bits_arange), 1)
|
||
bit_values = bit_values.reshape(codes.shape[0], -1, 32)
|
||
shifts = mx.arange(32, dtype=mx.uint32)
|
||
packed = mx.sum(mx.left_shift(bit_values, shifts), axis=-1)
|
||
|
||
packed_shape = (*orig[:-1], orig[-1] * bits // 32)
|
||
return packed.reshape(packed_shape)
|
||
|
||
|
||
def _weighted_affine_quantize(w, group_size: int, bits: int, importance):
|
||
"""Quantize with a small imatrix-weighted clipping search.
|
||
|
||
The output layout intentionally matches ``mx.quantize(..., mode="affine")``.
|
||
"""
|
||
orig = tuple(w.shape)
|
||
grouped = w.reshape(-1, orig[-1] // group_size, group_size)
|
||
grouped_f = grouped.astype(mx.float32)
|
||
|
||
imp = importance
|
||
if not isinstance(imp, mx.array):
|
||
imp = mx.array(imp)
|
||
if tuple(imp.shape) == (orig[-1],):
|
||
imp = mx.broadcast_to(imp, orig)
|
||
elif len(orig) >= 3 and tuple(imp.shape) == (orig[0], orig[-1]):
|
||
imp = mx.broadcast_to(imp[:, None, :], orig)
|
||
else:
|
||
imp = mx.broadcast_to(imp, orig)
|
||
imp = imp.reshape(grouped.shape).astype(mx.float32)
|
||
imp = mx.maximum(imp, mx.array(1e-8, mx.float32))
|
||
|
||
base_scales, base_biases = _affine_minmax_params(grouped_f, bits)
|
||
best_scales = base_scales
|
||
best_biases = base_biases
|
||
best_codes = mx.clip(
|
||
mx.round((grouped_f - base_biases) / base_scales),
|
||
0,
|
||
mx.array((1 << bits) - 1, mx.float32),
|
||
)
|
||
best_err = mx.sum(
|
||
imp * (grouped_f - (best_codes * base_scales + base_biases)) ** 2,
|
||
axis=-1,
|
||
keepdims=True,
|
||
)
|
||
|
||
n_bins = mx.array((1 << bits) - 1, mx.float32)
|
||
eps = mx.array(1e-7, mx.float32)
|
||
w_min = mx.min(grouped_f, axis=-1, keepdims=True)
|
||
w_max = mx.max(grouped_f, axis=-1, keepdims=True)
|
||
for edge, opposite, sign in ((w_max, w_min, -1.0), (w_min, w_max, 1.0)):
|
||
raw = mx.maximum(mx.abs(edge - opposite) / n_bins, eps) * sign
|
||
q0 = mx.round(edge / raw)
|
||
scale0 = mx.where(q0 != 0, edge / q0, raw)
|
||
bias0 = mx.where(q0 == 0, mx.array(0.0, mx.float32), edge)
|
||
for factor in (0.5, 0.625, 0.75, 0.875, 1.0, 1.125, 1.25):
|
||
scales = scale0 * mx.array(factor, mx.float32)
|
||
biases = bias0
|
||
codes = mx.clip(mx.round((grouped_f - biases) / scales), 0, n_bins)
|
||
recon = codes * scales + biases
|
||
err = mx.sum(imp * (grouped_f - recon) ** 2, axis=-1, keepdims=True)
|
||
take = err < best_err
|
||
best_err = mx.where(take, err, best_err)
|
||
best_scales = mx.where(take, scales, best_scales)
|
||
best_biases = mx.where(take, biases, best_biases)
|
||
|
||
packed = _pack_affine_codes(
|
||
grouped_f.reshape(orig), best_scales, best_biases, group_size, bits
|
||
)
|
||
scale_shape = (*orig[:-1], orig[-1] // group_size)
|
||
scales = best_scales.reshape(scale_shape).astype(w.dtype)
|
||
biases = best_biases.reshape(scale_shape).astype(w.dtype)
|
||
mx.eval(packed, scales, biases)
|
||
return packed, scales, biases
|
||
|
||
|
||
def _importance_is_uniform(importance) -> bool:
|
||
try:
|
||
imp = importance if isinstance(importance, mx.array) else mx.array(importance)
|
||
if imp.size == 0:
|
||
return True
|
||
lo = mx.min(imp.astype(mx.float32))
|
||
hi = mx.max(imp.astype(mx.float32))
|
||
mx.eval(lo, hi)
|
||
return abs(float(hi.item()) - float(lo.item())) <= 1e-8
|
||
except Exception:
|
||
return False
|
||
|
||
|
||
def _row_chunks_with_bounds(t, max_elems):
|
||
rows = t.shape[0]
|
||
if rows == 0:
|
||
return
|
||
epr = max(1, t.size // rows)
|
||
rpc = max(1, max_elems // epr)
|
||
for r0 in range(0, rows, rpc):
|
||
r1 = min(rows, r0 + rpc)
|
||
if isinstance(t, _LazyTensor):
|
||
chunk = t._load_rows(r0, r1)
|
||
else:
|
||
chunk = t[r0:r1]
|
||
mx.eval(chunk)
|
||
yield r0, r1, chunk
|
||
|
||
|
||
def _row_chunks(t, max_elems):
|
||
for _, _, chunk in _row_chunks_with_bounds(t, max_elems):
|
||
yield chunk
|
||
|
||
|
||
def _quantize_chunked(w, group_size, bits, mode, importance=None):
|
||
_MLX_MAX_ELEMS = 1 << 30
|
||
max_elems = max(group_size, min(_QUANTIZE_CHUNK_BYTES // 2, _MLX_MAX_ELEMS))
|
||
if importance is not None and _importance_is_uniform(importance):
|
||
importance = None
|
||
if importance is not None and mode == "affine":
|
||
if not isinstance(w, _LazyTensor) and w.size <= max_elems:
|
||
return _weighted_affine_quantize(w, group_size, bits, importance)
|
||
orig = tuple(w.shape)
|
||
qws, scs, bis = [], [], []
|
||
for r0, r1, chunk in _row_chunks_with_bounds(w, max_elems):
|
||
mx.eval(chunk)
|
||
imp = importance
|
||
if isinstance(importance, mx.array) and importance.ndim > 1:
|
||
if importance.shape[0] == orig[0]:
|
||
imp = importance[r0:r1]
|
||
cqw, csc, cbi = _weighted_affine_quantize(
|
||
chunk,
|
||
group_size,
|
||
bits,
|
||
imp,
|
||
)
|
||
mx.eval(cqw, csc, cbi)
|
||
qws.append(cqw)
|
||
scs.append(csc)
|
||
bis.append(cbi)
|
||
mx.synchronize()
|
||
mx.clear_cache()
|
||
qw = mx.concatenate(qws, axis=0)
|
||
scales = mx.concatenate(scs, axis=0)
|
||
biases = mx.concatenate(bis, axis=0)
|
||
mx.eval(qw, scales, biases)
|
||
flat_rows = 1
|
||
for d in orig[:-1]:
|
||
flat_rows *= d
|
||
if qw.shape[0] == flat_rows and len(orig) > 2:
|
||
qw = qw.reshape(*orig[:-1], -1)
|
||
scales = scales.reshape(*orig[:-1], -1)
|
||
biases = biases.reshape(*orig[:-1], -1)
|
||
return qw, scales, biases
|
||
|
||
if not isinstance(w, _LazyTensor) and w.size <= max_elems:
|
||
qw, scales, *rest = mx.quantize(w, group_size=group_size, bits=bits, mode=mode)
|
||
return qw, scales, (rest[0] if rest else None)
|
||
orig = tuple(w.shape)
|
||
qws, scs, bis = [], [], []
|
||
for chunk in _row_chunks(w, max_elems):
|
||
flat = chunk.reshape(-1, chunk.shape[-1])
|
||
mx.eval(flat)
|
||
cqw, csc, *crest = mx.quantize(
|
||
flat, group_size=group_size, bits=bits, mode=mode
|
||
)
|
||
mx.eval(cqw, csc)
|
||
qws.append(cqw)
|
||
scs.append(csc)
|
||
if crest:
|
||
bis.append(crest[0])
|
||
mx.synchronize()
|
||
mx.clear_cache()
|
||
qw = mx.concatenate(qws, axis=0)
|
||
scales = mx.concatenate(scs, axis=0)
|
||
biases = mx.concatenate(bis, axis=0) if bis else None
|
||
mx.eval(qw, scales)
|
||
flat_rows = 1
|
||
for d in orig[:-1]:
|
||
flat_rows *= d
|
||
if qw.shape[0] == flat_rows and len(orig) > 2:
|
||
qw = qw.reshape(*orig[:-1], -1)
|
||
scales = scales.reshape(*orig[:-1], -1)
|
||
if biases is not None:
|
||
biases = biases.reshape(*orig[:-1], -1)
|
||
return qw, scales, biases
|
||
|
||
|
||
# --- end chunked-quantize helpers ---
|
||
|
||
|
||
def quantize_oq_streaming(
|
||
model_path: str,
|
||
output_path: str,
|
||
oq_level: int,
|
||
group_size: int = 64,
|
||
progress_callback: Optional[Callable[[str, float], None]] = None,
|
||
text_only: bool = False,
|
||
target_bpw: float | None = None,
|
||
hard_cap_bpw: float | None = None,
|
||
sensitivity_model_path: str = "",
|
||
dtype: str = "bfloat16",
|
||
preserve_mtp: bool = False,
|
||
auto_proxy_sensitivity: bool = True,
|
||
trust_remote_code: bool = False,
|
||
enhanced: bool = False,
|
||
imatrix_cache_path: str = "",
|
||
imatrix_reuse_cache: bool = True,
|
||
imatrix_strict: bool = False,
|
||
imatrix_num_samples: int = 128,
|
||
imatrix_seq_length: int = 512,
|
||
) -> None:
|
||
"""Tensor-by-tensor quantization. Memory: ~3-4GB regardless of model size.
|
||
|
||
Reads tensors one at a time from safetensors, quantizes with the universal
|
||
predicate, and writes output shards. Never loads the full model.
|
||
|
||
Args:
|
||
model_path: Path to source model directory.
|
||
output_path: Path for output (must not exist).
|
||
oq_level: Quantization level from OQ_LEVELS.
|
||
group_size: Default quantization group size.
|
||
progress_callback: Optional fn(phase_name, progress_pct) for updates.
|
||
text_only: Skip vision encoder weights for VLM models.
|
||
dtype: Target fp dtype for non-quantized weights and quant scales/biases.
|
||
Must be "bfloat16" (default) or "float16". float16 yields ~20%
|
||
faster prefill on M1/M2 Apple Silicon (native fp16 support), but
|
||
is unsupported for DeepSeek V4.
|
||
preserve_mtp: Keep mtp.* tensors and config fields in the output so
|
||
the Lightning MTP toggle works after quantization. Stashes mtp.*
|
||
keys around the model.sanitize() call (which would otherwise
|
||
strip them) and re-merges. When False (default), mtp.* tensors
|
||
are stripped *and* the output config's mtp_num_hidden_layers /
|
||
num_nextn_predict_layers are normalized to 0 to keep the
|
||
quantized model self-consistent.
|
||
auto_proxy_sensitivity: When True (default) and the source model
|
||
exceeds available RAM, automatically build a temporary uniform
|
||
4-bit proxy on disk and run sensitivity measurement on it,
|
||
preserving oQ's data-driven mixed-precision allocation. When
|
||
False, the quantization aborts on RAM-exceeding models with a
|
||
RuntimeError so callers always get a real sensitivity-driven
|
||
output. Ignored if sensitivity_model_path is set explicitly.
|
||
trust_remote_code: Forwarded to mlx-lm/mlx-vlm model loads when a
|
||
checkpoint requires custom model code.
|
||
enhanced: Enable oQe imatrix-weighted quantization. When False, the
|
||
existing oQ streaming path is unchanged.
|
||
imatrix_cache_path: oQe native imatrix cache path. Required when
|
||
enhanced is True; the admin manager supplies an automatic path.
|
||
imatrix_reuse_cache: Reuse a compatible imatrix cache if present.
|
||
imatrix_strict: Fail on missing or mismatched imatrix entries instead
|
||
of falling back to standard oQ quantization for those tensors.
|
||
imatrix_num_samples: Calibration sample count for imatrix collection.
|
||
imatrix_seq_length: Calibration sequence length for imatrix collection.
|
||
"""
|
||
if oq_level not in OQ_LEVELS:
|
||
raise ValueError(
|
||
f"Invalid oQ level {oq_level}. Must be one of {sorted(OQ_LEVELS)}"
|
||
)
|
||
if dtype not in OQ_DTYPES:
|
||
raise ValueError(f"Invalid dtype {dtype!r}. Must be one of {OQ_DTYPES}")
|
||
target_dtype = mx.bfloat16 if dtype == "bfloat16" else mx.float16
|
||
|
||
source = Path(model_path)
|
||
output = Path(output_path)
|
||
if output.exists():
|
||
raise ValueError(f"Output directory already exists: {output_path}")
|
||
|
||
_progress_last_log = {"time": 0.0, "pct": -100.0, "detail": ""}
|
||
|
||
def cb(
|
||
phase: str,
|
||
pct: float,
|
||
detail: str = "",
|
||
meta: dict[str, Any] | None = None,
|
||
) -> None:
|
||
_emit_progress(progress_callback, phase, pct, detail, meta)
|
||
now = _time.monotonic()
|
||
should_log = (
|
||
pct >= 100.0
|
||
or pct - float(_progress_last_log["pct"]) >= 2.0
|
||
or now - float(_progress_last_log["time"]) >= 10.0
|
||
or (detail and detail != _progress_last_log["detail"])
|
||
)
|
||
if should_log:
|
||
message = detail or phase
|
||
logger.info("oQ%s progress %.1f%%: %s", f"{oq_level:g}", pct, message)
|
||
_progress_last_log.update({"time": now, "pct": pct, "detail": detail})
|
||
|
||
config_path = source / "config.json"
|
||
with open(config_path) as f:
|
||
config = json.load(f)
|
||
_validate_oq_dtype_for_model(config, dtype)
|
||
config["_oq_use_budget_plan"] = oq_level in _OQ_BPW_TARGETS
|
||
|
||
output.mkdir(parents=True, exist_ok=True)
|
||
|
||
cb("loading", 5.0, "Reading model config")
|
||
|
||
weight_files = sorted(source.glob("*.safetensors"))
|
||
if not weight_files:
|
||
raise ValueError(f"No .safetensors files found in {model_path}")
|
||
|
||
cb("loading", 8.0, "Indexing source weights")
|
||
|
||
all_weights = _LazyTensorIndex(weight_files)
|
||
if (
|
||
preserve_mtp
|
||
and not any(_is_mtp_tensor(k) for k in all_weights.keys())
|
||
and not _source_has_nextn_tensors(all_weights.keys(), config)
|
||
):
|
||
logger.warning(
|
||
"Preserve MTP requested for %s, but no mtp.* tensors were found "
|
||
"in the checkpoint; disabling MTP preservation",
|
||
source.name,
|
||
)
|
||
preserve_mtp = False
|
||
|
||
logger.info(
|
||
f"oQ{oq_level:g} streaming: {len(all_weights)} tensors in "
|
||
f"{len(weight_files)} shards"
|
||
)
|
||
|
||
sensitivity_map_path = Path(model_path, "oq_sensitivity_map.json")
|
||
from omlx.settings import get_system_memory as _get_system_memory
|
||
|
||
_model_bytes = all_weights.nbytes()
|
||
_system_ram = _get_system_memory()
|
||
_model_exceeds_ram = _model_bytes > int(_system_ram * _MAX_MODEL_RAM_FRACTION)
|
||
if _model_exceeds_ram:
|
||
logger.info(
|
||
f"oQ{oq_level:g}: model size ({_model_bytes / 1e9:.1f} GB) exceeds "
|
||
f"80% of system RAM ({_system_ram / 1e9:.1f} GB), "
|
||
"OOM-prone paths will be skipped"
|
||
)
|
||
|
||
# RAM-safe calibration proxy, shared between oQe imatrix collection and
|
||
# auto-proxy sensitivity so an exceeds-RAM run builds it at most once.
|
||
# Built lazily (an imatrix cache hit never pays for it) and deleted as
|
||
# soon as the last calibration pass is done.
|
||
_ram_safe_proxy_dir: Path | None = None
|
||
|
||
def _ensure_ram_safe_proxy() -> Path:
|
||
nonlocal _ram_safe_proxy_dir
|
||
if _ram_safe_proxy_dir is None:
|
||
logger.warning(
|
||
f"oQ{oq_level:g}: model size ({_model_bytes / 1e9:.1f} GB) "
|
||
f"exceeds {int(_MAX_MODEL_RAM_FRACTION * 100)}% of system RAM "
|
||
f"({_system_ram / 1e9:.1f} GB). Building a uniform "
|
||
f"{_PROXY_QUANT_BITS}-bit proxy on disk for the calibration "
|
||
"passes."
|
||
)
|
||
_ram_safe_proxy_dir = _build_proxy_for_sensitivity(
|
||
model_path,
|
||
config=config,
|
||
dtype=dtype,
|
||
working_dir=str(output.parent),
|
||
trust_remote_code=trust_remote_code,
|
||
preserve_mtp=preserve_mtp,
|
||
)
|
||
return _ram_safe_proxy_dir
|
||
|
||
def _cleanup_ram_safe_proxy() -> None:
|
||
nonlocal _ram_safe_proxy_dir
|
||
if _ram_safe_proxy_dir is not None and _ram_safe_proxy_dir.exists():
|
||
shutil.rmtree(_ram_safe_proxy_dir, ignore_errors=True)
|
||
logger.info(
|
||
f"oQ{oq_level:g}: cleaned up calibration proxy at "
|
||
f"{_ram_safe_proxy_dir}"
|
||
)
|
||
_ram_safe_proxy_dir = None
|
||
|
||
cb("loading", 12.0, "Preparing quantization inputs")
|
||
|
||
imatrix_data: OQImatrixData | None = None
|
||
imatrix_report: dict[str, Any] | None = None
|
||
if enhanced:
|
||
if imatrix_num_samples < 1:
|
||
raise ValueError("imatrix_num_samples must be >= 1")
|
||
if imatrix_seq_length < 1:
|
||
raise ValueError("imatrix_seq_length must be >= 1")
|
||
if not imatrix_cache_path:
|
||
imatrix_cache_path = str(
|
||
output.parent
|
||
/ ".oqe_imatrix"
|
||
/ (
|
||
f"{source.name}-oQe-s{int(imatrix_num_samples)}"
|
||
f"-l{int(imatrix_seq_length)}.npz"
|
||
)
|
||
)
|
||
cb("imatrix", 13.0, "Preparing oQe imatrix calibration")
|
||
|
||
def _imatrix_load_path() -> str:
|
||
cb(
|
||
"imatrix",
|
||
13.0,
|
||
"Building RAM-safe proxy for imatrix calibration",
|
||
)
|
||
return str(_ensure_ram_safe_proxy())
|
||
|
||
try:
|
||
imatrix_data = _load_or_collect_imatrix(
|
||
model_path,
|
||
config,
|
||
cache_path=imatrix_cache_path,
|
||
reuse_cache=imatrix_reuse_cache,
|
||
num_samples=int(imatrix_num_samples),
|
||
seq_length=int(imatrix_seq_length),
|
||
strict=imatrix_strict,
|
||
trust_remote_code=trust_remote_code,
|
||
progress_callback=cb,
|
||
progress_start=13.0,
|
||
progress_end=18.0,
|
||
load_path_factory=(
|
||
_imatrix_load_path if _model_exceeds_ram else None
|
||
),
|
||
)
|
||
except BaseException:
|
||
_cleanup_ram_safe_proxy()
|
||
raise
|
||
cb("imatrix", 18.0, "oQe imatrix calibration ready")
|
||
imatrix_report = {
|
||
"enabled": True,
|
||
"cache_path": imatrix_data.path,
|
||
"cache_reused": imatrix_data.reused,
|
||
"entry_count": len(imatrix_data.entries),
|
||
"calib_dataset": imatrix_data.metadata.get(
|
||
"calib_dataset", _OQE_CALIB_DATASET
|
||
),
|
||
"collection": imatrix_data.metadata.get("collection", {}),
|
||
"expert_coverage": imatrix_data.metadata.get("expert_coverage", {}),
|
||
"applied": [],
|
||
"missing": [],
|
||
"mismatched": [],
|
||
"zero_count_experts": 0,
|
||
}
|
||
|
||
if sensitivity_map_path.exists():
|
||
sensitivity_map = json.loads(sensitivity_map_path.read_text(encoding="utf-8"))
|
||
logger.info(f"{sensitivity_map_path} found, skipping measuring.")
|
||
else:
|
||
# --- Sensitivity measurement (before sanitize-plan discovery) ---------
|
||
# Must run before _build_model_sanitizer + _discover_sanitize_plan,
|
||
# because the discovery pass feeds _TrackedTensor proxies through
|
||
# Model.sanitize which corrupts mutable state in the MTP sanitize
|
||
# patch (weights.pop on tracked objects). Running sensitivity first
|
||
# ensures vlm_load_model sees a pristine patch chain.
|
||
if sensitivity_model_path:
|
||
logger.info(f"oQ{oq_level:g}: measuring sensitivity via proxy model")
|
||
sensitivity_map = _measure_sensitivity_from_quantized_model(
|
||
sensitivity_model_path,
|
||
config,
|
||
oq_level,
|
||
num_samples=128,
|
||
seq_length=256,
|
||
trust_remote_code=trust_remote_code,
|
||
)
|
||
elif (
|
||
not _model_exceeds_ram
|
||
and str(config.get("model_type", "")).startswith("deepseek_v4")
|
||
and isinstance(config.get("quantization_config"), dict)
|
||
and config["quantization_config"].get("quant_method") == "fp8"
|
||
):
|
||
# Native-fp8 source (e.g. DeepSeek-V4-Flash): the checkpoint
|
||
# loads as a quantized model (mxfp4 experts / mxfp8 attention),
|
||
# so the raw qdq measurement would only perturb the few float
|
||
# Linears. Measure on the source itself with the re-quantization
|
||
# perturbation instead.
|
||
logger.info(
|
||
f"oQ{oq_level:g}: pre-quantized fp8 source, measuring "
|
||
"sensitivity on source"
|
||
)
|
||
sensitivity_map = _measure_sensitivity_from_quantized_model(
|
||
model_path,
|
||
config,
|
||
oq_level,
|
||
num_samples=128,
|
||
seq_length=256,
|
||
trust_remote_code=trust_remote_code,
|
||
)
|
||
elif _model_exceeds_ram and auto_proxy_sensitivity:
|
||
logger.warning(
|
||
f"oQ{oq_level:g}: model size ({_model_bytes / 1e9:.1f} GB) exceeds "
|
||
f"{int(_MAX_MODEL_RAM_FRACTION * 100)}% of system RAM "
|
||
f"({_system_ram / 1e9:.1f} GB). Auto-building a uniform "
|
||
f"{_PROXY_QUANT_BITS}-bit proxy on disk so sensitivity "
|
||
"measurement stays data-driven."
|
||
)
|
||
try:
|
||
_proxy_dir = _ensure_ram_safe_proxy()
|
||
logger.info(
|
||
f"oQ{oq_level:g}: proxy ready at {_proxy_dir}, measuring sensitivity"
|
||
)
|
||
sensitivity_map = _measure_sensitivity_from_quantized_model(
|
||
str(_proxy_dir),
|
||
config,
|
||
oq_level,
|
||
num_samples=128,
|
||
seq_length=256,
|
||
trust_remote_code=trust_remote_code,
|
||
)
|
||
except Exception as e:
|
||
raise RuntimeError(
|
||
f"oQ{oq_level:g}: auto-proxy sensitivity failed ({e}). "
|
||
"Pass sensitivity_model_path with a pre-quantized version "
|
||
"of this model, or run on a machine with enough RAM for "
|
||
"full-fp16 sensitivity measurement."
|
||
) from e
|
||
finally:
|
||
_cleanup_ram_safe_proxy()
|
||
elif _model_exceeds_ram:
|
||
raise RuntimeError(
|
||
f"oQ{oq_level:g}: model exceeds {int(_MAX_MODEL_RAM_FRACTION * 100)}% "
|
||
"of system RAM and auto_proxy_sensitivity is disabled. "
|
||
"Enable auto_proxy_sensitivity, pass sensitivity_model_path "
|
||
"with a pre-quantized version of this model, or run on a "
|
||
"machine with enough RAM."
|
||
)
|
||
else:
|
||
logger.info(
|
||
f"oQ{oq_level:g}: measuring layer sensitivity for streaming path"
|
||
)
|
||
sensitivity_map = _measure_sensitivity(
|
||
model_path,
|
||
config,
|
||
oq_level,
|
||
num_samples=128,
|
||
seq_length=256,
|
||
trust_remote_code=trust_remote_code,
|
||
)
|
||
|
||
# Single enforcement point. Inner measurement helpers may return {} on
|
||
# load / calibration / layer-discovery failure; treat that as a hard
|
||
# error here so the rest of quantize_oq_streaming never runs without a
|
||
# data-driven sensitivity map.
|
||
if not sensitivity_map:
|
||
_cleanup_ram_safe_proxy()
|
||
raise RuntimeError(
|
||
f"oQ{oq_level:g}: sensitivity measurement produced no scores. "
|
||
"Check the preceding log lines for the root cause (model load, "
|
||
"calibration data, or layer discovery), and either fix it or "
|
||
"pass an explicit sensitivity_model_path."
|
||
)
|
||
|
||
# Calibration passes are done — drop the RAM-safe proxy (built when the
|
||
# source exceeds system RAM; a cached sensitivity map plus an imatrix
|
||
# cache hit means it was never built at all).
|
||
_cleanup_ram_safe_proxy()
|
||
|
||
cb(
|
||
"loading",
|
||
19.0 if enhanced else 15.0,
|
||
"Preparing quantization plan",
|
||
)
|
||
|
||
# --- Sanitize-plan discovery ------------------------------------------
|
||
sanitize_fn = _build_model_sanitizer(config, text_only=text_only)
|
||
cast_predicate = getattr(sanitize_fn, "_omlx_cast_predicate", None)
|
||
# When preserve_mtp is True, the patched sanitize functions
|
||
# (mlx_lm_mtp/qwen35_model.py and mlx_vlm_mtp/qwen35_vlm_model.py)
|
||
# keep mtp.* in the output and apply the +1 RMSNorm shift to MTP
|
||
# norms. No stash/merge wrapper needed — the patch covers both paths.
|
||
if sanitize_fn is not None:
|
||
try:
|
||
plan = _discover_sanitize_plan(sanitize_fn, all_weights)
|
||
all_weights = _DiscoveredPlan(plan, all_weights)
|
||
logger.info(
|
||
f"oQ{oq_level:g}: discovered streaming sanitize plan, "
|
||
f"{len(all_weights)} output tensors"
|
||
)
|
||
except Exception as e:
|
||
if _model_exceeds_ram:
|
||
raise RuntimeError(
|
||
f"oQ{oq_level:g}: streaming sanitize-plan discovery "
|
||
f"failed ({e}) and the eager fallback is unsafe with "
|
||
f"model size {_model_bytes / 1e9:.1f} GB exceeding "
|
||
f"{int(_MAX_MODEL_RAM_FRACTION * 100)}% of system RAM "
|
||
f"({_system_ram / 1e9:.1f} GB). Run on a machine with "
|
||
"enough RAM, or extend _TrackedTensor to cover the "
|
||
"indexing pattern the sanitize uses."
|
||
) from e
|
||
logger.warning(
|
||
f"Streaming discovery failed ({e}), falling back to eager sanitize"
|
||
)
|
||
try:
|
||
all_weights = sanitize_fn(all_weights)
|
||
logger.info(
|
||
f"oQ{oq_level:g}: eager sanitize applied, {len(all_weights)} tensors"
|
||
)
|
||
except Exception as e2:
|
||
logger.warning(f"Sanitize failed ({e2}), using original names")
|
||
|
||
config["_oq_non_quantizable"] = _build_non_quantizable_set(config)
|
||
config["_oq_sensitivity_map"] = {str(k): v for k, v in sensitivity_map.items()}
|
||
logger.info(f"oQ{oq_level:g}: sensitivity applied ({len(sensitivity_map)} layers)")
|
||
|
||
named_shapes = _collect_named_weight_shapes_from_weights(all_weights)
|
||
if text_only:
|
||
named_shapes = {
|
||
k: v
|
||
for k, v in named_shapes.items()
|
||
if not _is_vision_tensor(k) and not _is_audio_tensor(k)
|
||
}
|
||
if not preserve_mtp:
|
||
# Match the eager path (_should_skip_tensor): when MTP heads are
|
||
# not being preserved, drop ``mtp.*`` tensors from the plan so the
|
||
# quantizer doesn't reserve bits for them and the output shards
|
||
# don't include them. Otherwise the output would carry the source
|
||
# mtp.* weights while the config's mtp_num_hidden_layers gets
|
||
# zeroed by _normalize_mtp_in_config — a config/weights mismatch
|
||
# that breaks VLM load with "Received N parameters not in model".
|
||
named_shapes = {k: v for k, v in named_shapes.items() if not _is_mtp_tensor(k)}
|
||
# Pre-quantized source tensors whose pre-boost target bits already cover
|
||
# the source precision are passed through in their packed form. Price
|
||
# them at their true cost and keep them out of the boost competition.
|
||
# Boosts only ever raise bits, so the passthrough decision is monotone.
|
||
fixed_overrides = {}
|
||
if hasattr(all_weights, "pop_packed"):
|
||
_pre_boost_config = {**config, "_oq_boost_map": {}}
|
||
for _path in named_shapes:
|
||
_info = all_weights.source_quant_info(f"{_path}.weight")
|
||
if _info is None:
|
||
continue
|
||
_floor_bits, _, _ = _get_predicate_bits(
|
||
f"{_path}.weight", _pre_boost_config, oq_level, group_size
|
||
)
|
||
if _floor_bits is not None and _floor_bits >= _info["bits"]:
|
||
fixed_overrides[_path] = {
|
||
"bits": _info["bits"],
|
||
"group_size": _info["group_size"],
|
||
"mode": _info["mode"],
|
||
}
|
||
if fixed_overrides:
|
||
logger.info(
|
||
f"oQ{oq_level:g}: {len(fixed_overrides)} pre-quantized tensors "
|
||
"will pass through in source precision"
|
||
)
|
||
_level_targets = _bpw_targets_for_level(oq_level)
|
||
if _level_targets is not None:
|
||
_t = target_bpw if target_bpw is not None else _level_targets[0]
|
||
_c = hard_cap_bpw if hard_cap_bpw is not None else _level_targets[1]
|
||
plan = _build_quant_plan(
|
||
named_shapes,
|
||
config,
|
||
oq_level,
|
||
target_bpw=_t,
|
||
hard_cap_bpw=_c,
|
||
fixed_overrides=fixed_overrides,
|
||
)
|
||
config["_oq_boost_map"] = plan.boost_map
|
||
logger.info(
|
||
f"oQ{oq_level:g}: quant plan -> {plan.effective_bpw:.2f} bpw "
|
||
f"with {len(plan.boost_map)} boosts"
|
||
)
|
||
else:
|
||
config["_oq_boost_map"] = {}
|
||
|
||
cb("loading", 20.0, "Starting tensor quantization")
|
||
|
||
tensor_names = list(all_weights.keys())
|
||
out_shard_data = {}
|
||
out_shard_idx = 0
|
||
weight_map = {}
|
||
base_bits = _base_bits_for_level(oq_level)
|
||
base_mode = _mode_for_bits(base_bits)
|
||
base_gs = _gs_for_mode(base_bits, group_size)
|
||
quantization_config = {"group_size": base_gs, "bits": base_bits, "mode": base_mode}
|
||
per_layer_config = {}
|
||
start_time = _time.monotonic()
|
||
last_quant_display_pct = -1
|
||
last_quant_emit_time = 0.0
|
||
|
||
total_bytes = _progress_total_bytes(all_weights, source)
|
||
processed_bytes = 0
|
||
|
||
for tensor_name in tensor_names:
|
||
# Pre-quantized source tensor at or below the target precision:
|
||
# emit the packed mxfp4/mxfp8 form unchanged (no dequant-requant).
|
||
handled_packed = False
|
||
if (
|
||
hasattr(all_weights, "pop_packed")
|
||
and not (
|
||
text_only
|
||
and (_is_vision_tensor(tensor_name) or _is_audio_tensor(tensor_name))
|
||
)
|
||
and not (not preserve_mtp and _is_mtp_tensor(tensor_name))
|
||
):
|
||
src_info = all_weights.source_quant_info(tensor_name)
|
||
if src_info is not None and _should_quantize_tensor(
|
||
tensor_name, all_weights.plan_shape(tensor_name)
|
||
):
|
||
bits, gs, qmode = _get_predicate_bits(
|
||
tensor_name, config, oq_level, group_size
|
||
)
|
||
if bits is not None and bits >= src_info["bits"]:
|
||
qw, scales = all_weights.pop_packed(tensor_name)
|
||
tensor_bytes = qw.nbytes + scales.nbytes
|
||
base = tensor_name[: -len(".weight")]
|
||
out_shard_data[f"{base}.weight"] = qw
|
||
out_shard_data[f"{base}.scales"] = scales
|
||
per_layer_config[base] = {
|
||
"bits": src_info["bits"],
|
||
"group_size": src_info["group_size"],
|
||
"mode": src_info["mode"],
|
||
}
|
||
del qw, scales
|
||
handled_packed = True
|
||
|
||
if not handled_packed:
|
||
w_mx = all_weights.pop(tensor_name)
|
||
if isinstance(w_mx, _LazyTensor):
|
||
w_mx = w_mx[:]
|
||
tensor_bytes = w_mx.nbytes
|
||
shape = w_mx.shape
|
||
|
||
if text_only and (
|
||
_is_vision_tensor(tensor_name) or _is_audio_tensor(tensor_name)
|
||
):
|
||
del w_mx
|
||
processed_bytes += tensor_bytes
|
||
continue
|
||
|
||
if not preserve_mtp and _is_mtp_tensor(tensor_name):
|
||
# Strip MTP tensors when the caller asked not to preserve them.
|
||
# _normalize_mtp_in_config will zero mtp_num_hidden_layers in
|
||
# the output config so the result stays self-consistent.
|
||
del w_mx
|
||
processed_bytes += tensor_bytes
|
||
continue
|
||
|
||
if _should_quantize_tensor(tensor_name, shape):
|
||
bits, gs, qmode = _get_predicate_bits(
|
||
tensor_name, config, oq_level, group_size
|
||
)
|
||
|
||
if bits is not None and len(shape) >= 2 and shape[-1] % gs == 0:
|
||
# Cast to target dtype before quantize: scales/biases inherit
|
||
# the input dtype, which drives inference speed on Apple
|
||
# Silicon (M1/M2 prefer float16, M3/M4 handle both).
|
||
if (
|
||
mx.issubdtype(w_mx.dtype, mx.floating)
|
||
and w_mx.dtype != target_dtype
|
||
):
|
||
w_mx = w_mx.astype(target_dtype)
|
||
importance = None
|
||
if imatrix_data is not None and qmode == "affine":
|
||
importance = _lookup_imatrix_importance(
|
||
imatrix_data,
|
||
tensor_name,
|
||
tuple(shape),
|
||
strict=imatrix_strict,
|
||
report=imatrix_report,
|
||
)
|
||
qw, scales, biases = _quantize_chunked(
|
||
w_mx,
|
||
gs,
|
||
bits,
|
||
qmode,
|
||
importance=importance,
|
||
)
|
||
|
||
base = tensor_name
|
||
if base.endswith(".weight"):
|
||
base = base[:-7]
|
||
|
||
out_shard_data[f"{base}.weight"] = qw
|
||
out_shard_data[f"{base}.scales"] = scales
|
||
if biases is not None:
|
||
out_shard_data[f"{base}.biases"] = biases
|
||
|
||
base_qmode = _mode_for_bits(base_bits)
|
||
base_gs_check = _gs_for_mode(base_bits, group_size)
|
||
if bits != base_bits or gs != base_gs_check or qmode != base_qmode:
|
||
layer_cfg = {"bits": bits, "group_size": gs}
|
||
layer_cfg["mode"] = qmode
|
||
per_layer_config[base] = layer_cfg
|
||
else:
|
||
if cast_predicate is None or cast_predicate(tensor_name):
|
||
w_mx = _cast_passthrough_tensor(tensor_name, w_mx, target_dtype)
|
||
out_shard_data[tensor_name] = w_mx
|
||
else:
|
||
if cast_predicate is None or cast_predicate(tensor_name):
|
||
w_mx = _cast_passthrough_tensor(tensor_name, w_mx, target_dtype)
|
||
out_shard_data[tensor_name] = w_mx
|
||
|
||
del w_mx
|
||
|
||
current_bytes = sum(v.nbytes for v in out_shard_data.values())
|
||
if current_bytes >= _MAX_SHARD_BYTES:
|
||
shard_name = f"model-{out_shard_idx + 1:05d}-of-PLACEHOLDER.safetensors"
|
||
shard_path = output / shard_name
|
||
mx.save_safetensors(
|
||
str(shard_path), out_shard_data, metadata={"format": "mlx"}
|
||
)
|
||
for k in out_shard_data:
|
||
weight_map[k] = shard_name
|
||
out_shard_idx += 1
|
||
out_shard_data = {}
|
||
mx.synchronize()
|
||
mx.clear_cache()
|
||
logger.info(f"oQ{oq_level:g}: wrote output shard {out_shard_idx}")
|
||
|
||
processed_bytes += tensor_bytes
|
||
elapsed = _time.monotonic() - start_time
|
||
frac = min(max(processed_bytes / max(total_bytes, 1), 0.0), 1.0)
|
||
pct = 20.0 + frac * 70.0
|
||
display_pct = min(100, max(0, int(frac * 100)))
|
||
if (
|
||
display_pct != last_quant_display_pct
|
||
or elapsed - last_quant_emit_time >= 2.0
|
||
or frac >= 1.0
|
||
):
|
||
last_quant_display_pct = display_pct
|
||
last_quant_emit_time = elapsed
|
||
if elapsed > 1.0 and frac > 0.01:
|
||
eta_secs = max(0.0, elapsed / frac * (1.0 - frac))
|
||
mins = int(eta_secs // 60)
|
||
secs = int(eta_secs % 60)
|
||
cb(
|
||
f"quantizing_eta|{display_pct}|100|{mins}:{secs:02d}",
|
||
pct,
|
||
(
|
||
f"Quantized {display_pct}% of tensor bytes; "
|
||
f"ETA {mins}:{secs:02d}"
|
||
),
|
||
{
|
||
"processed_bytes": processed_bytes,
|
||
"total_bytes": total_bytes,
|
||
"eta_seconds": int(eta_secs),
|
||
},
|
||
)
|
||
else:
|
||
cb(
|
||
f"quantizing_eta|{display_pct}|100|",
|
||
pct,
|
||
f"Quantized {display_pct}% of tensor bytes",
|
||
{"processed_bytes": processed_bytes, "total_bytes": total_bytes},
|
||
)
|
||
|
||
del all_weights
|
||
mx.synchronize()
|
||
mx.clear_cache()
|
||
|
||
if out_shard_data:
|
||
total_shards = out_shard_idx + 1
|
||
if total_shards == 1:
|
||
shard_name = "model.safetensors"
|
||
else:
|
||
shard_name = f"model-{out_shard_idx + 1:05d}-of-PLACEHOLDER.safetensors"
|
||
shard_path = output / shard_name
|
||
mx.save_safetensors(str(shard_path), out_shard_data, metadata={"format": "mlx"})
|
||
for k in out_shard_data:
|
||
weight_map[k] = shard_name
|
||
out_shard_idx += 1
|
||
del out_shard_data
|
||
|
||
total_shards = out_shard_idx
|
||
if total_shards > 1:
|
||
for i in range(total_shards):
|
||
old_name = f"model-{i + 1:05d}-of-PLACEHOLDER.safetensors"
|
||
new_name = f"model-{i + 1:05d}-of-{total_shards:05d}.safetensors"
|
||
old_path = output / old_name
|
||
new_path = output / new_name
|
||
if old_path.exists():
|
||
old_path.rename(new_path)
|
||
for k, v in weight_map.items():
|
||
if v == old_name:
|
||
weight_map[k] = new_name
|
||
|
||
cb("saving", 92.0, "Writing model metadata")
|
||
|
||
if total_shards > 1:
|
||
total_size = sum(f.stat().st_size for f in output.glob("*.safetensors"))
|
||
index = {
|
||
"metadata": {"total_size": total_size},
|
||
"weight_map": dict(sorted(weight_map.items())),
|
||
}
|
||
with open(output / "model.safetensors.index.json", "w") as f:
|
||
json.dump(index, f, indent=2)
|
||
|
||
output_config = dict(config)
|
||
for temp_key in (
|
||
"_oq_sensitivity_map",
|
||
"_oq_boost_map",
|
||
"_oq_use_budget_plan",
|
||
"_oq_non_quantizable",
|
||
):
|
||
output_config.pop(temp_key, None)
|
||
if text_only:
|
||
for key in (
|
||
"vision_config",
|
||
"image_token_id",
|
||
"video_token_id",
|
||
"vision_start_token_id",
|
||
"vision_end_token_id",
|
||
"audio_config",
|
||
"audio_token_id",
|
||
"boa_token_id",
|
||
"eoa_token_id",
|
||
"eoa_token_index",
|
||
):
|
||
output_config.pop(key, None)
|
||
if not preserve_mtp:
|
||
# Default path: zero out MTP layer counts so the quantized model
|
||
# doesn't claim to have an MTP head while its weights have been
|
||
# stripped. This keeps the output self-consistent — mtp_enabled
|
||
# toggle's compatibility check (_has_mtp_heads) reads these
|
||
# fields and will correctly report "no MTP heads" instead of
|
||
# crashing during model.load_weights() with the cryptic
|
||
# "Missing N parameters" error.
|
||
_normalize_mtp_in_config(output_config)
|
||
# Ensure eos_token_id is present (mlx-lm adds it from tokenizer)
|
||
if "eos_token_id" not in output_config:
|
||
try:
|
||
from transformers import AutoTokenizer
|
||
|
||
_tok = AutoTokenizer.from_pretrained(str(source))
|
||
if hasattr(_tok, "eos_token_id") and _tok.eos_token_id is not None:
|
||
# Some models have multiple EOS tokens
|
||
eos_ids = getattr(_tok, "additional_special_tokens_ids", [])
|
||
if _tok.eos_token_id not in eos_ids:
|
||
eos_ids = [_tok.eos_token_id] + eos_ids
|
||
# Check generation_config for eos_token_id list
|
||
gen_config_path = source / "generation_config.json"
|
||
if gen_config_path.exists():
|
||
with open(gen_config_path) as f:
|
||
gen_cfg = json.load(f)
|
||
if "eos_token_id" in gen_cfg:
|
||
output_config["eos_token_id"] = gen_cfg["eos_token_id"]
|
||
logger.info(
|
||
f"Added eos_token_id from generation_config: {gen_cfg['eos_token_id']}"
|
||
)
|
||
elif eos_ids:
|
||
output_config["eos_token_id"] = (
|
||
eos_ids if len(eos_ids) > 1 else eos_ids[0]
|
||
)
|
||
except Exception as e:
|
||
logger.debug(f"Could not resolve eos_token_id: {e}")
|
||
quant_info = dict(quantization_config)
|
||
for key, val in per_layer_config.items():
|
||
quant_info[key] = val
|
||
output_config["quantization"] = quant_info
|
||
output_config["quantization_config"] = quant_info
|
||
with open(output / "config.json", "w") as f:
|
||
json.dump(output_config, f, indent=2, ensure_ascii=False)
|
||
if imatrix_report is not None:
|
||
for key in ("applied", "missing"):
|
||
imatrix_report[key] = sorted(set(imatrix_report[key]))
|
||
with open(output / "oq_imatrix_report.json", "w") as f:
|
||
json.dump(imatrix_report, f, indent=2, ensure_ascii=False)
|
||
|
||
for pattern in (
|
||
"tokenizer.json",
|
||
"tokenizer_config.json",
|
||
"special_tokens_map.json",
|
||
"tokenizer.model",
|
||
"generation_config.json",
|
||
"chat_template.json",
|
||
"chat_template.jinja",
|
||
"preprocessor_config.json",
|
||
"processor_config.json",
|
||
"added_tokens.json",
|
||
"merges.txt",
|
||
"vocab.json",
|
||
):
|
||
for src_file in source.glob(pattern):
|
||
shutil.copy2(src_file, output / src_file.name)
|
||
|
||
for py_file in source.glob("*.py"):
|
||
shutil.copy2(py_file, output / py_file.name)
|
||
|
||
cb("saving", 100.0, "Quantized model saved")
|
||
logger.info(
|
||
f"oQ{oq_level:g} streaming: completed -> {output_path} ({total_shards} shards)"
|
||
)
|
||
|
||
|
||
_SENS_NUM_SAMPLES = 128
|
||
_SENS_SEQ_LENGTH = 256
|
||
_OQE_CALIB_DATASET = "oqe_code_multilingual"
|
||
_OQE_IMATRIX_FORMAT = "omlx-oqe-imatrix-cache"
|
||
_OQE_MAX_SAMPLE_MULTIPLIER = 8
|
||
_OQE_MAX_ADAPTIVE_SAMPLES = 1024
|
||
_OQE_MIN_EXPERT_COUNT = 16
|
||
_OQE_MIN_EXPERT_COUNT_PERCENTILE = 5
|
||
_OQE_SWITCH_LINEAR_CLASSES = {"SwitchLinear", "QuantizedSwitchLinear"}
|
||
_OQ_CODE_MULTILINGUAL_KEYS = (
|
||
"code",
|
||
"en",
|
||
"ko",
|
||
"zh",
|
||
"ja",
|
||
"tool_calling",
|
||
"reasoning",
|
||
)
|
||
_OQE_CODE_MULTILINGUAL_KEYS = (
|
||
"tool_calling",
|
||
"chat",
|
||
"mixed",
|
||
"reasoning",
|
||
"code",
|
||
"en",
|
||
"ko",
|
||
"zh",
|
||
"ja",
|
||
"bartowski",
|
||
)
|
||
|
||
|
||
CALIB_DATASETS = {
|
||
"default": "Built-in (General)",
|
||
"wikitext": "WikiText-2",
|
||
"c4": "C4 (Web Crawl)",
|
||
"code": "Code (StarCoder)",
|
||
"multilingual": "Multilingual (CulturaX)",
|
||
"code_multilingual": "Code + Multilingual + Reasoning",
|
||
_OQE_CALIB_DATASET: "oQe Balanced Code + Multilingual + Tool/Reasoning",
|
||
}
|
||
|
||
|
||
def _load_calibration_data(
|
||
tokenizer,
|
||
dataset: str = "code_multilingual",
|
||
num_samples: int = _SENS_NUM_SAMPLES,
|
||
seq_length: int = _SENS_SEQ_LENGTH,
|
||
):
|
||
"""Load calibration data for sensitivity measurement.
|
||
|
||
Uses built-in calibration data by default (no download needed).
|
||
Built-in data includes English, code, Korean, Chinese, Japanese.
|
||
|
||
Args:
|
||
tokenizer: Model tokenizer.
|
||
dataset: "code_multilingual" (built-in default), "code", "multilingual",
|
||
"default" (mlx-lm generic), or HuggingFace dataset names.
|
||
num_samples: Number of calibration samples.
|
||
seq_length: Sequence length per sample.
|
||
|
||
Returns:
|
||
MLX array of shape (num_samples, seq_length) or None on failure.
|
||
"""
|
||
if dataset == _OQE_CALIB_DATASET:
|
||
# oQe (enhanced=True) requests this built-in corpus by name. If the data
|
||
# file is not present in the installed package, the generic handler below
|
||
# would swallow the error and silently calibrate the imatrix on a smaller,
|
||
# different corpus, producing a subtly worse enhanced model with no signal
|
||
# to the caller. Fail loudly instead: a missing built-in data file is a
|
||
# broken installation the user cannot fix by retrying. See package-data in
|
||
# pyproject.toml, which must ship oqe_calibration_data.json.
|
||
oqe_path = Path(__file__).parent / "oqe_calibration_data.json"
|
||
if not oqe_path.exists():
|
||
raise FileNotFoundError(
|
||
f"oQe calibration corpus not found at {oqe_path}. This file ships "
|
||
"with omlx but is missing from the current installation, so "
|
||
"enhanced quantization cannot calibrate correctly. Reinstall omlx "
|
||
"from a distribution that includes oqe_calibration_data.json."
|
||
)
|
||
if dataset in ("code_multilingual", "code", "multilingual", _OQE_CALIB_DATASET):
|
||
try:
|
||
return _load_builtin_calibration(
|
||
tokenizer, dataset, num_samples, seq_length
|
||
)
|
||
except Exception as e:
|
||
logger.warning(
|
||
f"Built-in calibration failed: {e}, falling back to mlx-lm default"
|
||
)
|
||
|
||
if dataset == "default":
|
||
try:
|
||
from mlx_lm.quant.utils import load_data
|
||
|
||
return load_data(
|
||
tokenizer, num_samples=num_samples, sequence_length=seq_length
|
||
)
|
||
except ImportError:
|
||
logger.warning("mlx_lm.quant.utils.load_data not available")
|
||
return None
|
||
|
||
try:
|
||
return _load_hf_calibration(tokenizer, dataset, num_samples, seq_length)
|
||
except Exception as e:
|
||
logger.warning(f"Failed to load {dataset}: {e}, falling back to built-in")
|
||
|
||
try:
|
||
return _load_builtin_calibration(
|
||
tokenizer, "code_multilingual", num_samples, seq_length
|
||
)
|
||
except Exception:
|
||
return None
|
||
|
||
|
||
def _load_builtin_calibration(
|
||
tokenizer, dataset: str, num_samples: int, seq_length: int
|
||
):
|
||
"""Load from built-in calibration JSON files shipped with the package."""
|
||
import mlx.core as mx
|
||
|
||
data_file = (
|
||
"oqe_calibration_data.json"
|
||
if dataset == _OQE_CALIB_DATASET
|
||
else "oq_calibration_data.json"
|
||
)
|
||
data_path = Path(__file__).parent / data_file
|
||
if not data_path.exists():
|
||
raise FileNotFoundError(f"Built-in calibration data not found: {data_path}")
|
||
|
||
with open(data_path, encoding="utf-8") as f:
|
||
all_data = json.load(f)
|
||
|
||
if dataset == _OQE_CALIB_DATASET:
|
||
texts = []
|
||
for key in _OQE_CODE_MULTILINGUAL_KEYS:
|
||
texts.extend(all_data.get(key, []))
|
||
elif dataset == "code_multilingual":
|
||
texts = []
|
||
for key in _OQ_CODE_MULTILINGUAL_KEYS:
|
||
texts.extend(all_data.get(key, []))
|
||
elif dataset == "code":
|
||
texts = all_data.get("code", []) + all_data.get("en", [])
|
||
elif dataset == "multilingual":
|
||
texts = []
|
||
for key in ("en", "ko", "zh", "ja"):
|
||
texts.extend(all_data.get(key, []))
|
||
else:
|
||
texts = []
|
||
for v in all_data.values():
|
||
texts.extend(v)
|
||
|
||
if not texts:
|
||
raise ValueError("No calibration text available")
|
||
|
||
total_kb = sum(len(t) for t in texts) // 1024
|
||
logger.info(f"Built-in calibration: {len(texts)} texts, {total_kb} KB ({dataset})")
|
||
|
||
all_ids = []
|
||
for text in texts:
|
||
ids = tokenizer.encode(text)
|
||
if hasattr(ids, "input_ids"):
|
||
ids = ids.input_ids
|
||
if isinstance(ids, list):
|
||
all_ids.extend(ids)
|
||
else:
|
||
all_ids.extend(ids.tolist() if hasattr(ids, "tolist") else list(ids))
|
||
tokens = mx.array(all_ids)
|
||
|
||
usable = (tokens.size // seq_length) * seq_length
|
||
if usable == 0:
|
||
raise ValueError(f"Not enough tokens ({tokens.size} < {seq_length})")
|
||
tokens = tokens[:usable].reshape(-1, seq_length)
|
||
|
||
if num_samples > 0 and tokens.shape[0] > num_samples:
|
||
indices = mx.random.permutation(tokens.shape[0])[:num_samples]
|
||
tokens = tokens[indices]
|
||
|
||
logger.info(f"Calibration: {tokens.shape[0]} samples x {seq_length} tokens")
|
||
return tokens
|
||
|
||
|
||
def _load_hf_calibration(tokenizer, dataset: str, num_samples: int, seq_length: int):
|
||
"""Load calibration data from HuggingFace datasets."""
|
||
try:
|
||
from datasets import load_dataset
|
||
except ImportError:
|
||
raise ImportError(
|
||
"datasets library required for non-default calibration. "
|
||
"Install with: pip install datasets"
|
||
)
|
||
|
||
logger.info(f"Loading calibration dataset: {dataset}")
|
||
|
||
if dataset == "wikitext":
|
||
ds = load_dataset("wikitext", "wikitext-2-raw-v1", split="test")
|
||
texts = "\n".join(t for t in ds["text"] if t.strip())
|
||
elif dataset == "c4":
|
||
ds = load_dataset("allenai/c4", "en", split="validation", streaming=True)
|
||
texts = "\n".join(
|
||
item["text"] for i, item in enumerate(ds) if i < num_samples * 2
|
||
)
|
||
elif dataset == "code":
|
||
ds = load_dataset(
|
||
"bigcode/starcoderdata", "python", split="train", streaming=True
|
||
)
|
||
texts = "\n".join(
|
||
item["content"] for i, item in enumerate(ds) if i < num_samples * 2
|
||
)
|
||
elif dataset == "multilingual":
|
||
langs = ["en", "ko", "zh", "ja", "de", "fr", "es"]
|
||
per_lang = max(1, num_samples // len(langs))
|
||
all_texts = []
|
||
for lang in langs:
|
||
try:
|
||
ds = load_dataset("uonlp/CulturaX", lang, split="train", streaming=True)
|
||
lang_texts = [
|
||
item["text"] for i, item in enumerate(ds) if i < per_lang * 2
|
||
]
|
||
all_texts.extend(lang_texts)
|
||
except Exception:
|
||
logger.warning(f"Failed to load CulturaX/{lang}, skipping")
|
||
texts = "\n".join(all_texts)
|
||
elif dataset == "code_multilingual":
|
||
half = max(1, num_samples // 2)
|
||
code_texts = []
|
||
try:
|
||
ds = load_dataset(
|
||
"bigcode/starcoderdata", "python", split="train", streaming=True
|
||
)
|
||
code_texts = [item["content"] for i, item in enumerate(ds) if i < half * 2]
|
||
except Exception:
|
||
logger.warning("Failed to load code dataset")
|
||
|
||
ml_texts = []
|
||
for lang in ["en", "ko", "zh", "ja"]:
|
||
try:
|
||
ds = load_dataset("uonlp/CulturaX", lang, split="train", streaming=True)
|
||
ml_texts.extend(
|
||
item["text"] for i, item in enumerate(ds) if i < half // 2
|
||
)
|
||
except Exception:
|
||
pass
|
||
texts = "\n".join(code_texts + ml_texts)
|
||
else:
|
||
raise ValueError(f"Unknown calibration dataset: {dataset}")
|
||
|
||
if not texts:
|
||
raise ValueError(f"No text loaded from {dataset}")
|
||
|
||
tokens = tokenizer.encode(texts)
|
||
if hasattr(tokens, "input_ids"):
|
||
tokens = tokens.input_ids
|
||
if isinstance(tokens, list):
|
||
tokens = mx.array(tokens)
|
||
elif not isinstance(tokens, mx.array):
|
||
import numpy as np
|
||
|
||
tokens = mx.array(np.array(tokens))
|
||
|
||
if tokens.ndim > 1:
|
||
tokens = tokens.reshape(-1)
|
||
|
||
n_tokens = tokens.size
|
||
usable = (n_tokens // seq_length) * seq_length
|
||
if usable == 0:
|
||
raise ValueError(f"Not enough tokens from {dataset} (got {n_tokens})")
|
||
tokens = tokens[:usable].reshape(-1, seq_length)
|
||
|
||
n_available = tokens.shape[0]
|
||
if num_samples > 0 and n_available > num_samples:
|
||
indices = mx.random.permutation(n_available)[:num_samples]
|
||
tokens = tokens[indices]
|
||
|
||
logger.info(
|
||
f"Calibration: {tokens.shape[0]} samples × {seq_length} tokens from {dataset}"
|
||
)
|
||
return tokens
|
||
|
||
|
||
def _find_model_layers(model):
|
||
"""Find embedding function and transformer layers in the model.
|
||
|
||
Searches common model structures: standard, VLM, and direct.
|
||
Returns (embed_fn, layers) or (None, None).
|
||
"""
|
||
embed_fn = None
|
||
layers = None
|
||
|
||
if hasattr(model, "model") and hasattr(model.model, "embed_tokens"):
|
||
embed_fn = model.model.embed_tokens
|
||
layers = model.model.layers
|
||
elif hasattr(model, "language_model") and hasattr(model.language_model, "model"):
|
||
lm = model.language_model.model
|
||
if hasattr(lm, "embed_tokens"):
|
||
embed_fn = lm.embed_tokens
|
||
layers = lm.layers
|
||
elif hasattr(model, "embed_tokens"):
|
||
embed_fn = model.embed_tokens
|
||
layers = model.layers
|
||
elif hasattr(model, "backbone") and hasattr(model.backbone, "embeddings"):
|
||
embed_fn = model.backbone.embeddings
|
||
layers = model.layers
|
||
|
||
return embed_fn, layers
|
||
|
||
|
||
def _forward_layer_result(block, inputs, mask, position_ids):
|
||
"""Forward pass through a transformer layer, returning output and aux."""
|
||
if isinstance(position_ids, dict) and position_ids.get("kind") == "glm_moe_dsa":
|
||
try:
|
||
result = block(
|
||
inputs,
|
||
mask,
|
||
None,
|
||
position_ids.get("prev_topk_indices"),
|
||
)
|
||
if isinstance(result, tuple):
|
||
return result[0], result[1] if len(result) > 1 else None
|
||
return result, None
|
||
except (TypeError, ValueError, RuntimeError, AttributeError) as e:
|
||
logger.debug(
|
||
f"_forward_layer: GLM MoE DSA signature failed for "
|
||
f"{type(block).__name__}: {e}"
|
||
)
|
||
return None, None
|
||
|
||
last_exc = None
|
||
for call_args in [
|
||
(inputs, mask, None, position_ids),
|
||
(inputs, mask, None),
|
||
(inputs, mask),
|
||
(inputs, None, mask, None),
|
||
(inputs,),
|
||
]:
|
||
try:
|
||
result = block(*call_args)
|
||
if isinstance(result, tuple):
|
||
return result[0], result[1] if len(result) > 1 else None
|
||
return result, None
|
||
except (TypeError, ValueError, RuntimeError, AttributeError) as e:
|
||
last_exc = e
|
||
continue
|
||
if last_exc is not None:
|
||
logger.debug(
|
||
f"_forward_layer: all signatures failed for "
|
||
f"{type(block).__name__}: {last_exc}"
|
||
)
|
||
return None, None
|
||
|
||
|
||
def _forward_layer(block, inputs, mask, position_ids):
|
||
"""Forward pass through a transformer layer with flexible signature."""
|
||
return _forward_layer_result(block, inputs, mask, position_ids)[0]
|
||
|
||
|
||
def _layer_masks_for_model(model, layers, inputs):
|
||
"""Build the per-layer mask schedule used by the original model."""
|
||
if hasattr(model, "make_cache") and any(
|
||
hasattr(layer, "is_linear") for layer in layers
|
||
):
|
||
try:
|
||
from mlx_lm.models.base import create_attention_mask, create_ssm_mask
|
||
|
||
cache = model.make_cache()
|
||
fa_idx = getattr(getattr(model, "model", model), "fa_idx", 0)
|
||
ssm_idx = getattr(getattr(model, "model", model), "ssm_idx", 0)
|
||
fa_cache = cache[fa_idx] if fa_idx < len(cache) else None
|
||
ssm_cache = cache[ssm_idx] if ssm_idx < len(cache) else None
|
||
try:
|
||
fa_mask = create_attention_mask(inputs, fa_cache)
|
||
except TypeError:
|
||
# mlx-lm API changed — cache.make_mask signature differs
|
||
fa_mask = None
|
||
try:
|
||
ssm_mask = create_ssm_mask(inputs, ssm_cache)
|
||
except TypeError:
|
||
ssm_mask = None
|
||
if fa_mask is not None or ssm_mask is not None:
|
||
if fa_mask is None:
|
||
fa_mask = nn.MultiHeadAttention.create_additive_causal_mask(
|
||
inputs.shape[1]
|
||
).astype(inputs.dtype if hasattr(inputs, "dtype") else mx.float16)
|
||
# SSM layers (GatedDeltaNet) expect (B, S) boolean mask, not
|
||
# (S, S) causal mask. During calibration there is no padding,
|
||
# so None is the correct mask for SSM layers.
|
||
return [
|
||
ssm_mask if getattr(layer, "is_linear", False) else fa_mask
|
||
for layer in layers
|
||
]
|
||
except (ImportError, AttributeError):
|
||
pass
|
||
|
||
seq_len = inputs.shape[1]
|
||
mask = nn.MultiHeadAttention.create_additive_causal_mask(seq_len)
|
||
dtype = inputs.dtype if hasattr(inputs, "dtype") else mx.float16
|
||
return [mask.astype(dtype)] * len(layers)
|
||
|
||
|
||
def _qdq_weight_only(weight, bits: int, group_size: int, mode: str):
|
||
qw, scales, *rest = mx.quantize(weight, group_size=group_size, bits=bits, mode=mode)
|
||
return mx.dequantize(
|
||
qw,
|
||
scales,
|
||
rest[0] if rest else None,
|
||
group_size=group_size,
|
||
bits=bits,
|
||
mode=mode,
|
||
)
|
||
|
||
|
||
def _temporary_quantize_block(block, config, oq_level, group_size: int):
|
||
"""Quantize-dequantize a block using the active predicate configuration."""
|
||
saved = {}
|
||
for path, module in tree_flatten(block.leaf_modules(), is_leaf=nn.Module.is_module):
|
||
if not hasattr(module, "weight") or not hasattr(module, "to_quantized"):
|
||
continue
|
||
if getattr(module.weight, "ndim", 0) < 2:
|
||
continue
|
||
norm_path = _normalize_quant_path(path)
|
||
bits, gs, mode = _get_predicate_bits(norm_path, config, oq_level, group_size)
|
||
if bits is None or module.weight.shape[-1] % gs != 0:
|
||
continue
|
||
saved[path] = module.weight
|
||
module.weight = _qdq_weight_only(module.weight, bits, gs, mode)
|
||
return saved
|
||
|
||
|
||
def _restore_saved_weights(block, saved):
|
||
"""Restore temporarily quantized block weights."""
|
||
modules_by_path = dict(
|
||
tree_flatten(block.leaf_modules(), is_leaf=nn.Module.is_module)
|
||
)
|
||
for path, weight in saved.items():
|
||
if path in modules_by_path:
|
||
modules_by_path[path].weight = weight
|
||
|
||
|
||
def _prepare_layer_inputs(model, layers, calib_data, inputs):
|
||
"""Model-specific (inputs, per-layer masks, 4th forward arg) for
|
||
block-level sensitivity forwards.
|
||
|
||
DeepSeek V4 blocks run on a 4D hidden (B, S, hc_mult, hidden), take a
|
||
window-limited array mask, and need the real token ids as their 4th
|
||
argument (hash expert routing indexes tid2eid with them). Everything
|
||
else keeps the generic 3D inputs + causal masks + position ids.
|
||
"""
|
||
model_type = str(
|
||
getattr(model, "model_type", "")
|
||
or getattr(getattr(model, "args", None), "model_type", "")
|
||
or getattr(
|
||
getattr(getattr(model, "model", None), "args", None),
|
||
"model_type",
|
||
"",
|
||
)
|
||
)
|
||
if model_type.startswith("deepseek_v4"):
|
||
args = model.args
|
||
h = mx.broadcast_to(
|
||
inputs[:, :, None, :],
|
||
(inputs.shape[0], inputs.shape[1], args.hc_mult, inputs.shape[2]),
|
||
)
|
||
h = mx.contiguous(h)
|
||
mask = create_attention_mask(
|
||
h[:, :, 0, :],
|
||
None,
|
||
window_size=args.sliding_window,
|
||
return_array=True,
|
||
)
|
||
return h, [mask] * len(layers), calib_data
|
||
if model_type == "glm_moe_dsa":
|
||
mask = create_attention_mask(inputs, None, return_array=True)
|
||
state = {"kind": "glm_moe_dsa", "prev_topk_indices": None}
|
||
return inputs, [mask] * len(layers), state
|
||
masks = _layer_masks_for_model(model, layers, inputs)
|
||
position_ids = mx.arange(calib_data.shape[1])[None, :]
|
||
return inputs, masks, position_ids
|
||
|
||
|
||
class _ImatrixCaptureWrapper(nn.Module):
|
||
"""Temporary module wrapper used only during oQe calibration."""
|
||
|
||
def __init__(self, module, name: str, collector: "OQImatrixCollector"):
|
||
super().__init__()
|
||
object.__setattr__(self, "_module", module)
|
||
object.__setattr__(self, "_name", name)
|
||
object.__setattr__(self, "_collector", collector)
|
||
|
||
def __getattr__(self, key: str):
|
||
try:
|
||
return super().__getattr__(key)
|
||
except AttributeError as exc:
|
||
try:
|
||
module = object.__getattribute__(self, "_module")
|
||
except AttributeError:
|
||
raise exc
|
||
return getattr(module, key)
|
||
|
||
def __call__(self, *args, **kwargs):
|
||
if args:
|
||
if (
|
||
type(self._module).__name__ in _OQE_SWITCH_LINEAR_CLASSES
|
||
and len(args) >= 2
|
||
):
|
||
self._collector.collect_switch(
|
||
self._name, self._module, args[0], args[1]
|
||
)
|
||
else:
|
||
self._collector.collect_dense(self._name, self._module, args[0])
|
||
return self._module(*args, **kwargs)
|
||
|
||
|
||
class OQImatrixCollector:
|
||
"""Collect input activation energy for oQe weighted quantization."""
|
||
|
||
def __init__(self):
|
||
self.entries: dict[str, OQImatrixEntry] = {}
|
||
self._original_modules: dict[str, Any] = {}
|
||
self.capture_module_classes: dict[str, int] = {}
|
||
self.switch_capture_modules = 0
|
||
|
||
@staticmethod
|
||
def _is_capture_module(module) -> bool:
|
||
cls = type(module).__name__
|
||
if cls in _OQE_SWITCH_LINEAR_CLASSES:
|
||
return hasattr(module, "weight") and getattr(module.weight, "ndim", 0) == 3
|
||
# QuantizedLinear capture lets imatrix collection run against
|
||
# already-quantized checkpoints (e.g. deriving a recalibrated MTP
|
||
# head from an oQ8 model when the bf16 source is gone).
|
||
return (
|
||
cls in ("Linear", "QuantizedLinear")
|
||
and hasattr(module, "weight")
|
||
and getattr(module.weight, "ndim", 0) == 2
|
||
)
|
||
|
||
@staticmethod
|
||
def _module_in_dim(module) -> int:
|
||
w = module.weight
|
||
bits = getattr(module, "bits", None)
|
||
if bits and w.dtype == mx.uint32:
|
||
return int(w.shape[-1] * 32 // int(bits))
|
||
return int(w.shape[-1])
|
||
|
||
def install(self, model) -> int:
|
||
replacements = []
|
||
for name, module in model.named_modules():
|
||
if not name or not self._is_capture_module(module):
|
||
continue
|
||
cls = type(module).__name__
|
||
self.capture_module_classes[cls] = (
|
||
self.capture_module_classes.get(cls, 0) + 1
|
||
)
|
||
if cls in _OQE_SWITCH_LINEAR_CLASSES:
|
||
self.switch_capture_modules += 1
|
||
self._original_modules[name] = module
|
||
replacements.append((name, _ImatrixCaptureWrapper(module, name, self)))
|
||
if replacements:
|
||
model.update_modules(tree_unflatten(replacements), strict=False)
|
||
return len(replacements)
|
||
|
||
def restore(self, model) -> None:
|
||
if self._original_modules:
|
||
model.update_modules(
|
||
tree_unflatten(list(self._original_modules.items())),
|
||
strict=False,
|
||
)
|
||
self._original_modules.clear()
|
||
|
||
def _ensure_entry(self, name: str, sums_shape, counts_shape) -> OQImatrixEntry:
|
||
entry = self.entries.get(name)
|
||
if entry is None:
|
||
entry = OQImatrixEntry(
|
||
in_sum2=np.zeros(sums_shape, dtype=np.float32),
|
||
counts=np.zeros(counts_shape, dtype=np.int64),
|
||
)
|
||
self.entries[name] = entry
|
||
return entry
|
||
|
||
def collect_dense(self, name: str, module, x) -> None:
|
||
try:
|
||
in_dim = self._module_in_dim(module)
|
||
if getattr(x, "shape", ()) and int(x.shape[-1]) != in_dim:
|
||
return
|
||
mx.eval(x)
|
||
x_np = np.asarray(x.astype(mx.float32)).reshape(-1, in_dim)
|
||
if x_np.shape[0] == 0:
|
||
return
|
||
entry = self._ensure_entry(name, (in_dim,), (1,))
|
||
entry.in_sum2 += np.square(x_np, dtype=np.float32).sum(axis=0)
|
||
entry.counts[0] += x_np.shape[0]
|
||
except Exception as e:
|
||
logger.debug("oQe imatrix dense capture skipped for %s: %s", name, e)
|
||
|
||
@staticmethod
|
||
def _accumulate_switch(
|
||
entry: OQImatrixEntry,
|
||
idx_flat: np.ndarray,
|
||
x_np: np.ndarray,
|
||
n_experts: int,
|
||
token_ids: np.ndarray | None = None,
|
||
) -> None:
|
||
entry.counts += np.bincount(idx_flat, minlength=n_experts)[:n_experts]
|
||
x_sq = np.square(x_np, dtype=np.float32)
|
||
if token_ids is None:
|
||
source_rows = np.arange(idx_flat.shape[0], dtype=np.int64)
|
||
else:
|
||
source_rows = np.asarray(token_ids, dtype=np.int64).reshape(-1)
|
||
if source_rows.shape[0] != idx_flat.shape[0]:
|
||
return
|
||
|
||
order = np.argsort(idx_flat, kind="stable")
|
||
idx_sorted = idx_flat[order]
|
||
boundaries = np.flatnonzero(np.diff(idx_sorted)) + 1
|
||
starts = np.concatenate(([0], boundaries))
|
||
ends = np.concatenate((boundaries, [idx_sorted.shape[0]]))
|
||
|
||
for start, end in zip(starts, ends):
|
||
expert = int(idx_sorted[start])
|
||
rows = source_rows[order[start:end]]
|
||
entry.in_sum2[expert] += x_sq[rows].sum(axis=0)
|
||
|
||
def collect_switch(self, name: str, module, x, indices) -> None:
|
||
try:
|
||
weight_shape = getattr(module.weight, "shape", ())
|
||
n_experts = int(getattr(module, "num_experts", weight_shape[0]))
|
||
in_dim = int(getattr(module, "input_dims", weight_shape[-1]))
|
||
if getattr(x, "shape", ()) and int(x.shape[-1]) != in_dim:
|
||
return
|
||
mx.eval(x, indices)
|
||
x_np = np.asarray(x.astype(mx.float32)).reshape(-1, in_dim)
|
||
idx_np = np.asarray(indices).astype(np.int64)
|
||
if idx_np.size == 0 or x_np.shape[0] == 0:
|
||
return
|
||
if idx_np.ndim == 1:
|
||
idx_flat = idx_np.reshape(-1)
|
||
if x_np.shape[0] != idx_flat.shape[0]:
|
||
return
|
||
x_source = x_np
|
||
token_ids = None
|
||
else:
|
||
idx_2d = idx_np.reshape(-1, idx_np.shape[-1])
|
||
if x_np.shape[0] == idx_2d.shape[0]:
|
||
idx_flat = idx_2d.reshape(-1)
|
||
token_ids = np.repeat(
|
||
np.arange(idx_2d.shape[0], dtype=np.int64),
|
||
idx_2d.shape[1],
|
||
)
|
||
x_source = x_np
|
||
elif x_np.shape[0] == idx_2d.size:
|
||
idx_flat = idx_2d.reshape(-1)
|
||
x_source = x_np
|
||
token_ids = None
|
||
else:
|
||
return
|
||
valid = (idx_flat >= 0) & (idx_flat < n_experts)
|
||
if not np.any(valid):
|
||
return
|
||
idx_flat = idx_flat[valid]
|
||
if token_ids is None:
|
||
x_source = x_source[valid]
|
||
else:
|
||
token_ids = token_ids[valid]
|
||
entry = self._ensure_entry(name, (n_experts, in_dim), (n_experts,))
|
||
self._accumulate_switch(entry, idx_flat, x_source, n_experts, token_ids)
|
||
except Exception as e:
|
||
logger.debug("oQe imatrix switch capture skipped for %s: %s", name, e)
|
||
|
||
|
||
def _collect_mtp_head_imatrix(model, batch, hidden) -> bool:
|
||
"""Run the MTP head over a calibration micro-batch.
|
||
|
||
The trunk-layer walk never invokes the head, so without this pass every
|
||
``mtp.*`` linear lands in the imatrix "missing" list and gets quantized
|
||
without calibration — measurably hurting draft acceptance. Mirrors the
|
||
decode-time contract: fuse the trunk's post-norm hidden at position t
|
||
with the embedding of token t+1 (the head's own input RMSNorms make the
|
||
residual pre/post-norm difference negligible for activation statistics).
|
||
"""
|
||
inner = getattr(model, "language_model", None) or model
|
||
mtp = getattr(inner, "mtp", None)
|
||
if mtp is None:
|
||
return False
|
||
try:
|
||
if isinstance(mtp, (list, tuple)):
|
||
# DeepSeek-V4 style: MTPBlock stack consuming the raw (4D)
|
||
# trunk hidden; Model.mtp_forward wires mask/cache/embedding.
|
||
out = inner.mtp_forward(
|
||
hidden[:, :-1],
|
||
batch[:, 1:],
|
||
inner.make_mtp_cache(),
|
||
)
|
||
mx.eval(out)
|
||
return True
|
||
core = getattr(inner, "model", None) or inner
|
||
norm = getattr(core, "norm", None)
|
||
embed = getattr(core, "embed_tokens", None)
|
||
if norm is None or embed is None:
|
||
return False
|
||
# The head's attention reads cache.offset unconditionally on the
|
||
# mlx-vlm classes — give it a fresh cache per micro-batch.
|
||
make_cache = getattr(inner, "make_mtp_cache", None)
|
||
if callable(make_cache):
|
||
mtp_cache = make_cache()
|
||
else:
|
||
from mlx_lm.models.cache import KVCache
|
||
|
||
mtp_cache = [KVCache() for _ in mtp.layers]
|
||
h = norm(hidden[:, :-1, :])
|
||
out = mtp(h, batch[:, 1:], embed, mtp_cache)
|
||
mx.eval(out)
|
||
return True
|
||
except Exception as e:
|
||
logger.warning("oQe imatrix MTP head pass skipped: %s", e)
|
||
return False
|
||
|
||
|
||
def _collect_imatrix_from_model(
|
||
model,
|
||
tokenizer,
|
||
config,
|
||
*,
|
||
calib_dataset: str,
|
||
num_samples: int,
|
||
seq_length: int,
|
||
progress_callback=None,
|
||
progress_start: float = 13.0,
|
||
progress_end: float = 18.0,
|
||
) -> tuple[dict[str, OQImatrixEntry], dict[str, Any]]:
|
||
adaptive_max_samples = max(
|
||
int(num_samples),
|
||
min(
|
||
int(num_samples) * _OQE_MAX_SAMPLE_MULTIPLIER,
|
||
_OQE_MAX_ADAPTIVE_SAMPLES,
|
||
),
|
||
)
|
||
calib_data = _load_calibration_data(
|
||
tokenizer,
|
||
dataset=calib_dataset,
|
||
num_samples=adaptive_max_samples,
|
||
seq_length=seq_length,
|
||
)
|
||
if calib_data is None:
|
||
return {}, {"dataset": calib_dataset, "processed_samples": 0}
|
||
|
||
embed_fn, layers = _find_model_layers(model)
|
||
if embed_fn is None or layers is None:
|
||
return {}, {"dataset": calib_dataset, "processed_samples": 0}
|
||
|
||
collector = OQImatrixCollector()
|
||
installed = collector.install(model)
|
||
if installed == 0:
|
||
return {}, {
|
||
"dataset": calib_dataset,
|
||
"installed_modules": 0,
|
||
"processed_samples": 0,
|
||
}
|
||
|
||
available_samples = int(calib_data.shape[0])
|
||
max_samples = min(available_samples, adaptive_max_samples)
|
||
step_samples = max(1, int(num_samples))
|
||
batch_plan = _oqe_calibration_batch_plan(
|
||
config,
|
||
requested_samples=step_samples,
|
||
seq_length=seq_length,
|
||
)
|
||
micro_batch_size = int(batch_plan["micro_batch_size"])
|
||
processed_samples = 0
|
||
micro_batches = 0
|
||
rounds: list[dict[str, Any]] = []
|
||
require_expert_counts = _imatrix_requires_expert_counts(
|
||
config, collector.switch_capture_modules
|
||
)
|
||
coverage = _imatrix_expert_coverage_stats(collector.entries)
|
||
logger.info(
|
||
"oQe imatrix: adaptive max=%d, step=%d, micro-batch=%d "
|
||
"(available=%s, capture budget=%s)",
|
||
max_samples,
|
||
step_samples,
|
||
micro_batch_size,
|
||
_format_size(int(batch_plan["live_available_bytes"])),
|
||
_format_size(int(batch_plan["capture_budget_bytes"])),
|
||
)
|
||
try:
|
||
while processed_samples < max_samples:
|
||
next_samples = min(processed_samples + step_samples, max_samples)
|
||
while processed_samples < next_samples:
|
||
micro_next = min(processed_samples + micro_batch_size, next_samples)
|
||
batch = calib_data[processed_samples:micro_next]
|
||
if int(batch.shape[0]) == 0:
|
||
break
|
||
|
||
inputs = embed_fn(batch)
|
||
inputs, layer_masks, position_ids = _prepare_layer_inputs(
|
||
model, layers, batch, inputs
|
||
)
|
||
|
||
for layer_idx, block in enumerate(layers):
|
||
layer_mask = (
|
||
layer_masks[layer_idx] if layer_idx < len(layer_masks) else None
|
||
)
|
||
prev_aux = (
|
||
position_ids.get("prev_topk_indices")
|
||
if isinstance(position_ids, dict)
|
||
and position_ids.get("kind") == "glm_moe_dsa"
|
||
else None
|
||
)
|
||
out, aux = _forward_layer_result(
|
||
block, inputs, layer_mask, position_ids
|
||
)
|
||
if out is None:
|
||
continue
|
||
mx.eval(out)
|
||
inputs = out
|
||
if (
|
||
isinstance(position_ids, dict)
|
||
and position_ids.get("kind") == "glm_moe_dsa"
|
||
):
|
||
position_ids["prev_topk_indices"] = aux or prev_aux
|
||
mx.synchronize()
|
||
mx.clear_cache()
|
||
|
||
# MTP-head pass: the layer walk above leaves ``inputs`` as
|
||
# the final-layer hidden states; feed them (post-norm) plus
|
||
# the shifted token ids through the head so its linears
|
||
# contribute imatrix entries too.
|
||
if _collect_mtp_head_imatrix(model, batch, inputs):
|
||
mx.synchronize()
|
||
mx.clear_cache()
|
||
|
||
processed_samples = micro_next
|
||
micro_batches += 1
|
||
coverage = _imatrix_expert_coverage_stats(collector.entries)
|
||
coverage_sufficient = _imatrix_expert_coverage_sufficient(
|
||
coverage, require_expert_counts=require_expert_counts
|
||
)
|
||
collection_sufficient = (
|
||
processed_samples >= int(num_samples) and coverage_sufficient
|
||
)
|
||
frac = min(max(processed_samples / max(max_samples, 1), 0.0), 1.0)
|
||
pct = progress_start + frac * (progress_end - progress_start)
|
||
detail = (
|
||
f"oQe imatrix {processed_samples}/{max_samples} samples "
|
||
f"(micro-batch {int(batch.shape[0])}, "
|
||
f"zero experts {coverage.get('zero_count_experts', 0)})"
|
||
)
|
||
_emit_progress(
|
||
progress_callback,
|
||
"imatrix",
|
||
pct,
|
||
detail,
|
||
{
|
||
"processed_samples": processed_samples,
|
||
"max_samples": max_samples,
|
||
"requested_samples": int(num_samples),
|
||
"micro_batch_size": micro_batch_size,
|
||
"micro_batches": micro_batches,
|
||
"coverage_sufficient": coverage_sufficient,
|
||
"collection_sufficient": collection_sufficient,
|
||
"requires_expert_counts": require_expert_counts,
|
||
"coverage": coverage,
|
||
},
|
||
)
|
||
logger.info(
|
||
"oQe imatrix: %d/%d samples, zero experts=%d, "
|
||
"p05=%.1f, expert_coverage=%s, sufficient=%s",
|
||
processed_samples,
|
||
max_samples,
|
||
int(coverage.get("zero_count_experts", 0)),
|
||
float(coverage.get("p05_count", 0.0)),
|
||
coverage_sufficient,
|
||
collection_sufficient,
|
||
)
|
||
if collection_sufficient:
|
||
break
|
||
mx.synchronize()
|
||
mx.clear_cache()
|
||
|
||
if int(processed_samples) == 0:
|
||
break
|
||
coverage = _imatrix_expert_coverage_stats(collector.entries)
|
||
coverage_sufficient = _imatrix_expert_coverage_sufficient(
|
||
coverage, require_expert_counts=require_expert_counts
|
||
)
|
||
collection_sufficient = (
|
||
processed_samples >= int(num_samples) and coverage_sufficient
|
||
)
|
||
rounds.append(
|
||
{
|
||
"processed_samples": processed_samples,
|
||
"coverage_sufficient": coverage_sufficient,
|
||
"collection_sufficient": collection_sufficient,
|
||
"coverage": coverage,
|
||
}
|
||
)
|
||
if collection_sufficient:
|
||
break
|
||
mx.synchronize()
|
||
mx.clear_cache()
|
||
finally:
|
||
collector.restore(model)
|
||
|
||
coverage_sufficient = _imatrix_expert_coverage_sufficient(
|
||
coverage, require_expert_counts=require_expert_counts
|
||
)
|
||
collection_sufficient = (
|
||
processed_samples >= int(num_samples) and coverage_sufficient
|
||
)
|
||
if require_expert_counts and not coverage.get("has_expert_counts", False):
|
||
logger.warning(
|
||
"oQe imatrix: model config expects routed experts, but no expert "
|
||
"activation counts were captured"
|
||
)
|
||
|
||
metadata = {
|
||
"dataset": calib_dataset,
|
||
"requested_samples": int(num_samples),
|
||
"seq_length": int(seq_length),
|
||
"adaptive": True,
|
||
"adaptive_step_samples": step_samples,
|
||
"adaptive_max_samples": max_samples,
|
||
"available_samples": available_samples,
|
||
"micro_batch_size": micro_batch_size,
|
||
"micro_batches": micro_batches,
|
||
"batch_plan": batch_plan,
|
||
"processed_samples": processed_samples,
|
||
"installed_modules": installed,
|
||
"capture_module_classes": dict(
|
||
sorted(collector.capture_module_classes.items())
|
||
),
|
||
"switch_capture_modules": int(collector.switch_capture_modules),
|
||
"requires_expert_counts": require_expert_counts,
|
||
"coverage_sufficient": coverage_sufficient,
|
||
"collection_sufficient": collection_sufficient,
|
||
"coverage": coverage,
|
||
"rounds": rounds,
|
||
}
|
||
return collector.entries, metadata
|
||
|
||
|
||
def _collect_imatrix(
|
||
model_path: str,
|
||
config: dict,
|
||
*,
|
||
calib_dataset: str = _OQE_CALIB_DATASET,
|
||
num_samples: int = 128,
|
||
seq_length: int = 512,
|
||
trust_remote_code: bool = False,
|
||
progress_callback=None,
|
||
progress_start: float = 13.0,
|
||
progress_end: float = 18.0,
|
||
) -> tuple[dict[str, OQImatrixEntry], dict[str, Any]]:
|
||
from omlx.utils.model_loading import (
|
||
_checkpoint_has_mtp_weights,
|
||
_has_mtp_heads,
|
||
maybe_apply_pre_load_patches,
|
||
)
|
||
|
||
is_vlm = _has_vision_subconfig(config)
|
||
has_mtp_weights = _checkpoint_has_mtp_weights(model_path)
|
||
maybe_apply_pre_load_patches(model_path, for_vlm=is_vlm)
|
||
|
||
restore_mtp_active = None
|
||
if _has_mtp_heads(config) and has_mtp_weights:
|
||
# Attach the MTP head on the temporary calibration model so the
|
||
# head-forward pass in _collect_imatrix_from_model can exercise its
|
||
# linears (they'd otherwise land in the imatrix "missing" list).
|
||
try:
|
||
from omlx.patches.mlx_lm_mtp import is_mtp_active, set_mtp_active
|
||
|
||
if is_vlm:
|
||
from omlx.patches.mlx_vlm_mtp import (
|
||
apply_mlx_vlm_mtp_patch,
|
||
apply_mlx_vlm_mtp_runtime_patch,
|
||
)
|
||
|
||
apply_mlx_vlm_mtp_patch()
|
||
apply_mlx_vlm_mtp_runtime_patch()
|
||
prev_active = is_mtp_active()
|
||
set_mtp_active(True)
|
||
|
||
def _restore_mtp_active():
|
||
set_mtp_active(prev_active)
|
||
|
||
restore_mtp_active = _restore_mtp_active
|
||
except Exception as e:
|
||
logger.debug("MTP runtime patch skipped for oQe imatrix: %s", e)
|
||
|
||
try:
|
||
if is_vlm:
|
||
import mlx.nn as _nn
|
||
from mlx_vlm.utils import load_model as vlm_load_model
|
||
|
||
_orig_lw = _nn.Module.load_weights
|
||
|
||
def _lenient_load_weights(self, file_or_weights, *args, **kwargs):
|
||
kwargs.pop("strict", None)
|
||
return _orig_lw(self, file_or_weights, *args, strict=False, **kwargs)
|
||
|
||
_nn.Module.load_weights = _lenient_load_weights
|
||
try:
|
||
model = vlm_load_model(
|
||
Path(model_path),
|
||
lazy=True,
|
||
trust_remote_code=trust_remote_code,
|
||
)
|
||
finally:
|
||
_nn.Module.load_weights = _orig_lw
|
||
from mlx_lm.tokenizer_utils import load as load_tokenizer
|
||
|
||
tokenizer = load_tokenizer(Path(model_path))
|
||
else:
|
||
from mlx_lm import load as lm_load
|
||
|
||
model, tokenizer = lm_load(
|
||
model_path,
|
||
lazy=True,
|
||
trust_remote_code=trust_remote_code,
|
||
model_config=_sensitivity_lm_config_override(config),
|
||
)
|
||
except Exception as e:
|
||
logger.error("oQe imatrix: model load failed (%s)", e)
|
||
return {}, {"dataset": calib_dataset, "processed_samples": 0}
|
||
finally:
|
||
if restore_mtp_active is not None:
|
||
restore_mtp_active()
|
||
|
||
try:
|
||
return _collect_imatrix_from_model(
|
||
model,
|
||
tokenizer,
|
||
config,
|
||
calib_dataset=calib_dataset,
|
||
num_samples=num_samples,
|
||
seq_length=seq_length,
|
||
progress_callback=progress_callback,
|
||
progress_start=progress_start,
|
||
progress_end=progress_end,
|
||
)
|
||
finally:
|
||
del model, tokenizer
|
||
mx.synchronize()
|
||
mx.clear_cache()
|
||
|
||
|
||
def _oqe_cache_missing_mtp_entries(cache: OQImatrixData, config: dict) -> bool:
|
||
"""True when the model declares MTP heads but the cache predates the
|
||
MTP-head collection pass (no ``mtp.*`` entries) — force a recollect so
|
||
the head gets calibrated quantization instead of landing in "missing"."""
|
||
try:
|
||
from omlx.utils.model_loading import _has_mtp_heads
|
||
|
||
if not _has_mtp_heads(config):
|
||
return False
|
||
except Exception:
|
||
return False
|
||
return not any(
|
||
".mtp." in key or key.startswith("mtp.") for key in cache.entries
|
||
)
|
||
|
||
|
||
def _load_or_collect_imatrix(
|
||
model_path: str,
|
||
config: dict,
|
||
*,
|
||
cache_path: str,
|
||
reuse_cache: bool,
|
||
num_samples: int,
|
||
seq_length: int,
|
||
strict: bool,
|
||
trust_remote_code: bool,
|
||
calib_dataset: str = _OQE_CALIB_DATASET,
|
||
progress_callback=None,
|
||
progress_start: float = 13.0,
|
||
progress_end: float = 18.0,
|
||
load_path_factory: Callable[[], str] | None = None,
|
||
) -> OQImatrixData:
|
||
source = Path(model_path)
|
||
path = Path(cache_path)
|
||
expected = _source_imatrix_signature(
|
||
source,
|
||
config,
|
||
num_samples=num_samples,
|
||
seq_length=seq_length,
|
||
calib_dataset=calib_dataset,
|
||
)
|
||
if reuse_cache and path.exists():
|
||
cache = _load_oqe_imatrix(path)
|
||
if _oqe_cache_matches(cache, expected):
|
||
if _oqe_cache_missing_mtp_entries(cache, config):
|
||
logger.info(
|
||
"oQe imatrix: cache predates MTP-head collection "
|
||
"(no mtp.* entries), recollecting %s",
|
||
path,
|
||
)
|
||
elif _oqe_cache_has_required_expert_coverage(cache):
|
||
cache.reused = True
|
||
logger.info("oQe imatrix: using cache %s", path)
|
||
_emit_progress(
|
||
progress_callback,
|
||
"imatrix",
|
||
progress_end,
|
||
f"Using oQe imatrix cache ({len(cache.entries)} entries)",
|
||
{"cache_path": str(path), "entry_count": len(cache.entries)},
|
||
)
|
||
return cache
|
||
logger.info(
|
||
"oQe imatrix: cache missing required expert coverage, "
|
||
"recollecting %s",
|
||
path,
|
||
)
|
||
else:
|
||
logger.info("oQe imatrix: cache metadata mismatch, recollecting %s", path)
|
||
|
||
logger.info(
|
||
"oQe imatrix: collecting %d samples x %d tokens from %s",
|
||
num_samples,
|
||
seq_length,
|
||
calib_dataset,
|
||
)
|
||
# ``load_path_factory`` swaps in an alternate checkpoint for the
|
||
# calibration forwards (a RAM-safe quantized proxy of the source when
|
||
# the source exceeds system RAM). Resolved only on a cache miss so a
|
||
# reusable cache never pays the proxy build. The cache signature stays
|
||
# keyed to the source checkpoint either way.
|
||
load_path = model_path
|
||
if load_path_factory is not None:
|
||
load_path = load_path_factory()
|
||
entries, collection_metadata = _collect_imatrix(
|
||
load_path,
|
||
config,
|
||
calib_dataset=calib_dataset,
|
||
num_samples=num_samples,
|
||
seq_length=seq_length,
|
||
trust_remote_code=trust_remote_code,
|
||
progress_callback=progress_callback,
|
||
progress_start=progress_start,
|
||
progress_end=progress_end,
|
||
)
|
||
if not entries:
|
||
raise RuntimeError("oQe imatrix collection produced no entries")
|
||
metadata = {
|
||
**expected,
|
||
"entry_count": len(entries),
|
||
"collection": collection_metadata,
|
||
"expert_coverage": collection_metadata.get("coverage", {}),
|
||
"requires_expert_counts": bool(
|
||
collection_metadata.get("requires_expert_counts", False)
|
||
),
|
||
"processed_samples": int(collection_metadata.get("processed_samples", 0)),
|
||
}
|
||
_save_oqe_imatrix(path, entries, metadata)
|
||
logger.info("oQe imatrix: wrote cache %s (%d entries)", path, len(entries))
|
||
return OQImatrixData(
|
||
entries=entries,
|
||
metadata=metadata,
|
||
path=str(path),
|
||
reused=False,
|
||
)
|
||
|
||
|
||
def _measure_sensitivity_from_model(
|
||
model,
|
||
tokenizer,
|
||
config,
|
||
oq_level,
|
||
calib_dataset="code_multilingual",
|
||
num_samples=32,
|
||
seq_length=256,
|
||
):
|
||
"""Measure per-layer quantization sensitivity on an already-loaded model.
|
||
|
||
Does NOT modify weights — uses temporary quantize→dequantize per layer.
|
||
Used by both streaming (after temporary load) and enhanced (before AWQ).
|
||
|
||
Returns:
|
||
Dict of {layer_idx: relative_mse_score}.
|
||
"""
|
||
calib_data = _load_calibration_data(
|
||
tokenizer,
|
||
dataset=calib_dataset,
|
||
num_samples=num_samples,
|
||
seq_length=seq_length,
|
||
)
|
||
if calib_data is None:
|
||
return {}
|
||
|
||
embed_fn, layers = _find_model_layers(model)
|
||
if embed_fn is None or layers is None:
|
||
return {}
|
||
|
||
inputs = embed_fn(calib_data)
|
||
inputs, layer_masks, position_ids = _prepare_layer_inputs(
|
||
model, layers, calib_data, inputs
|
||
)
|
||
sensitivity = {}
|
||
|
||
for layer_idx, block in enumerate(layers):
|
||
layer_mask = layer_masks[layer_idx] if layer_idx < len(layer_masks) else None
|
||
prev_aux = (
|
||
position_ids.get("prev_topk_indices")
|
||
if isinstance(position_ids, dict)
|
||
and position_ids.get("kind") == "glm_moe_dsa"
|
||
else None
|
||
)
|
||
out_float, baseline_aux = _forward_layer_result(
|
||
block, inputs, layer_mask, position_ids
|
||
)
|
||
if out_float is None:
|
||
continue
|
||
|
||
saved = _temporary_quantize_block(
|
||
block, config, oq_level, _OQ_DEFAULT_GROUP_SIZE
|
||
)
|
||
if isinstance(position_ids, dict) and position_ids.get("kind") == "glm_moe_dsa":
|
||
position_ids["prev_topk_indices"] = prev_aux
|
||
out_quant, _ = _forward_layer_result(block, inputs, layer_mask, position_ids)
|
||
if out_quant is not None:
|
||
raw_mse = ((out_float - out_quant) ** 2).mean()
|
||
out_magnitude = (out_float**2).mean()
|
||
mse_val = raw_mse / mx.maximum(out_magnitude, 1e-10)
|
||
mx.eval(mse_val)
|
||
sensitivity[layer_idx] = mse_val.item()
|
||
|
||
_restore_saved_weights(block, saved)
|
||
|
||
if isinstance(position_ids, dict) and position_ids.get("kind") == "glm_moe_dsa":
|
||
position_ids["prev_topk_indices"] = baseline_aux
|
||
inputs = out_float
|
||
mx.synchronize()
|
||
mx.clear_cache()
|
||
|
||
if sensitivity:
|
||
ranked = sorted(sensitivity.items(), key=lambda x: -x[1])
|
||
logger.info(
|
||
f"oQ{oq_level:g}: layer sensitivity (descending): "
|
||
+ ", ".join(f"L{i}={s:.4f}" for i, s in ranked)
|
||
)
|
||
|
||
return sensitivity
|
||
|
||
|
||
def _measure_sensitivity(
|
||
model_path: str,
|
||
config: dict,
|
||
oq_level,
|
||
calib_dataset="code_multilingual",
|
||
num_samples=32,
|
||
seq_length=256,
|
||
trust_remote_code: bool = False,
|
||
):
|
||
"""Measure sensitivity by loading model temporarily. Used by streaming path."""
|
||
from omlx.utils.model_loading import (
|
||
_checkpoint_has_mtp_weights,
|
||
_has_mtp_heads,
|
||
maybe_apply_pre_load_patches,
|
||
)
|
||
|
||
# Treat any model with a vision sub-config (vision_config / vit_config /
|
||
# mm_vision_tower) as a VLM for the MTP attach decision. The classifier
|
||
# in model_discovery._has_vision_subconfig owns the canonical predicate.
|
||
is_vlm = _has_vision_subconfig(config)
|
||
has_mtp_weights = _checkpoint_has_mtp_weights(model_path)
|
||
|
||
# Reuse the centralised pre-load dispatch so every current and future
|
||
# patch (MTP sanitize, DeepSeek V4, nested-visual, load_config, …) is
|
||
# applied exactly as in the production load path.
|
||
maybe_apply_pre_load_patches(model_path, for_vlm=is_vlm)
|
||
|
||
# maybe_apply_pre_load_patches leaves mtp_active False, which is correct
|
||
# for the text path: the patched qwen35_model.sanitize self-consistently
|
||
# strips mtp.* when no head is attached. The VLM path needs both patches.
|
||
# apply_mlx_vlm_mtp_patch fixes Model.sanitize so language_model.mtp.*
|
||
# weights survive the load with the correct keys (stock mlx-vlm sanitize
|
||
# strips them, which is what made the strict load fail with "Missing N
|
||
# parameters" and the measurement silently return {}). The runtime patch
|
||
# then attaches the MTP head so the checkpoint matches the model. Both
|
||
# are idempotent. Sensitivity only reads backbone decoder layers, so this
|
||
# is load-only.
|
||
restore_mtp_active = None
|
||
if is_vlm and _has_mtp_heads(config) and has_mtp_weights:
|
||
try:
|
||
from omlx.patches.mlx_lm_mtp import is_mtp_active, set_mtp_active
|
||
from omlx.patches.mlx_vlm_mtp import (
|
||
apply_mlx_vlm_mtp_patch,
|
||
apply_mlx_vlm_mtp_runtime_patch,
|
||
)
|
||
|
||
apply_mlx_vlm_mtp_patch()
|
||
apply_mlx_vlm_mtp_runtime_patch()
|
||
prev_active = is_mtp_active()
|
||
set_mtp_active(True)
|
||
restore_mtp_active = lambda: set_mtp_active(prev_active) # noqa: E731
|
||
except Exception as e:
|
||
logger.debug(f"mlx-vlm MTP runtime patch skipped for sensitivity: {e}")
|
||
|
||
try:
|
||
if is_vlm:
|
||
import mlx.nn as _nn
|
||
from mlx_vlm.utils import load_model as vlm_load_model
|
||
|
||
# mlx_vlm.load_model calls model.load_weights(weights) without strict=False.
|
||
# Shared-KV models (e.g. Gemma 4 2B/4B) omit k/v weights for shared layers,
|
||
# so strict=True raises ValueError. Relax temporarily — sensitivity only needs
|
||
# approximate weights; shared layers receive pre-computed KV at inference time.
|
||
_orig_lw = _nn.Module.load_weights
|
||
|
||
def _lenient_load_weights(self, file_or_weights, *args, **kwargs):
|
||
kwargs.pop("strict", None)
|
||
return _orig_lw(self, file_or_weights, *args, strict=False, **kwargs)
|
||
|
||
_nn.Module.load_weights = _lenient_load_weights
|
||
try:
|
||
# No QAT config override needed here: mlx_vlm.utils.load_model
|
||
# uses quantization_config.get("quant_method") rather than direct
|
||
# key access, so a missing quant_method falls through silently.
|
||
model = vlm_load_model(
|
||
Path(model_path),
|
||
lazy=True,
|
||
trust_remote_code=trust_remote_code,
|
||
)
|
||
finally:
|
||
_nn.Module.load_weights = _orig_lw
|
||
from mlx_lm.tokenizer_utils import load as load_tokenizer
|
||
|
||
tokenizer = load_tokenizer(Path(model_path))
|
||
else:
|
||
from omlx.utils.model_loading import lm_load_compat as lm_load
|
||
|
||
model, tokenizer = lm_load(
|
||
model_path,
|
||
lazy=True,
|
||
trust_remote_code=trust_remote_code,
|
||
model_config=_sensitivity_lm_config_override(config),
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"Sensitivity measurement: model load failed ({e})")
|
||
return {}
|
||
finally:
|
||
if restore_mtp_active is not None:
|
||
restore_mtp_active()
|
||
|
||
sensitivity = _measure_sensitivity_from_model(
|
||
model,
|
||
tokenizer,
|
||
config,
|
||
oq_level,
|
||
calib_dataset,
|
||
num_samples,
|
||
seq_length,
|
||
)
|
||
|
||
del model, tokenizer
|
||
mx.synchronize()
|
||
mx.clear_cache()
|
||
|
||
return sensitivity
|
||
|
||
|
||
_REQUANT_VALID_BITS = {2, 3, 4, 5, 6, 8}
|
||
|
||
|
||
def _perturb_bits_for(bits: int):
|
||
"""Closest valid re-quantization width below ``bits``, or None."""
|
||
lower = [b for b in _REQUANT_VALID_BITS if b < bits]
|
||
return max(lower) if lower else None
|
||
|
||
|
||
def _build_proxy_for_sensitivity(
|
||
model_path: str,
|
||
*,
|
||
config: dict | None = None,
|
||
dtype: str,
|
||
working_dir: str | None = None,
|
||
trust_remote_code: bool = False,
|
||
preserve_mtp: bool = False,
|
||
) -> Path:
|
||
"""Build a temporary uniform 4-bit proxy for calibration passes.
|
||
|
||
Used when the source model exceeds available RAM and full-fp16
|
||
sensitivity measurement / oQe imatrix collection is not feasible. The
|
||
proxy keeps oQ data-driven; without it, quantize_oq_streaming aborts
|
||
the run with a RuntimeError.
|
||
|
||
``working_dir`` controls where the proxy is written. Defaults to the
|
||
system temp dir when None, but callers should pass the parent of the
|
||
output directory so the proxy lands on the same volume the user has
|
||
already provisioned for the quantized output. This avoids the trap of
|
||
Linux ``/tmp`` being tmpfs (RAM-backed), which would defeat the whole
|
||
point of the OOM-driven proxy.
|
||
|
||
``preserve_mtp`` keeps the MTP head in the proxy so the imatrix head
|
||
pass can exercise its linears; sensitivity-only proxies leave it off.
|
||
|
||
The caller is responsible for deleting the returned directory.
|
||
"""
|
||
# Reserve a unique temp name and let the streaming writer create it.
|
||
proxy_dir = Path(tempfile.mkdtemp(prefix="omlx_oq_proxy_", dir=working_dir))
|
||
shutil.rmtree(proxy_dir)
|
||
_build_streaming_proxy_for_sensitivity(
|
||
model_path,
|
||
proxy_dir,
|
||
dtype=dtype,
|
||
trust_remote_code=trust_remote_code,
|
||
preserve_mtp=preserve_mtp,
|
||
)
|
||
return proxy_dir
|
||
|
||
|
||
def _build_streaming_proxy_for_sensitivity(
|
||
model_path: str,
|
||
output_path: Path,
|
||
*,
|
||
dtype: str,
|
||
trust_remote_code: bool = False,
|
||
preserve_mtp: bool = False,
|
||
) -> None:
|
||
"""Build a loadable 4-bit sensitivity proxy without loading the source.
|
||
|
||
This is the RAM-safe counterpart to ``mlx_lm.convert(..., quantize=True)``.
|
||
It uses the same header-only tensor index, streaming sanitize discovery,
|
||
FP8 dequantization, and chunked quantization path as oQ itself, but skips
|
||
sensitivity measurement and dynamic boost planning. The proxy is only used
|
||
to rank layer sensitivity, so a compact uniform-ish 4-bit model is enough.
|
||
"""
|
||
del trust_remote_code # Kept for API symmetry; model code comes from config.
|
||
|
||
source = Path(model_path)
|
||
output = Path(output_path)
|
||
if output.exists():
|
||
raise ValueError(f"Proxy output directory already exists: {output}")
|
||
|
||
with open(source / "config.json") as f:
|
||
config = json.load(f)
|
||
_validate_oq_dtype_for_model(config, dtype)
|
||
target_dtype = mx.bfloat16 if dtype == "bfloat16" else mx.float16
|
||
|
||
weight_files = sorted(source.glob("*.safetensors"))
|
||
if not weight_files:
|
||
raise ValueError(f"No .safetensors files found in {model_path}")
|
||
|
||
all_weights = _LazyTensorIndex(weight_files)
|
||
sanitize_fn = _build_model_sanitizer(config, text_only=False)
|
||
cast_predicate = getattr(sanitize_fn, "_omlx_cast_predicate", None)
|
||
if sanitize_fn is not None:
|
||
try:
|
||
plan = _discover_sanitize_plan(sanitize_fn, all_weights)
|
||
all_weights = _DiscoveredPlan(plan, all_weights)
|
||
logger.info(
|
||
"oQ proxy: discovered streaming sanitize plan, "
|
||
f"{len(all_weights)} output tensors"
|
||
)
|
||
except Exception as e:
|
||
raise RuntimeError(
|
||
"oQ proxy: streaming sanitize-plan discovery failed "
|
||
f"({e}). Extend _TrackedTensor for this sanitize pattern "
|
||
"or provide sensitivity_model_path explicitly."
|
||
) from e
|
||
|
||
config["_oq_non_quantizable"] = _build_non_quantizable_set(config)
|
||
config["_oq_use_budget_plan"] = False
|
||
config["_oq_boost_map"] = {}
|
||
|
||
output.mkdir(parents=True, exist_ok=False)
|
||
|
||
out_shard_data = {}
|
||
out_shard_idx = 0
|
||
weight_map = {}
|
||
per_layer_config = {}
|
||
tensor_names = list(all_weights.keys())
|
||
base_bits = _PROXY_QUANT_BITS
|
||
base_gs = _PROXY_QUANT_GROUP_SIZE
|
||
base_mode = "affine"
|
||
quantization_config = {
|
||
"group_size": base_gs,
|
||
"bits": base_bits,
|
||
"mode": base_mode,
|
||
}
|
||
|
||
def _flush_shard() -> None:
|
||
nonlocal out_shard_data, out_shard_idx
|
||
if not out_shard_data:
|
||
return
|
||
shard_name = f"model-{out_shard_idx + 1:05d}-of-PLACEHOLDER.safetensors"
|
||
shard_path = output / shard_name
|
||
mx.save_safetensors(str(shard_path), out_shard_data, metadata={"format": "mlx"})
|
||
for key in out_shard_data:
|
||
weight_map[key] = shard_name
|
||
out_shard_idx += 1
|
||
out_shard_data = {}
|
||
mx.synchronize()
|
||
mx.clear_cache()
|
||
|
||
for tensor_name in tensor_names:
|
||
handled_packed = False
|
||
if hasattr(all_weights, "pop_packed") and not _is_mtp_tensor(tensor_name):
|
||
src_info = all_weights.source_quant_info(tensor_name)
|
||
if src_info is not None and _should_quantize_tensor(
|
||
tensor_name, all_weights.plan_shape(tensor_name)
|
||
):
|
||
pred = universal_quant_predicate(
|
||
tensor_name, None, config, _PROXY_QUANT_BITS
|
||
)
|
||
if pred is not False and base_bits >= src_info["bits"]:
|
||
qw, scales = all_weights.pop_packed(tensor_name)
|
||
base = tensor_name[: -len(".weight")]
|
||
out_shard_data[f"{base}.weight"] = qw
|
||
out_shard_data[f"{base}.scales"] = scales
|
||
per_layer_config[base] = {
|
||
"bits": src_info["bits"],
|
||
"group_size": src_info["group_size"],
|
||
"mode": src_info["mode"],
|
||
}
|
||
del qw, scales
|
||
handled_packed = True
|
||
|
||
if not handled_packed:
|
||
w_mx = all_weights.pop(tensor_name)
|
||
if isinstance(w_mx, _LazyTensor):
|
||
w_mx = w_mx[:]
|
||
shape = w_mx.shape
|
||
|
||
if _is_mtp_tensor(tensor_name) and not preserve_mtp:
|
||
del w_mx
|
||
continue
|
||
|
||
if _should_quantize_tensor(tensor_name, shape):
|
||
pred = universal_quant_predicate(
|
||
tensor_name, None, config, _PROXY_QUANT_BITS
|
||
)
|
||
if pred is not False and len(shape) >= 2 and shape[-1] % base_gs == 0:
|
||
if (
|
||
mx.issubdtype(w_mx.dtype, mx.floating)
|
||
and w_mx.dtype != target_dtype
|
||
):
|
||
w_mx = w_mx.astype(target_dtype)
|
||
qw, scales, biases = _quantize_chunked(
|
||
w_mx, base_gs, base_bits, base_mode
|
||
)
|
||
base = (
|
||
tensor_name[:-7]
|
||
if tensor_name.endswith(".weight")
|
||
else tensor_name
|
||
)
|
||
out_shard_data[f"{base}.weight"] = qw
|
||
out_shard_data[f"{base}.scales"] = scales
|
||
if biases is not None:
|
||
out_shard_data[f"{base}.biases"] = biases
|
||
del qw, scales, biases
|
||
else:
|
||
if cast_predicate is None or cast_predicate(tensor_name):
|
||
w_mx = _cast_passthrough_tensor(tensor_name, w_mx, target_dtype)
|
||
out_shard_data[tensor_name] = w_mx
|
||
else:
|
||
if cast_predicate is None or cast_predicate(tensor_name):
|
||
w_mx = _cast_passthrough_tensor(tensor_name, w_mx, target_dtype)
|
||
out_shard_data[tensor_name] = w_mx
|
||
|
||
del w_mx
|
||
|
||
if sum(v.nbytes for v in out_shard_data.values()) >= _MAX_SHARD_BYTES:
|
||
_flush_shard()
|
||
|
||
del all_weights
|
||
mx.synchronize()
|
||
mx.clear_cache()
|
||
_flush_shard()
|
||
|
||
total_shards = out_shard_idx
|
||
if total_shards == 1:
|
||
only = output / "model-00001-of-PLACEHOLDER.safetensors"
|
||
final = output / "model.safetensors"
|
||
only.rename(final)
|
||
for key in list(weight_map):
|
||
weight_map[key] = "model.safetensors"
|
||
elif total_shards > 1:
|
||
for i in range(total_shards):
|
||
old_name = f"model-{i + 1:05d}-of-PLACEHOLDER.safetensors"
|
||
new_name = f"model-{i + 1:05d}-of-{total_shards:05d}.safetensors"
|
||
old_path = output / old_name
|
||
new_path = output / new_name
|
||
if old_path.exists():
|
||
old_path.rename(new_path)
|
||
for key, value in list(weight_map.items()):
|
||
if value == old_name:
|
||
weight_map[key] = new_name
|
||
|
||
total_size = sum(f.stat().st_size for f in output.glob("*.safetensors"))
|
||
index = {
|
||
"metadata": {"total_size": total_size},
|
||
"weight_map": dict(sorted(weight_map.items())),
|
||
}
|
||
with open(output / "model.safetensors.index.json", "w") as f:
|
||
json.dump(index, f, indent=2)
|
||
|
||
output_config = dict(config)
|
||
for temp_key in (
|
||
"_oq_sensitivity_map",
|
||
"_oq_boost_map",
|
||
"_oq_use_budget_plan",
|
||
"_oq_non_quantizable",
|
||
):
|
||
output_config.pop(temp_key, None)
|
||
if not preserve_mtp:
|
||
_normalize_mtp_in_config(output_config)
|
||
quant_info = dict(quantization_config)
|
||
for key, val in per_layer_config.items():
|
||
quant_info[key] = val
|
||
output_config["quantization"] = quant_info
|
||
output_config["quantization_config"] = quant_info
|
||
with open(output / "config.json", "w") as f:
|
||
json.dump(output_config, f, indent=2, ensure_ascii=False)
|
||
|
||
_copy_model_sidecars(source, output)
|
||
|
||
|
||
def _measure_sensitivity_from_quantized_model(
|
||
model_path: str,
|
||
config: dict,
|
||
oq_level,
|
||
calib_dataset="code_multilingual",
|
||
num_samples=32,
|
||
seq_length=256,
|
||
trust_remote_code: bool = False,
|
||
):
|
||
"""Measure sensitivity via re-quantization on a quantized model.
|
||
|
||
Loads a quantized model (~4x less memory than fp16) and perturbs each
|
||
layer by re-quantizing one valid bit-width below the module's own
|
||
bits. The relative MSE ranking matches fp16 qdq-MSE with ~90% top-10
|
||
overlap.
|
||
"""
|
||
from omlx.utils.model_loading import (
|
||
_checkpoint_has_mtp_weights,
|
||
_has_mtp_heads,
|
||
lm_load_compat as lm_load,
|
||
maybe_apply_pre_load_patches,
|
||
)
|
||
|
||
# Reuse the centralised pre-load dispatch (DeepSeek V4 base patch,
|
||
# load_model replacement for F8_E8M0 checkpoints, MTP sanitize, ...)
|
||
# so the quantized source/proxy loads exactly as in production.
|
||
# Idempotent; harmless for plain mlx-lm proxies.
|
||
is_vlm = _has_vision_subconfig(config)
|
||
has_mtp_weights = _checkpoint_has_mtp_weights(model_path)
|
||
maybe_apply_pre_load_patches(model_path, for_vlm=is_vlm)
|
||
|
||
restore_mtp_active = None
|
||
try:
|
||
if is_vlm:
|
||
if _has_mtp_heads(config) and has_mtp_weights:
|
||
try:
|
||
from omlx.patches.mlx_lm_mtp import is_mtp_active, set_mtp_active
|
||
from omlx.patches.mlx_vlm_mtp import (
|
||
apply_mlx_vlm_mtp_patch,
|
||
apply_mlx_vlm_mtp_runtime_patch,
|
||
)
|
||
|
||
apply_mlx_vlm_mtp_patch()
|
||
apply_mlx_vlm_mtp_runtime_patch()
|
||
prev_active = is_mtp_active()
|
||
set_mtp_active(True)
|
||
restore_mtp_active = lambda: set_mtp_active(
|
||
prev_active
|
||
) # noqa: E731
|
||
except Exception as e:
|
||
logger.debug(
|
||
"mlx-vlm MTP runtime patch skipped for proxy sensitivity: "
|
||
f"{e}"
|
||
)
|
||
|
||
from mlx_lm.tokenizer_utils import load as load_tokenizer
|
||
from mlx_vlm.utils import load_model as vlm_load_model
|
||
|
||
model = vlm_load_model(
|
||
Path(model_path),
|
||
lazy=True,
|
||
trust_remote_code=trust_remote_code,
|
||
)
|
||
tokenizer = load_tokenizer(Path(model_path))
|
||
else:
|
||
# Mirror the main quantize path's MTP patch sequence so an
|
||
# MTP-bearing quantized proxy (e.g. a Qwen3.5 LLM oQ output with
|
||
# preserve_mtp=True) loads cleanly. Without set_mtp_active(True) the
|
||
# mlx-lm __init__ skips ``self.mtp`` and the load rejects the
|
||
# ``mtp.*`` weights present in the proxy.
|
||
try:
|
||
from omlx.patches.mlx_lm_mtp import (
|
||
apply_mlx_lm_mtp_patch,
|
||
is_mtp_active,
|
||
set_mtp_active,
|
||
)
|
||
|
||
have_lm_patch = apply_mlx_lm_mtp_patch()
|
||
except Exception:
|
||
have_lm_patch = False
|
||
is_mtp_active = None
|
||
set_mtp_active = None
|
||
|
||
if have_lm_patch:
|
||
prev_active = is_mtp_active()
|
||
set_mtp_active(True)
|
||
restore_mtp_active = lambda: set_mtp_active(prev_active) # noqa: E731
|
||
|
||
model, tokenizer = lm_load(
|
||
model_path,
|
||
lazy=True,
|
||
trust_remote_code=trust_remote_code,
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"Sensitivity proxy load failed ({e})")
|
||
return {}
|
||
finally:
|
||
if restore_mtp_active is not None:
|
||
restore_mtp_active()
|
||
|
||
if config.get("model_type") == "glm_moe_dsa":
|
||
capped_samples = min(num_samples, 16)
|
||
capped_seq = min(seq_length, 128)
|
||
if capped_samples != num_samples or capped_seq != seq_length:
|
||
logger.info(
|
||
"GLM MoE DSA proxy sensitivity: capping calibration to "
|
||
f"{capped_samples} samples x {capped_seq} tokens"
|
||
)
|
||
num_samples = capped_samples
|
||
seq_length = capped_seq
|
||
|
||
calib_data = _load_calibration_data(
|
||
tokenizer,
|
||
dataset=calib_dataset,
|
||
num_samples=num_samples,
|
||
seq_length=seq_length,
|
||
)
|
||
if calib_data is None:
|
||
del model, tokenizer
|
||
mx.synchronize()
|
||
mx.clear_cache()
|
||
return {}
|
||
|
||
embed_fn, layers = _find_model_layers(model)
|
||
if embed_fn is None or layers is None:
|
||
del model, tokenizer
|
||
mx.synchronize()
|
||
mx.clear_cache()
|
||
return {}
|
||
|
||
inputs = embed_fn(calib_data)
|
||
inputs, layer_masks, position_ids = _prepare_layer_inputs(
|
||
model, layers, calib_data, inputs
|
||
)
|
||
sensitivity = {}
|
||
|
||
for layer_idx, block in enumerate(layers):
|
||
layer_mask = layer_masks[layer_idx] if layer_idx < len(layer_masks) else None
|
||
prev_aux = (
|
||
position_ids.get("prev_topk_indices")
|
||
if isinstance(position_ids, dict)
|
||
and position_ids.get("kind") == "glm_moe_dsa"
|
||
else None
|
||
)
|
||
out_baseline, baseline_aux = _forward_layer_result(
|
||
block, inputs, layer_mask, position_ids
|
||
)
|
||
if out_baseline is None:
|
||
continue
|
||
# Materialize the baseline before mutating module weights below.
|
||
# Without this, the lazy graph would resolve baseline against the
|
||
# already-perturbed weights and the MSE would always be ~0.
|
||
mx.eval(out_baseline)
|
||
|
||
saved = {}
|
||
for p, m in tree_flatten(block.leaf_modules(), is_leaf=nn.Module.is_module):
|
||
if not hasattr(m, "scales") or not hasattr(m, "weight"):
|
||
continue
|
||
bits = getattr(m, "bits", 4)
|
||
gs = getattr(m, "group_size", 64)
|
||
mode = getattr(m, "mode", "affine")
|
||
# Perturb at the closest valid bit-width below the module's own
|
||
# bits (8→6, 4→3, ...). bits-1 alone silently skipped every
|
||
# 8-bit module (7 is not a valid width), which made the whole
|
||
# measurement a no-op on 8-bit-dominated checkpoints.
|
||
perturb_bits = _perturb_bits_for(bits)
|
||
if perturb_bits is None:
|
||
continue
|
||
w_float = mx.dequantize(
|
||
m.weight,
|
||
m.scales,
|
||
getattr(m, "biases", None),
|
||
group_size=gs,
|
||
bits=bits,
|
||
mode=mode,
|
||
)
|
||
saved[p] = (m.weight, m.scales, getattr(m, "biases", None), bits, mode)
|
||
qw, sc, *rest = mx.quantize(
|
||
w_float, group_size=gs, bits=perturb_bits, mode="affine"
|
||
)
|
||
m.weight = qw
|
||
m.scales = sc
|
||
m.biases = rest[0] if rest else None
|
||
m.bits = perturb_bits
|
||
m.mode = "affine"
|
||
# Force re-quant materialization so the next forward sees the
|
||
# perturbed weights instead of the lazy reference to the originals.
|
||
if m.biases is not None:
|
||
mx.eval(m.weight, m.scales, m.biases)
|
||
else:
|
||
mx.eval(m.weight, m.scales)
|
||
|
||
if isinstance(position_ids, dict) and position_ids.get("kind") == "glm_moe_dsa":
|
||
position_ids["prev_topk_indices"] = prev_aux
|
||
out_perturbed, _ = _forward_layer_result(
|
||
block, inputs, layer_mask, position_ids
|
||
)
|
||
|
||
modules_by_path = dict(
|
||
tree_flatten(block.leaf_modules(), is_leaf=nn.Module.is_module)
|
||
)
|
||
for p, (w, s, b, orig_bits, orig_mode) in saved.items():
|
||
if p in modules_by_path:
|
||
mod = modules_by_path[p]
|
||
mod.weight = w
|
||
mod.scales = s
|
||
if b is not None:
|
||
mod.biases = b
|
||
elif hasattr(mod, "biases"):
|
||
del mod.biases
|
||
mod.bits = orig_bits
|
||
mod.mode = orig_mode
|
||
|
||
if out_perturbed is not None:
|
||
# Cast to float32 first: float16 squared differences overflow
|
||
# easily on long sequences, producing NaN sensitivity scores.
|
||
ob32 = out_baseline.astype(mx.float32)
|
||
op32 = out_perturbed.astype(mx.float32)
|
||
raw_mse = ((ob32 - op32) ** 2).mean()
|
||
out_mag = (ob32**2).mean()
|
||
mse_val = raw_mse / mx.maximum(out_mag, 1e-10)
|
||
mx.eval(mse_val)
|
||
sensitivity[layer_idx] = mse_val.item()
|
||
|
||
if isinstance(position_ids, dict) and position_ids.get("kind") == "glm_moe_dsa":
|
||
position_ids["prev_topk_indices"] = baseline_aux
|
||
inputs = out_baseline
|
||
mx.eval(inputs)
|
||
mx.synchronize()
|
||
mx.clear_cache()
|
||
|
||
del model, tokenizer
|
||
mx.synchronize()
|
||
mx.clear_cache()
|
||
|
||
if sensitivity:
|
||
ranked = sorted(sensitivity.items(), key=lambda x: -x[1])
|
||
logger.info(
|
||
f"oQ{oq_level:g}: proxy sensitivity (descending): "
|
||
+ ", ".join(f"L{i}={s:.4f}" for i, s in ranked)
|
||
)
|
||
|
||
return sensitivity
|