Current section
Files
Jump to
Current section
Files
src/mlx_autograd.erl
-module(mlx_autograd).
%% Comprehensive automatic differentiation system for MLX
-export([
%% Core autograd functions
enable_grad/0, disable_grad/0, no_grad/1,
set_grad/2, get_grad/1, zero_grad/1,
backward/1, backward/2,
%% Gradient computation
grad/2, grad/3, grad/4,
value_and_grad/2, value_and_grad/3,
jacobian/2, jacobian/3,
hessian/2, hessian/3,
%% Function transformation
vjp/2, vjp/3,
jvp/3, jvp/4,
%% Checkpointing for memory efficiency
checkpoint/1, checkpoint/2,
%% Advanced gradient operations
stop_gradient/1,
detach/1,
custom_vjp/3,
%% Optimization helpers
clip_grad_norm/2, clip_grad_value/2,
accumulate_gradients/2,
%% Higher-order derivatives
grad_grad/3, laplacian/2
]).
%% Internal state for gradient tracking
-record(autograd_state, {
enabled = true,
graph = #{},
grad_cache = #{}
}).
%% Core autograd functions
enable_grad() ->
put(autograd_enabled, true),
ok.
disable_grad() ->
put(autograd_enabled, false),
ok.
no_grad(Fun) ->
OldState = get(autograd_enabled),
disable_grad(),
try
Fun()
after
case OldState of
true -> enable_grad();
_ -> disable_grad()
end
end.
set_grad(Array, Gradient) ->
case is_grad_enabled() of
true ->
GradDict = get_grad_dict(),
ArrayId = get_array_id(Array),
put(grad_dict, maps:put(ArrayId, Gradient, GradDict)),
ok;
false ->
{error, gradients_disabled}
end.
get_grad(Array) ->
GradDict = get_grad_dict(),
ArrayId = get_array_id(Array),
maps:get(ArrayId, GradDict, undefined).
zero_grad(Arrays) when is_list(Arrays) ->
lists:foreach(fun(Array) ->
ArrayId = get_array_id(Array),
GradDict = get_grad_dict(),
put(grad_dict, maps:remove(ArrayId, GradDict))
end, Arrays),
ok;
zero_grad(Array) ->
zero_grad([Array]).
backward(Loss) ->
backward(Loss, []).
backward(Loss, Options) ->
RetainGraph = proplists:get_value(retain_graph, Options, false),
CreateGraph = proplists:get_value(create_graph, Options, false),
case is_grad_enabled() of
true ->
% Start backward pass from loss
{ok, OnesLike} = mlx:ones(mlx:shape(Loss)),
backward_internal(Loss, OnesLike, RetainGraph, CreateGraph);
false ->
{error, gradients_disabled}
end.
%% Gradient computation functions
grad(Fun, Args) ->
grad(Fun, Args, []).
grad(Fun, Args, Argnums) ->
grad(Fun, Args, Argnums, []).
grad(Fun, Args, Argnums, Options) ->
% Compute gradient of Fun with respect to Args[Argnums]
HasAux = proplists:get_value(has_aux, Options, false),
% Enable gradient tracking
enable_grad(),
% Mark input arrays as requiring gradients
ArgsWithGrad = mark_arrays_for_grad(Args, Argnums),
% Forward pass
case HasAux of
true ->
{Output, Aux} = Fun(ArgsWithGrad),
% Backward pass
{ok, Grads} = compute_gradients(Output, ArgsWithGrad, Argnums),
{Grads, Aux};
false ->
Output = Fun(ArgsWithGrad),
% Backward pass
compute_gradients(Output, ArgsWithGrad, Argnums)
end.
value_and_grad(Fun, Args) ->
value_and_grad(Fun, Args, []).
value_and_grad(Fun, Args, Argnums) ->
% Compute both value and gradient
enable_grad(),
ArgsWithGrad = mark_arrays_for_grad(Args, Argnums),
Output = Fun(ArgsWithGrad),
{ok, Grads} = compute_gradients(Output, ArgsWithGrad, Argnums),
{Output, Grads}.
jacobian(Fun, Args) ->
jacobian(Fun, Args, []).
jacobian(Fun, Args, Argnums) ->
% Compute Jacobian matrix
enable_grad(),
ArgsWithGrad = mark_arrays_for_grad(Args, Argnums),
% Get output to determine dimensions
Output = Fun(ArgsWithGrad),
{ok, OutputShape} = mlx:shape(Output),
OutputSize = lists:foldl(fun(Dim, Acc) -> Dim * Acc end, 1, OutputShape),
% Compute Jacobian row by row
JacobianRows = lists:map(fun(I) ->
% Create unit vector for this output component
{ok, UnitVec} = create_unit_vector(OutputSize, I),
{ok, UnitVecReshaped} = mlx:reshape(UnitVec, OutputShape),
% Compute gradient for this output component
{ok, Grads} = compute_gradients_with_output_grad(Output, ArgsWithGrad,
Argnums, UnitVecReshaped),
Grads
end, lists:seq(0, OutputSize - 1)),
{ok, mlx:stack(JacobianRows, 0)}.
hessian(Fun, Args) ->
hessian(Fun, Args, []).
hessian(Fun, Args, Argnums) ->
% Compute Hessian matrix using grad(grad(...))
GradFun = fun(X) ->
{_Value, Grad} = value_and_grad(Fun, X, Argnums),
Grad
end,
jacobian(GradFun, Args, Argnums).
%% Vector-Jacobian and Jacobian-vector products
vjp(Fun, Args) ->
vjp(Fun, Args, []).
vjp(Fun, Args, Argnums) ->
% Vector-Jacobian product
enable_grad(),
ArgsWithGrad = mark_arrays_for_grad(Args, Argnums),
Output = Fun(ArgsWithGrad),
VjpFun = fun(Cotangent) ->
compute_gradients_with_output_grad(Output, ArgsWithGrad, Argnums, Cotangent)
end,
{Output, VjpFun}.
jvp(Fun, Args, Tangents) ->
jvp(Fun, Args, Tangents, []).
jvp(Fun, Args, Tangents, Argnums) ->
% Jacobian-vector product using forward-mode AD
enable_grad(),
% Create dual numbers for forward-mode AD
DualArgs = create_dual_numbers(Args, Tangents, Argnums),
DualOutput = Fun(DualArgs),
% Extract primal and tangent parts
{Primal, TangentOut} = extract_dual_parts(DualOutput),
{Primal, TangentOut}.
%% Checkpointing for memory efficiency
checkpoint(Fun) ->
checkpoint(Fun, []).
checkpoint(Fun, Args) ->
% Gradient checkpointing to save memory
% Store only inputs, recompute during backward pass
CheckpointId = make_ref(),
put({checkpoint, CheckpointId}, {Fun, Args}),
% Forward pass without storing intermediate values
Output = no_grad(fun() -> Fun(Args) end),
% Create custom backward function
set_backward_hook(Output, fun(Grad) ->
% Retrieve function and args
{StoredFun, StoredArgs} = get({checkpoint, CheckpointId}),
% Recompute forward pass with gradients
enable_grad(),
RecomputedOutput = StoredFun(StoredArgs),
% Continue backward pass
backward(RecomputedOutput, [{output_grad, Grad}])
end),
Output.
%% Advanced gradient operations
stop_gradient(Array) ->
% Stop gradient flow through this array
mlx_nif:stop_gradient(Array).
detach(Array) ->
% Detach array from computation graph
mlx_nif:detach(Array).
custom_vjp(Fun, FwdFun, BwdFun) ->
% Custom vector-Jacobian product
fun(Args) ->
% Forward pass using custom forward function
{Output, Aux} = FwdFun(Args),
% Set custom backward function
set_backward_hook(Output, fun(Grad) ->
BwdFun(Aux, Grad)
end),
Output
end.
%% Optimization helpers
clip_grad_norm(Gradients, MaxNorm) ->
% Clip gradients by global norm
TotalNorm = compute_total_norm(Gradients),
ClipCoeff = erlang:min(1.0, MaxNorm / TotalNorm),
lists:map(fun(Grad) ->
{ok, Clipped} = mlx:multiply(Grad, mlx:array(ClipCoeff)),
Clipped
end, Gradients).
clip_grad_value(Gradients, ClipValue) ->
% Clip gradients by value
lists:map(fun(Grad) ->
{ok, Clipped} = mlx:clip(Grad, mlx:array(-ClipValue), mlx:array(ClipValue)),
Clipped
end, Gradients).
accumulate_gradients(GradList1, GradList2) ->
% Accumulate gradients from multiple sources
lists:zipwith(fun(G1, G2) ->
case {G1, G2} of
{undefined, G} -> G;
{G, undefined} -> G;
{G1, G2} ->
{ok, Sum} = mlx:add(G1, G2),
Sum
end
end, GradList1, GradList2).
%% Higher-order derivatives
grad_grad(Fun, Args, Argnums) ->
% Second-order gradients
GradFun = fun(X) ->
{_Value, Grad} = value_and_grad(Fun, X, Argnums),
% Return sum of gradient for scalar output
mlx:sum(Grad)
end,
grad(GradFun, Args, Argnums).
laplacian(Fun, Args) ->
% Compute Laplacian (trace of Hessian)
{ok, H} = hessian(Fun, Args),
mlx_nif:trace(H).
%% Internal helper functions
is_grad_enabled() ->
get(autograd_enabled) =:= true.
get_grad_dict() ->
case get(grad_dict) of
undefined -> #{};
Dict -> Dict
end.
get_array_id(Array) ->
% Generate unique ID for array (simplified)
erlang:phash2(Array).
mark_arrays_for_grad(Args, []) ->
% Mark all arrays
lists:map(fun mark_for_grad/1, Args);
mark_arrays_for_grad(Args, Argnums) ->
% Mark specific indices
lists:map(fun({I, Arg}) ->
case lists:member(I, Argnums) of
true -> mark_for_grad(Arg);
false -> Arg
end
end, lists:zip(lists:seq(0, length(Args) - 1), Args)).
mark_for_grad(Array) ->
% Mark array as requiring gradients
mlx_nif:set_requires_grad(Array, true).
compute_gradients(Output, Args, Argnums) ->
% Compute gradients with respect to specified arguments
backward(Output),
Grads = lists:map(fun(I) ->
Arg = lists:nth(I + 1, Args),
get_grad(Arg)
end, Argnums),
{ok, Grads}.
compute_gradients_with_output_grad(Output, Args, Argnums, OutputGrad) ->
% Compute gradients with custom output gradient
backward(Output, [{output_grad, OutputGrad}]),
Grads = lists:map(fun(I) ->
Arg = lists:nth(I + 1, Args),
get_grad(Arg)
end, Argnums),
{ok, Grads}.
backward_internal(Array, Grad, RetainGraph, CreateGraph) ->
% Internal backward pass implementation
% This would interface with the MLX C++ autograd system
mlx_nif:backward(Array, Grad, #{
retain_graph => RetainGraph,
create_graph => CreateGraph
}).
create_unit_vector(Size, Index) ->
% Create unit vector with 1 at Index, 0 elsewhere
{ok, Zeros} = mlx:zeros([Size]),
{ok, One} = mlx:array(1.0),
mlx_nif:set_item(Zeros, Index, One).
set_backward_hook(Array, HookFun) ->
% Set custom backward hook for array
mlx_nif:register_hook(Array, HookFun).
create_dual_numbers(Args, Tangents, Argnums) ->
% Create dual numbers for forward-mode AD
lists:zipwith3(fun(I, Arg, Tangent) ->
case lists:member(I, Argnums) of
true -> {dual, Arg, Tangent};
false -> Arg
end
end, lists:seq(0, length(Args) - 1), Args, Tangents).
extract_dual_parts(DualOutput) ->
% Extract primal and tangent from dual number
case DualOutput of
{dual, Primal, Tangent} -> {Primal, Tangent};
Primal -> {Primal, undefined}
end.
compute_total_norm(Gradients) ->
% Compute total L2 norm of gradients
TotalSquared = lists:foldl(fun(Grad, Acc) ->
{ok, Square} = mlx:square(Grad),
{ok, Sum} = mlx:sum(Square),
case Acc of
0.0 -> Sum;
_ ->
{ok, NewAcc} = mlx:add(mlx:array(Acc), Sum),
mlx:to_list(NewAcc)
end
end, 0.0, Gradients),
math:sqrt(TotalSquared).