Files
mlx-lm/mlx_lm/utils.py
T
2026-04-22 00:34:09 -07:00

992 lines
32 KiB
Python

# Copyright © 2023-2024 Apple Inc.
import copy
import glob
import importlib
import inspect
import json
import os
import resource
import shutil
from pathlib import Path
from textwrap import dedent
from typing import (
Any,
Callable,
Dict,
List,
Optional,
Tuple,
Type,
Union,
)
import mlx.core as mx
import mlx.nn as nn
if os.getenv("MLXLM_USE_MODELSCOPE", "False").lower() == "true":
try:
from modelscope import snapshot_download
except ImportError:
raise ImportError("Run `pip install modelscope` to use ModelScope.")
else:
from huggingface_hub import snapshot_download
# For large models with lots of files
resource.setrlimit(resource.RLIMIT_NOFILE, (2048, 4096))
from mlx.utils import tree_flatten, tree_map, tree_reduce, tree_unflatten
# Local imports
from .tokenizer_utils import TokenizerWrapper
from .tokenizer_utils import load as _load_tokenizer
# Constants
MODEL_REMAPPING = {
"mistral": "llama",
"llava": "mistral3",
"phi-msft": "phixtral",
"falcon_mamba": "mamba",
"joyai_llm_flash": "deepseek_v3",
"kimi_k2": "deepseek_v3",
"qwen2_5_vl": "qwen2_vl",
"minimax_m2": "minimax",
"iquestcoder": "llama",
}
MAX_FILE_SIZE_GB = 5
def _parse_size(x):
sizes = {"M": 1e6, "G": 1e9, "MB": 1e6, "GB": 1e9, "": 1}
split = 0
for xi in x:
if not (xi.isdigit() or xi == "."):
break
split += 1
digits = float(x[:split])
size = (x[split:]).strip().upper()
return int(digits * sizes[size])
def _unpack_awq_weights(qweight: mx.array) -> mx.array:
bits = 4
pack_factor = 32 // bits
out_features, packed_in = qweight.shape
in_features = packed_in * pack_factor
mask = (1 << bits) - 1 # e.g., 0xF for 4-bit
shifts = mx.array([0, 4, 1, 5, 2, 6, 3, 7]) * bits
unpacked = (qweight[..., None] >> shifts) & mask
return unpacked.reshape(out_features, in_features)
def _transform_awq_weights(
weights: Dict[str, mx.array],
quantization_config: Dict[str, Any],
) -> Tuple[Dict[str, mx.array], Dict[str, Any]]:
bits = quantization_config.get("bits", 4)
if bits != 4:
raise ValueError(f"Only {bits=} is supported for AutoAWQ/GPTQ models.")
group_size = quantization_config.get("group_size", 128)
new_weights = {}
for key in list(weights.keys()):
if key.endswith(".g_idx"):
raise ValueError(
f"Found {key} in weights. Models with non-contiguous group indices "
"(g_idx) are not currently supported. Please use a model without g_idx "
"or re-quantize the model using mlx_lm.convert."
)
if key.endswith(".qweight"):
prefix = key[:-8] # Remove ".qweight"
qweight = weights[f"{prefix}.qweight"]
scales_key = f"{prefix}.scales"
qzeros_key = f"{prefix}.qzeros"
scales = weights[scales_key]
# AutoAWQ stores qweight as [in_features, out_features // pack_factor]
# MLX expects [out_features, in_features // pack_factor]
# We need to unpack, transpose, and repack
pack_factor = 32 // bits
in_features, packed_out = qweight.shape
out_features = packed_out * pack_factor
n_groups = in_features // group_size
# Unpack qweight: [in_features, out_features // pack_factor] -> [in_features, out_features]
unpacked_weight = _unpack_awq_weights(qweight)
# Transpose to MLX format: [out_features, in_features]
unpacked_weight = unpacked_weight.T
# Repack for MLX: [out_features, in_features] -> [out_features, in_features // pack_factor]
packed_in = in_features // pack_factor
repacked = unpacked_weight.reshape(out_features, packed_in, pack_factor)
shifts = mx.arange(pack_factor) * bits
weight = (
(repacked.astype(mx.uint32) << shifts).sum(axis=-1).astype(mx.uint32)
)
scales = mx.contiguous(scales.T)
# Handle qzeros if present (asymmetric quantization)
if qzeros_key in weights:
qzeros = weights[qzeros_key]
# qzeros shape: [n_groups, out_features // pack_factor]
# Unpack to get [n_groups, out_features]
unpacked_zeros = _unpack_awq_weights(qzeros)
# Transpose to [out_features, n_groups]
unpacked_zeros = unpacked_zeros.T
# Compute biases: MLX dequant = weight * scale + bias
# AWQ dequant = (weight - zero) * scale
# So: bias = -zero * scale
biases = -unpacked_zeros.astype(mx.float32) * scales
else:
# Symmetric quantization - zeros are implicitly 2^(bits-1)
zero_point = 1 << (bits - 1) # e.g., 8 for 4-bit
biases = mx.full(scales.shape, -zero_point, dtype=mx.float32) * scales
new_weights[f"{prefix}.weight"] = weight
new_weights[f"{prefix}.scales"] = scales
new_weights[f"{prefix}.biases"] = biases.astype(scales.dtype)
model_dtype = scales.dtype
elif not any(
key.endswith(suffix) for suffix in [".qweight", ".qzeros", ".scales"]
):
new_weights[key] = weights[key]
for k, w in new_weights.items():
if mx.issubdtype(w.dtype, mx.floating):
new_weights[k] = w.astype(model_dtype)
mlx_quantization = {
"group_size": group_size,
"bits": bits,
}
return new_weights, mlx_quantization
def _get_classes(config: dict):
"""
Retrieve the model and model args classes based on the configuration.
Args:
config (dict): The model configuration.
Returns:
A tuple containing the Model class and the ModelArgs class.
"""
model_type = config["model_type"]
model_type = MODEL_REMAPPING.get(model_type, model_type)
try:
arch = importlib.import_module(f"mlx_lm.models.{model_type}")
except ImportError:
msg = f"Model type {model_type} not supported."
raise ValueError(msg)
return arch.Model, arch.ModelArgs
def get_total_parameters(model):
leaf_modules = tree_flatten(
model.leaf_modules(), is_leaf=lambda m: isinstance(m, nn.Module)
)
def nparams(m):
if hasattr(m, "bits"):
n = 0 if not hasattr(m, "bias") else m.bias.size
return n + m.weight.size * 32 // m.bits
return sum(v.size for _, v in tree_flatten(m.parameters()))
return sum(nparams(m) for _, m in leaf_modules)
def compute_bits_per_weight(model):
model_bytes = tree_reduce(
lambda acc, x: acc + x.nbytes if isinstance(x, mx.array) else acc, model, 0
)
model_params = get_total_parameters(model)
return model_bytes * 8 / model_params
def _download(
path_or_hf_repo: str,
revision: Optional[str] = None,
allow_patterns: List[str] = None,
) -> Path:
"""
Ensures the model is available locally. If the path does not exist locally,
it is downloaded from the Hugging Face Hub.
Args:
path_or_hf_repo (str): The local path or Hugging Face repository ID of the model.
revision (str, optional): A revision id which can be a branch name, a tag, or a commit hash.
Returns:
Path: The local file path.
"""
model_path = Path(path_or_hf_repo)
if not model_path.exists():
allow_patterns = allow_patterns or [
"*.json",
"model*.safetensors",
"*.py",
"tokenizer.model",
"*.tiktoken",
"tiktoken.model",
"*.txt",
"*.jsonl",
"*.jinja",
]
model_path = Path(
snapshot_download(
path_or_hf_repo,
revision=revision,
allow_patterns=allow_patterns,
)
)
return model_path
def hf_repo_to_path(hf_repo):
return Path(snapshot_download(hf_repo, local_files_only=True))
def load_config(model_path: Path) -> dict:
with open(model_path / "config.json", "r") as f:
config = json.load(f)
generation_config_file = model_path / "generation_config.json"
if generation_config_file.exists():
generation_config = {}
try:
with open(generation_config_file, "r") as f:
generation_config = json.load(f)
except json.JSONDecodeError:
pass
if eos_token_id := generation_config.get("eos_token_id", False):
config["eos_token_id"] = eos_token_id
return config
def load_model(
model_path: Path,
lazy: bool = False,
strict: bool = True,
model_config: Optional[Dict[str, Any]] = None,
get_model_classes: Callable[[dict], Tuple[Type[nn.Module], Type]] = _get_classes,
) -> Tuple[nn.Module, dict]:
"""
Load and initialize the model from a given path.
Args:
model_path (Path): The path to load the model from.
lazy (bool): If False eval the model parameters to make sure they are
loaded in memory before returning, otherwise they will be loaded
when needed. Default: ``False``
strict (bool): Whether or not to raise an exception if weights don't
match. Default: ``True``
model_config (dict, optional): Optional configuration parameters for the
model. Defaults to an empty dictionary.
get_model_classes (Callable[[dict], Tuple[Type[nn.Module], Type]], optional):
A function that returns the model class and model args class given a config.
Defaults to the ``_get_classes`` function.
Returns:
Tuple[nn.Module, dict[str, Any]]: The loaded and initialized model and config.
Raises:
FileNotFoundError: If the weight files (.safetensors) are not found.
ValueError: If the model class or args class are not found or cannot be instantiated.
"""
config = load_config(model_path)
if model_config is not None:
config.update(model_config)
weight_files = glob.glob(str(model_path / "model*.safetensors"))
if not weight_files and strict:
raise FileNotFoundError(f"No safetensors found in {model_path}")
weights = {}
for wf in weight_files:
weights.update(mx.load(wf))
if (model_file := config.get("model_file")) is not None:
spec = importlib.util.spec_from_file_location(
"custom_model",
model_path / model_file,
)
arch = importlib.util.module_from_spec(spec)
spec.loader.exec_module(arch)
model_class, model_args_class = arch.Model, arch.ModelArgs
else:
model_class, model_args_class = get_model_classes(config=config)
if "quantization_config" not in config:
text_config = config.get("text_config", {})
if "quantization_config" in text_config:
config["quantization_config"] = text_config["quantization_config"]
model_args = model_args_class.from_dict(config)
model = model_class(model_args)
if hasattr(model, "sanitize"):
weights = model.sanitize(weights)
def _quantize(quantization):
def class_predicate(p, m):
# Handle custom per layer quantizations
if p in config["quantization"]:
return config["quantization"][p]
if not hasattr(m, "to_quantized"):
return False
return f"{p}.scales" in weights
nn.quantize(
model,
group_size=quantization["group_size"],
bits=quantization["bits"],
mode=quantization.get("mode", "affine"),
class_predicate=class_predicate,
)
if (quantization := config.get("quantization", None)) is not None:
_quantize(quantization)
elif quantization_config := config.get("quantization_config", False):
# Handle legacy quantization config
quant_method = quantization_config["quant_method"]
if quant_method == "bitnet":
from .models.bitlinear_layers import bitnet_quantize
model = bitnet_quantize(model, quantization_config)
elif quant_method == "mxfp4":
quantization = {"group_size": 32, "bits": 4, "mode": "mxfp4"}
config["quantization"] = quantization
config["quantization_config"] = quantization
_quantize(quantization)
elif quant_method == "compressed-tensors":
quantization = {"group_size": 32, "bits": 4, "mode": "affine"}
config["quantization"] = quantization
config["quantization_config"] = quantization
_quantize(quantization)
elif quant_method in ("awq", "gptq"):
# Transform AutoAWQ/GPTQ packed weights to MLX format
weights, quantization = _transform_awq_weights(weights, quantization_config)
config["quantization"] = quantization
config["quantization_config"] = quantization
_quantize(quantization)
if config.get("quantize_activations", False):
def _maybe_qq(m):
if isinstance(m, nn.QuantizedLinear):
if m.mode not in ("nvfp4", "mxfp8"):
raise ValueError(
"Mode ({m.mode}) does not support activation quantization"
)
if m.get("bias", False):
raise ValueError(
"Linear layer with bias does not support activation quantization"
)
out_dims, in_dims = m.weight.shape
in_dims *= 32 // m.bits
return nn.QQLinear(in_dims, out_dims, m.group_size, m.bits, m.mode)
else:
return m
leaves = tree_map(_maybe_qq, model.leaf_modules(), is_leaf=nn.Module.is_module)
model.update_modules(leaves)
model.eval()
model.load_weights(list(weights.items()), strict=strict)
if not lazy:
mx.eval(model.parameters())
return model, config
def load_adapters(model: nn.Module, adapter_path: str) -> nn.Module:
from .tuner.utils import load_adapters as _load_adapters
return _load_adapters(model, adapter_path)
def load_tokenizer(model_path, tokenizer_config_extra=None, eos_token_ids=None):
"""Load a huggingface tokenizer and try to infer the type of streaming
detokenizer to use.
"""
model_path = _download(
model_path,
allow_patterns=[
"*.json",
"*.py",
"tokenizer.model",
"*.tiktoken",
"tiktoken.model",
"*.txt",
"*.jsonl",
"*.jinja",
],
)
return _load_tokenizer(
model_path,
tokenizer_config_extra,
eos_token_ids=eos_token_ids,
)
def load(
path_or_hf_repo: str,
tokenizer_config: Optional[Dict[str, Any]] = None,
model_config: Optional[Dict[str, Any]] = None,
adapter_path: Optional[str] = None,
lazy: bool = False,
return_config: bool = False,
revision: Optional[str] = None,
) -> Union[
Tuple[nn.Module, TokenizerWrapper],
Tuple[nn.Module, TokenizerWrapper, Dict[str, Any]],
]:
"""
Load the model and tokenizer from a given path or a huggingface repository.
Args:
path_or_hf_repo (Path): The path or the huggingface repository to load the model from.
tokenizer_config (dict, optional): Configuration parameters specifically for the tokenizer.
Defaults to an empty dictionary.
model_config(dict, optional): Configuration parameters specifically for the model.
Defaults to an empty dictionary.
adapter_path (str, optional): Path to the LoRA adapters. If provided, applies LoRA layers
to the model. Default: ``None``.
lazy (bool): If ``False`` eval the model parameters to make sure they are
loaded in memory before returning, otherwise they will be loaded
when needed. Default: ``False``
return_config (bool: If ``True`` return the model config as the last item..
revision (str, optional): A revision id which can be a branch name, a tag, or a commit hash.
Returns:
Union[Tuple[nn.Module, TokenizerWrapper], Tuple[nn.Module, TokenizerWrapper, Dict[str, Any]]]:
A tuple containing the loaded model, tokenizer and, if requested, the model config.
Raises:
FileNotFoundError: If config file or safetensors are not found.
ValueError: If model class or args class are not found.
"""
model_path = _download(path_or_hf_repo, revision=revision)
model, config = load_model(model_path, lazy, model_config=model_config)
if adapter_path is not None:
model = load_adapters(model, adapter_path)
model.eval()
tokenizer = load_tokenizer(
model_path, tokenizer_config, eos_token_ids=config.get("eos_token_id", None)
)
if return_config:
return model, tokenizer, config
else:
return model, tokenizer
def sharded_load(
repo,
pipeline_group: Optional[mx.distributed.Group] = None,
tensor_group: Optional[mx.distributed.Group] = None,
return_config: bool = False,
*,
tokenizer_config: Optional[Dict[str, Any]] = None,
):
# Get model path with everything but weight safetensors
model_path = _download(
repo,
allow_patterns=[
"*.json",
"*.py",
"tokenizer.model",
"*.tiktoken",
"tiktoken.model",
"*.txt",
"*.jsonl",
"*.jinja",
],
)
# Lazy load model to figure out what type of sharding we can do and which
# weights we need to download.
model, config = load_model(model_path, lazy=True, strict=False)
has_pipelining = hasattr(model, "model") and hasattr(model.model, "pipeline")
has_tensor_parallel = hasattr(model, "shard")
if pipeline_group is not None and not has_pipelining:
raise ValueError(
"The model does not support pipelining but a pipeline_group was provided"
)
if tensor_group is not None and not has_tensor_parallel:
raise ValueError(
"The model does not support tensor parallelism but a tensor_group was provided"
)
if not has_pipelining and not has_tensor_parallel:
raise ValueError("The model does not support any sharding")
if pipeline_group is tensor_group is None:
if has_tensor_parallel:
tensor_group = mx.distributed.init()
elif has_pipelining:
pipeline_group = mx.distributed.init()
# If pipelining then figure out which files we need for the local shard
if pipeline_group is not None:
model.model.pipeline(pipeline_group)
# Figure out which files we need for the local shard
with open(model_path / "model.safetensors.index.json", "r") as fid:
weight_index = json.load(fid)["weight_map"]
local_files = set()
for k, _ in tree_flatten(model.parameters()):
if file_name := weight_index.get(k, None) is None:
raise ValueError(
"Pipeline loading is only supported for MLX converted models."
)
local_files.add(weight_index[k])
# Download weights for local shard
_download(repo, allow_patterns=local_files)
else:
_download(repo)
# Load and shard the model, and load the weights
tokenizer = load_tokenizer(
model_path,
tokenizer_config or {"trust_remote_code": True},
eos_token_ids=config.get("eos_token_id", None),
)
model, _ = load_model(model_path, lazy=True, strict=False)
if tensor_group is not None:
model.shard(tensor_group)
if pipeline_group is not None:
model.model.pipeline(pipeline_group)
mx.eval(model.parameters())
# Synchronize processes to avoid timeout
mx.eval(mx.distributed.all_sum(mx.array(1.0), stream=mx.cpu))
if return_config:
return model, tokenizer, config
else:
return model, tokenizer
def pipeline_load(repo, return_config=False):
return sharded_load(repo, mx.distributed.init(), None, return_config)
def make_shards(weights: dict, max_file_size_gb: int = MAX_FILE_SIZE_GB) -> list:
"""
Splits the weights into smaller shards.
Args:
weights (dict): Model weights.
max_file_size_gb (int): Maximum size of each shard in gigabytes.
Returns:
list: List of weight shards.
"""
max_file_size_bytes = max_file_size_gb << 30
shards = []
shard, shard_size = {}, 0
for k, v in weights.items():
if shard_size + v.nbytes > max_file_size_bytes:
shards.append(shard)
shard, shard_size = {}, 0
shard[k] = v
shard_size += v.nbytes
shards.append(shard)
return shards
def create_model_card(path: Union[str, Path], hf_path: Union[str, Path, None]):
"""
Uploads the model to Hugging Face hub.
Args:
path (Union[str, Path]): Local path to the model.
hf_path (Union[str, Path, None]): Path to the original Hugging Face model.
"""
from huggingface_hub import ModelCard, ModelCardData
if hf_path is None:
card = ModelCard.from_template(ModelCardData(language="en"))
else:
card = ModelCard.load(hf_path)
card.data.library_name = "mlx"
card.data.pipeline_tag = "text-generation"
if card.data.tags is None:
card.data.tags = ["mlx"]
elif "mlx" not in card.data.tags:
card.data.tags += ["mlx"]
if hf_path is not None:
card.data.base_model = str(hf_path)
card.text = ""
card.save(os.path.join(path, "README.md"))
def upload_to_hub(path: str, upload_repo: str):
"""
Uploads the model to Hugging Face hub.
Args:
path (str): Local path to the model.
upload_repo (str): Name of the HF repo to upload to.
"""
from huggingface_hub import HfApi, ModelCard, logging
from . import __version__
logging.set_verbosity_info()
card_path = Path(path) / "README.md"
card = ModelCard.load(card_path)
hf_path = card.data.base_model
if hf_path is not None:
provenance = f"""
This model [{upload_repo}](https://huggingface.co/{upload_repo}) was
converted to MLX format from [{hf_path}](https://huggingface.co/{hf_path})
using mlx-lm version **{__version__}**.
"""
else:
provenance = ""
card.text = dedent(
f"""
# {upload_repo}
{provenance}
## Use with mlx
```bash
pip install mlx-lm
```
```python
from mlx_lm import load, generate
model, tokenizer = load("{upload_repo}")
prompt = "hello"
if tokenizer.chat_template is not None:
messages = [{{"role": "user", "content": prompt}}]
prompt = tokenizer.apply_chat_template(
messages, add_generation_prompt=True, return_dict=False,
)
response = generate(model, tokenizer, prompt=prompt, verbose=True)
```
"""
)
card.save(card_path)
api = HfApi()
api.create_repo(repo_id=upload_repo, exist_ok=True)
api.upload_large_folder(
folder_path=path,
repo_id=upload_repo,
repo_type="model",
)
print(f"Upload successful, go to https://huggingface.co/{upload_repo} for details.")
def save_model(
save_path: Union[str, Path],
model: nn.Module,
*,
donate_model: bool = False,
) -> None:
"""Save model weights and metadata index into specified directory."""
if isinstance(save_path, str):
save_path = Path(save_path)
save_path.mkdir(parents=True, exist_ok=True)
weights = dict(tree_flatten(model.parameters()))
shards = make_shards(weights)
shards_count = len(shards)
shard_file_format = (
"model-{:05d}-of-{:05d}.safetensors"
if shards_count > 1
else "model.safetensors"
)
total_size = sum(v.nbytes for v in weights.values())
index_data = {
"metadata": {
"total_size": total_size,
"total_parameters": get_total_parameters(model),
},
"weight_map": {},
}
if donate_model:
model.update(tree_map(lambda _: mx.array([]), model.parameters()))
# Write the weights and make sure no references are kept other than the
# necessary ones
weights.clear()
del weights
for i in range(len(shards)):
shard = shards[i]
shards[i] = None
shard_name = shard_file_format.format(i + 1, shards_count)
shard_path = save_path / shard_name
mx.save_safetensors(str(shard_path), shard, metadata={"format": "mlx"})
for weight_name in shard.keys():
index_data["weight_map"][weight_name] = shard_name
del shard
index_data["weight_map"] = {
k: index_data["weight_map"][k] for k in sorted(index_data["weight_map"])
}
with open(save_path / "model.safetensors.index.json", "w") as f:
json.dump(
index_data,
f,
indent=4,
)
def quantize_model(
model: nn.Module,
config: dict,
group_size: Optional[int],
bits: Optional[int],
mode: str = "affine",
quant_predicate: Optional[Callable[[str, nn.Module], Union[bool, dict]]] = None,
) -> Tuple[nn.Module, dict]:
"""
Applies quantization to the model weights.
Args:
model (nn.Module): The model to be quantized.
config (dict): Model configuration.
group_size (Optional[int]): Group size for quantization.
bits (Optional[int]): Bits per weight for quantization.
mode (str): The quantization mode.
quant_predicate (Callable): A callable that decides how to quantize
each layer based on the path. Accepts the layer `path` and the
`module`. Returns either a bool to signify quantize/no quantize or
a dict of quantization parameters to pass to `to_quantized`.
Returns:
Tuple: Tuple containing quantized model and config.
"""
def defaults_for_mode(mode, group_size, bits):
mode_defaults = {
"affine": (64, 4),
"mxfp4": (32, 4),
"nvfp4": (16, 4),
"mxfp8": (32, 8),
}
default_group_size, default_bits = mode_defaults[mode]
return group_size or default_group_size, bits or default_bits
quantized_config = copy.deepcopy(config)
quant_predicate = quant_predicate or getattr(model, "quant_predicate", None)
group_size, bits = defaults_for_mode(mode, group_size, bits)
quant_params = {"group_size": group_size, "bits": bits, "mode": mode}
if "quantization" in quantized_config:
# If the model is already partially quantized, return params so that
# the config is set on a per-layer basis
fine_grained_config = True
else:
fine_grained_config = False
quantized_config["quantization"] = quant_params
def wrapped_predicate(path, module):
if not hasattr(module, "to_quantized"):
return False
if module.weight.shape[-1] % group_size != 0:
return False
bool_or_params = True
if quant_predicate is not None:
bool_or_params = quant_predicate(path, module)
if isinstance(bool_or_params, dict):
quantized_config["quantization"][path] = bool_or_params
elif fine_grained_config and bool_or_params:
quantized_config["quantization"][path] = quant_params
return bool_or_params
nn.quantize(
model,
group_size,
bits,
mode=mode,
class_predicate=wrapped_predicate,
)
# support hf model tree #957
quantized_config["quantization_config"] = quantized_config["quantization"]
bpw = compute_bits_per_weight(model)
print(f"[INFO] Quantized model with {bpw:.3f} bits per weight.")
return model, quantized_config
def dequantize_model(model: nn.Module) -> nn.Module:
"""
Dequantize the quantized layers in the model.
Args:
model (nn.Module): The model with quantized layers.
Returns:
nn.Module: The model with dequantized layers.
"""
from .models.switch_layers import QuantizedSwitchLinear, SwitchLinear
dequantize_layers = []
for name, module in model.named_modules():
bias = "bias" in module
if isinstance(module, nn.QuantizedLinear):
cls = nn.Linear
kwargs = {"bias": bias}
elif isinstance(module, nn.QuantizedEmbedding):
kwargs = {}
cls = nn.Embedding
elif isinstance(module, QuantizedSwitchLinear):
kwargs = {"bias": bias}
cls = SwitchLinear
else:
continue
weight = mx.dequantize(
module.weight,
module.scales,
module.biases,
module.group_size,
module.bits,
module.mode,
)
args = weight.shape[::-1]
m = cls(*args, **kwargs)
if bias:
m.bias = module.bias
m.weight = weight
dequantize_layers.append((name, m))
if len(dequantize_layers) > 0:
model.update_modules(tree_unflatten(dequantize_layers))
return model
def save_config(
config: dict,
config_path: Union[str, Path],
) -> None:
"""Save the model configuration to the ``config_path``.
The final configuration will be sorted before saving for better readability.
Args:
config (dict): The model configuration.
config_path (Union[str, Path]): Model configuration file path.
"""
# Clean unused keys
config.pop("_name_or_path", None)
config.pop("vision_config", None)
if "quantization" in config:
config["quantization_config"] = config["quantization"]
# sort the config for better readability
config = dict(sorted(config.items()))
# write the updated config to the config_path (if provided)
with open(config_path, "w") as fid:
json.dump(config, fid, indent=4)
def save(
dst_path: Union[str, Path],
src_path_or_repo: Union[str, Path],
model: nn.Module,
tokenizer: TokenizerWrapper,
config: Dict[str, Any],
donate_model: bool = True,
):
src_path = Path(src_path_or_repo)
if not src_path.exists():
hf_repo = src_path_or_repo
src_path = hf_repo_to_path(hf_repo)
else:
hf_repo = None
dst_path = Path(dst_path)
save_model(dst_path, model, donate_model=True)
save_config(config, config_path=dst_path / "config.json")
tokenizer.save_pretrained(dst_path)
for p in ["*.py", "generation_config.json"]:
for file in glob.glob(str(src_path / p)):
shutil.copy(file, dst_path)
create_model_card(dst_path, hf_repo)
def common_prefix_len(list1, list2):
"""
Calculates the length of the common prefix of two lists.
Args:
list1: The first list of strings.
list2: The second list of strings.
Returns:
The length of the common prefix. Returns 0 if lists are empty
or do not match at the first element.
"""
# Determine the maximum possible length of the common prefix
min_len = min(len(list1), len(list2))
# Iterate up to the length of the shorter list
for i in range(min_len):
if list1[i] != list2[i]:
# Mismatch found, the common prefix length is the current index
return i
# No mismatch found within the bounds of the shorter list,
# so the common prefix length is the length of the shorter list.
return min_len
def does_model_support_input_embeddings(model: nn.Module) -> bool:
"""
Check if the model supports input_embeddings in its call signature.
Args:
model (nn.Module): The model to check.
Returns:
bool: True if the model supports input_embeddings, False otherwise.
"""
try:
signature = inspect.signature(model.__call__)
return "input_embeddings" in signature.parameters
except (ValueError, TypeError):
return False