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/fft.ex
defmodule Mlx.FFT do
@moduledoc """
Fast Fourier Transform operations using MLX.
Provides 1D, 2D, and N-dimensional FFTs (complex and real) accelerated
on Apple Silicon via Metal.
## Example
a = Nx.tensor([1.0, 0.0, 0.0, 0.0], backend: Mlx.Backend)
freq = Mlx.FFT.fft(a)
"""
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
defp resolve_axis(ax, ndim) when ax < 0, do: ndim + ax
defp resolve_axis(ax, _ndim), do: ax
@doc """
1D discrete Fourier transform.
## Options
* `:n` - transform length (default: axis size)
* `:axis` - axis to transform (default: -1)
"""
def fft(%Nx.Tensor{} = a, opts \\ []) do
axis = Keyword.get(opts, :axis, -1)
resolved_axis = resolve_axis(axis, tuple_size(a.shape))
n = Keyword.get(opts, :n, elem(a.shape, resolved_axis))
ref = unwrap!(NIF.mlx_fft(from_ref(a), n, axis, s()))
to_nx_infer(ref)
end
@doc """
1D inverse discrete Fourier transform.
"""
def ifft(%Nx.Tensor{} = a, opts \\ []) do
axis = Keyword.get(opts, :axis, -1)
resolved_axis = resolve_axis(axis, tuple_size(a.shape))
n = Keyword.get(opts, :n, elem(a.shape, resolved_axis))
ref = unwrap!(NIF.mlx_ifft(from_ref(a), n, axis, s()))
to_nx_infer(ref)
end
@doc """
1D real-input FFT.
Input must be real-valued. Output has shape `n/2 + 1` along the transform axis.
"""
def rfft(%Nx.Tensor{} = a, opts \\ []) do
axis = Keyword.get(opts, :axis, -1)
resolved_axis = resolve_axis(axis, tuple_size(a.shape))
n = Keyword.get(opts, :n, elem(a.shape, resolved_axis))
ref = unwrap!(NIF.mlx_rfft(from_ref(a), n, axis, s()))
to_nx_infer(ref)
end
@doc """
1D inverse real FFT.
For input of length `m` along the transform axis, output length is `2*(m-1)`.
Use `:n` to specify the exact output length.
"""
def irfft(%Nx.Tensor{} = a, opts \\ []) do
axis = Keyword.get(opts, :axis, -1)
resolved_axis = resolve_axis(axis, tuple_size(a.shape))
# irfft: input has n/2+1 elements, output has n elements
# default n = 2 * (input_size - 1)
n = Keyword.get(opts, :n, 2 * (elem(a.shape, resolved_axis) - 1))
ref = unwrap!(NIF.mlx_irfft(from_ref(a), n, axis, s()))
to_nx_infer(ref)
end
@doc """
2D discrete Fourier transform.
## Options
* `:n` - list of transform lengths (default: axis sizes)
* `:axes` - list of axes to transform (default: last 2)
"""
def fft2(%Nx.Tensor{} = a, opts \\ []) do
{n_list, axes_list} = multi_opts(a, 2, opts)
ref = unwrap!(NIF.mlx_fft2(from_ref(a), n_list, axes_list, s()))
to_nx_infer(ref)
end
@doc """
2D inverse discrete Fourier transform.
"""
def ifft2(%Nx.Tensor{} = a, opts \\ []) do
{n_list, axes_list} = multi_opts(a, 2, opts)
ref = unwrap!(NIF.mlx_ifft2(from_ref(a), n_list, axes_list, s()))
to_nx_infer(ref)
end
@doc """
N-dimensional discrete Fourier transform.
"""
def fftn(%Nx.Tensor{} = a, opts \\ []) do
ndim = tuple_size(a.shape)
{n_list, axes_list} = multi_opts(a, ndim, opts)
ref = unwrap!(NIF.mlx_fftn(from_ref(a), n_list, axes_list, s()))
to_nx_infer(ref)
end
@doc """
N-dimensional inverse discrete Fourier transform.
"""
def ifftn(%Nx.Tensor{} = a, opts \\ []) do
ndim = tuple_size(a.shape)
{n_list, axes_list} = multi_opts(a, ndim, opts)
ref = unwrap!(NIF.mlx_ifftn(from_ref(a), n_list, axes_list, s()))
to_nx_infer(ref)
end
@doc """
2D real-input FFT.
"""
def rfft2(%Nx.Tensor{} = a, opts \\ []) do
{n_list, axes_list} = multi_opts(a, 2, opts)
ref = unwrap!(NIF.mlx_rfft2(from_ref(a), n_list, axes_list, s()))
to_nx_infer(ref)
end
@doc """
2D inverse real FFT.
Use `:n` to specify the output shape along the transform axes.
The last axis defaults to `2*(input_size - 1)`.
"""
def irfft2(%Nx.Tensor{} = a, opts \\ []) do
{n_list, axes_list} = multi_opts_irfft(a, 2, opts)
ref = unwrap!(NIF.mlx_irfft2(from_ref(a), n_list, axes_list, s()))
to_nx_infer(ref)
end
@doc """
N-dimensional real-input FFT.
"""
def rfftn(%Nx.Tensor{} = a, opts \\ []) do
ndim = tuple_size(a.shape)
{n_list, axes_list} = multi_opts(a, ndim, opts)
ref = unwrap!(NIF.mlx_rfftn(from_ref(a), n_list, axes_list, s()))
to_nx_infer(ref)
end
@doc """
N-dimensional inverse real FFT.
Use `:n` to specify the output shape along the transform axes.
The last axis defaults to `2*(input_size - 1)`.
"""
def irfftn(%Nx.Tensor{} = a, opts \\ []) do
ndim = tuple_size(a.shape)
{n_list, axes_list} = multi_opts_irfft(a, ndim, opts)
ref = unwrap!(NIF.mlx_irfftn(from_ref(a), n_list, axes_list, s()))
to_nx_infer(ref)
end
# Helpers
defp multi_opts(tensor, default_ndim, opts) do
axes = Keyword.get(opts, :axes, nil)
axes_list = axes || Enum.to_list(-min(default_ndim, tuple_size(tensor.shape))..-1)
n = Keyword.get(opts, :n, nil)
n_list =
n ||
Enum.map(axes_list, fn ax ->
resolved = resolve_axis(ax, tuple_size(tensor.shape))
elem(tensor.shape, resolved)
end)
{n_list, axes_list}
end
# For inverse real FFTs: last axis size defaults to 2*(input_size - 1)
defp multi_opts_irfft(tensor, default_ndim, opts) do
axes = Keyword.get(opts, :axes, nil)
axes_list = axes || Enum.to_list(-min(default_ndim, tuple_size(tensor.shape))..-1)
n = Keyword.get(opts, :n, nil)
n_list =
n ||
axes_list
|> Enum.with_index()
|> Enum.map(fn {ax, idx} ->
resolved = resolve_axis(ax, tuple_size(tensor.shape))
size = elem(tensor.shape, resolved)
# Last axis in irfft: output = 2*(input-1)
if idx == length(axes_list) - 1 do
2 * (size - 1)
else
size
end
end)
{n_list, axes_list}
end
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