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
Current section
Files
lib/mlx/io.ex
defmodule Mlx.IO do
@moduledoc """
Array I/O operations for MLX.
Supports saving/loading individual arrays (.npy format) and
SafeTensors format for collections of named tensors.
## Example
a = Nx.tensor([1.0, 2.0, 3.0], backend: Mlx.Backend)
Mlx.IO.save("/tmp/test.npy", a)
loaded = Mlx.IO.load("/tmp/test.npy")
"""
alias Mlx.NIF
defp s, do: elem(NIF.default_cpu_stream(), 1)
defp unwrap!({:ok, val}), do: val
defp unwrap!({:error, msg}), do: raise("MLX error: #{msg}")
defp from_ref(%Nx.Tensor{data: %Mlx.Backend{ref: ref}}), do: ref
@doc """
Save a single tensor to a .npy file.
"""
def save(path, %Nx.Tensor{} = tensor) when is_binary(path) do
ref = from_ref(tensor)
NIF.eval(ref)
case NIF.mlx_save(to_charlist(path), ref) do
:ok -> :ok
{:error, msg} -> raise "MLX error: #{msg}"
end
end
@doc """
Load a single tensor from a .npy file.
"""
def load(path) when is_binary(path) do
ref = unwrap!(NIF.mlx_load(to_charlist(path), s()))
to_nx_infer(ref)
end
@doc """
Save a map of named tensors in SafeTensors format.
## Arguments
* `path` - file path (should end in `.safetensors`)
* `tensors` - map of `%{String.t() => Nx.Tensor.t()}`
## Options
* `:metadata` - optional map of string metadata (default: %{})
"""
def save_safetensors(path, tensors, opts \\ []) when is_binary(path) and is_map(tensors) do
metadata = Keyword.get(opts, :metadata, %{})
{keys, arrays} =
tensors
|> Enum.map(fn {k, %Nx.Tensor{} = t} ->
ref = from_ref(t)
NIF.eval(ref)
{to_charlist(k), ref}
end)
|> Enum.unzip()
{meta_keys, meta_vals} =
metadata
|> Enum.map(fn {k, v} -> {to_charlist(k), to_charlist(v)} end)
|> Enum.unzip()
meta_keys = if meta_keys == [], do: [], else: meta_keys
meta_vals = if meta_vals == [], do: [], else: meta_vals
case NIF.mlx_save_safetensors(to_charlist(path), keys, arrays, meta_keys, meta_vals) do
:ok -> :ok
{:error, msg} -> raise "MLX error: #{msg}"
end
end
@doc """
Load tensors from a SafeTensors file.
Returns `{tensors_map, metadata_map}` where:
* `tensors_map` - `%{String.t() => Nx.Tensor.t()}`
* `metadata_map` - `%{String.t() => String.t()}`
"""
def load_safetensors(path) when is_binary(path) do
{arr_pairs, meta_pairs} = unwrap!(NIF.mlx_load_safetensors(to_charlist(path), s()))
tensors =
arr_pairs
|> Enum.map(fn {key, ref} ->
{to_string(key), to_nx_infer(ref)}
end)
|> Map.new()
metadata =
meta_pairs
|> Enum.map(fn {k, v} -> {to_string(k), to_string(v)} end)
|> Map.new()
{tensors, metadata}
end
@doc """
Load weights from a file or directory, returning a flat map of `%{String.t() => Nx.Tensor.t()}`.
Supports `.npy` files, `.safetensors` files, and directories containing
`model.safetensors` or sharded safetensors with an index file.
## Options
* `:format` - force format (`:npy` or `:safetensors`), auto-detected by default
* `:dtype` - Nx type to cast all tensors to (e.g. `{:f, 16}`)
## Examples
weights = Mlx.IO.load_weights("/path/to/model.safetensors")
weights = Mlx.IO.load_weights("/path/to/model_dir")
weights = Mlx.IO.load_weights("/path/to/array.npy")
"""
def load_weights(path, opts \\ []) when is_binary(path) do
format = Keyword.get(opts, :format)
dtype = Keyword.get(opts, :dtype)
tensors =
cond do
File.dir?(path) ->
load_weights_from_dir(path)
format == :npy || (!format && String.ends_with?(path, ".npy")) ->
%{"weights" => load(path)}
format == :safetensors || (!format && String.ends_with?(path, ".safetensors")) ->
{tensors_map, _metadata} = load_safetensors(path)
tensors_map
true ->
raise ArgumentError,
"cannot detect format for #{path}; use :format option or a recognized extension (.npy, .safetensors)"
end
maybe_cast_dtype(tensors, dtype)
end
defp load_weights_from_dir(dir) do
index_path = Path.join(dir, "model.safetensors.index.json")
single_path = Path.join(dir, "model.safetensors")
cond do
File.exists?(index_path) ->
load_sharded_safetensors(dir, index_path)
File.exists?(single_path) ->
{tensors_map, _metadata} = load_safetensors(single_path)
tensors_map
true ->
raise ArgumentError,
"directory #{dir} does not contain model.safetensors or model.safetensors.index.json"
end
end
defp load_sharded_safetensors(dir, index_path) do
index = index_path |> File.read!() |> Jason.decode!()
weight_map = Map.get(index, "weight_map", %{})
# Group weights by shard file — load each shard file only once
shards_to_keys =
Enum.group_by(weight_map, fn {_key, shard_file} -> shard_file end, fn {key, _shard} ->
key
end)
Enum.reduce(shards_to_keys, %{}, fn {shard_file, keys}, acc ->
shard_path = Path.join(dir, shard_file)
{shard_tensors, _metadata} = load_safetensors(shard_path)
Enum.reduce(keys, acc, fn key, inner_acc ->
case Map.fetch(shard_tensors, key) do
{:ok, tensor} -> Map.put(inner_acc, key, tensor)
:error -> inner_acc
end
end)
end)
end
defp maybe_cast_dtype(tensors, nil), do: tensors
defp maybe_cast_dtype(tensors, dtype) do
Map.new(tensors, fn {key, tensor} -> {key, Nx.as_type(tensor, dtype)} end)
end
# Helpers
defp to_nx_infer(ref) do
{:ok, shape_list} = NIF.shape(ref)
{:ok, dtype_atom} = NIF.dtype(ref)
type =
case Mlx.Dtype.to_nx(dtype_atom) do
nil -> {:f, 32}
t -> t
end
shape = List.to_tuple(shape_list)
%Nx.Tensor{
data: %Mlx.Backend{ref: ref},
type: type,
shape: shape,
names: List.duplicate(nil, tuple_size(shape))
}
end
end