Packages

MLX bindings for Elixir via mlx-c. An Nx backend and Nx.Defn compiler for Apple's MLX machine learning framework on Apple Silicon.

Current section

Files

Jump to
elixir_mlx lib mlx models.ex
Raw

lib/mlx/models.ex

defmodule Mlx.Models do
@moduledoc """
High-level model loading pipeline for MLX.
Provides a complete pipeline from model files (local or HuggingFace Hub)
to `Axon.ModelState` structs, including weight unflattening, optional
dequantization, and key remapping.
## Example
# Load from a local directory
state = Mlx.Models.load_model_state("/path/to/model_dir")
# Load with dtype casting
state = Mlx.Models.load_model_state("/path/to/model_dir", dtype: {:f, 16})
# Load from HuggingFace Hub (requires :req dep)
state = Mlx.Models.load_model_state("bert-base-uncased")
"""
@doc """
Load a model configuration from a `config.json` file.
Accepts either a directory path (looks for `config.json` inside) or
a direct path to a JSON file.
Returns a map of the parsed JSON.
"""
def load_config(path) when is_binary(path) do
config_path =
if File.dir?(path) do
Path.join(path, "config.json")
else
path
end
unless File.exists?(config_path) do
raise ArgumentError, "config file not found: #{config_path}"
end
config_path |> File.read!() |> Jason.decode!()
end
@doc """
Unflatten a map with dot-separated keys into a nested map.
## Example
iex> Mlx.Models.unflatten_params(%{"a.b.c" => 1, "a.b.d" => 2, "x" => 3})
%{"a" => %{"b" => %{"c" => 1, "d" => 2}}, "x" => 3}
"""
def unflatten_params(flat_map) when is_map(flat_map) do
Enum.reduce(flat_map, %{}, fn {key, value}, acc ->
parts = String.split(key, ".")
deep_put(acc, parts, value)
end)
end
@doc """
Create an `Axon.ModelState` from a weight map.
## Options
* `:unflatten` - unflatten dot-separated keys into nested map (default: `true`)
* `:remap_fn` - function `(key :: String.t()) -> String.t()` to remap weight names
"""
def to_model_state(weights, opts \\ []) when is_map(weights) do
ensure_axon!()
unflatten = Keyword.get(opts, :unflatten, true)
remap_fn = Keyword.get(opts, :remap_fn)
weights =
if remap_fn do
Map.new(weights, fn {k, v} -> {remap_fn.(k), v} end)
else
weights
end
weights =
if unflatten do
unflatten_params(weights)
else
weights
end
Axon.ModelState.new(weights)
end
@doc """
Full model loading pipeline: resolve path → load weights → dequantize → remap → unflatten → ModelState.
Accepts a local directory path, a local file path, or a HuggingFace repo ID.
## Options
* `:revision` - HF revision (default: `"main"`)
* `:token` - HF API token
* `:cache_dir` - override Hub cache directory
* `:dtype` - Nx type to cast all tensors to
* `:remap_fn` - function to remap weight names
* `:unflatten` - unflatten dot-separated keys (default: `true`)
* `:dequantize` - dequantize quantized weights if `quantize_config.json` exists (default: `true`)
"""
def load_model_state(path_or_repo_id, opts \\ []) do
ensure_axon!()
dtype = Keyword.get(opts, :dtype)
remap_fn = Keyword.get(opts, :remap_fn)
unflatten = Keyword.get(opts, :unflatten, true)
do_dequantize = Keyword.get(opts, :dequantize, true)
model_dir = resolve_model_dir(path_or_repo_id, opts)
# Load weights
weights = Mlx.IO.load_weights(model_dir, dtype: dtype)
# Dequantize if quantize_config.json exists
weights =
if do_dequantize do
quantize_config_path = Path.join(model_dir, "quantize_config.json")
if File.exists?(quantize_config_path) do
dequantize_weights(weights, quantize_config_path)
else
weights
end
else
weights
end
# Remap keys
weights =
if remap_fn do
Map.new(weights, fn {k, v} -> {remap_fn.(k), v} end)
else
weights
end
# Unflatten
weights =
if unflatten do
unflatten_params(weights)
else
weights
end
Axon.ModelState.new(weights)
end
# Private helpers
defp resolve_model_dir(path_or_repo_id, opts) do
cond do
File.dir?(path_or_repo_id) ->
path_or_repo_id
File.exists?(path_or_repo_id) ->
# Single file — return its parent directory
Path.dirname(path_or_repo_id)
true ->
# Assume it's a HuggingFace repo ID — download via Hub
hub_opts =
opts
|> Keyword.take([:revision, :token, :cache_dir, :force_download])
|> Keyword.put(:allow_patterns, ["*.safetensors", "*.json", "*.safetensors.index.json"])
Mlx.Hub.snapshot_download(path_or_repo_id, hub_opts)
end
end
defp dequantize_weights(weights, config_path) do
config = config_path |> File.read!() |> Jason.decode!()
group_size = Map.get(config, "group_size", 64)
bits = Map.get(config, "bits", 4)
# Find weight/scales/biases triplets:
# For a quantized layer "foo.weight", there will be "foo.scales" and "foo.biases"
weight_keys =
weights
|> Map.keys()
|> Enum.filter(&String.ends_with?(&1, ".weight"))
Enum.reduce(weight_keys, weights, fn weight_key, acc ->
base = String.replace_suffix(weight_key, ".weight", "")
scales_key = "#{base}.scales"
biases_key = "#{base}.biases"
case {Map.fetch(acc, scales_key), Map.fetch(acc, biases_key)} do
{{:ok, scales}, {:ok, biases}} ->
dequantized =
Mlx.Quantize.dequantize(acc[weight_key], scales, biases,
group_size: group_size,
bits: bits
)
acc
|> Map.put(weight_key, dequantized)
|> Map.delete(scales_key)
|> Map.delete(biases_key)
_ ->
acc
end
end)
end
defp deep_put(map, [key], value) do
Map.put(map, key, value)
end
defp deep_put(map, [key | rest], value) do
existing = Map.get(map, key, %{})
Map.put(map, key, deep_put(existing, rest, value))
end
defp ensure_axon! do
unless Code.ensure_loaded?(Axon.ModelState) do
raise """
Axon is required for model state operations.
Add {:axon, "~> 0.8"} to your dependencies in mix.exs.
"""
end
end
end