-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).