Packages

Tensor library for Gleam/BEAM with a pure Gleam API, zero-copy views, and optional native acceleration

Retired package: Release invalid

Current section

Files

Jump to
viva_tensor src viva_tensor_distributed.erl
Raw

src/viva_tensor_distributed.erl

%% viva_tensor_distributed.erl - Distributed tensor operations
%%
%% Provides BEAM-native distributed computing for tensor operations.
%% Uses Erlang's built-in distribution for zero-copy message passing.
%%
%% Key concepts:
%% - Row sharding: Split matrix A by rows across nodes
%% - Broadcast B: Send matrix B to all nodes
%% - Parallel execution: Each node computes its shard
%% - Gather results: Collect and combine partial results
-module(viva_tensor_distributed).
-export([
spawn_matmul_task/6,
await_task/1,
connect_nodes/1,
list_nodes/0,
shard_tensor/3,
gather_results/1,
parallel_matmul/5
]).
%% ==========================================================================
%% Task Spawning
%% ==========================================================================
%% Spawn a matrix multiplication task on a remote node
%% Returns a reference that can be awaited
spawn_matmul_task({node, NodeName}, A, B, M, N, K) ->
Caller = self(),
Ref = make_ref(),
%% Spawn on remote node (or local if same node)
Node = list_to_atom(binary_to_list(NodeName)),
spawn(Node, fun() ->
Result = compute_matmul(A, B, M, N, K),
Caller ! {matmul_result, Ref, Result}
end),
{task_ref, Ref}.
%% Await a task result with timeout
await_task({task_ref, Ref}) ->
receive
{matmul_result, Ref, Result} ->
{ok, Result}
after 30000 ->
{error, <<"timeout">>}
end.
%% ==========================================================================
%% Node Management
%% ==========================================================================
%% Connect to a list of nodes
connect_nodes(NodeNames) ->
Results = [net_adm:ping(list_to_atom(binary_to_list(N))) || N <- NodeNames],
{ok, Results}.
%% List all connected nodes
list_nodes() ->
Nodes = [node() | nodes()],
[{node, atom_to_binary(N, utf8)} || N <- Nodes].
%% ==========================================================================
%% Tensor Sharding
%% ==========================================================================
%% Shard a tensor (list of floats) into N parts by rows
%% Returns list of {Data, RowCount} tuples
shard_tensor(Data, RowSize, NumShards) ->
TotalRows = length(Data) div RowSize,
RowsPerShard = TotalRows div NumShards,
Remainder = TotalRows rem NumShards,
shard_tensor_acc(Data, RowSize, RowsPerShard, Remainder, NumShards, 0, []).
shard_tensor_acc(_Data, _RowSize, _RowsPerShard, _Remainder, NumShards, Current, Acc)
when Current >= NumShards ->
lists:reverse(Acc);
shard_tensor_acc(Data, RowSize, RowsPerShard, Remainder, NumShards, Current, Acc) ->
%% First 'Remainder' shards get an extra row
Extra = case Current < Remainder of true -> 1; false -> 0 end,
Rows = RowsPerShard + Extra,
Elements = Rows * RowSize,
{ShardData, Rest} = lists:split(min(Elements, length(Data)), Data),
Shard = {ShardData, Rows},
shard_tensor_acc(Rest, RowSize, RowsPerShard, Remainder, NumShards, Current + 1, [Shard | Acc]).
%% ==========================================================================
%% Gather Results
%% ==========================================================================
%% Gather results from multiple task refs
gather_results(TaskRefs) ->
gather_results_acc(TaskRefs, []).
gather_results_acc([], Acc) ->
{ok, lists:append(lists:reverse(Acc))};
gather_results_acc([TaskRef | Rest], Acc) ->
case await_task(TaskRef) of
{ok, Result} ->
gather_results_acc(Rest, [Result | Acc]);
{error, Reason} ->
{error, Reason}
end.
%% ==========================================================================
%% Parallel Matrix Multiplication
%% ==========================================================================
%% High-level API: Parallel matmul across connected nodes
%% A[M,K] @ B[K,N] -> C[M,N]
parallel_matmul(A, B, M, N, K) ->
Nodes = list_nodes(),
NumNodes = length(Nodes),
case NumNodes of
0 ->
{error, <<"no_nodes">>};
1 ->
%% Single node: just compute locally
{ok, compute_matmul(A, B, M, N, K)};
_ ->
%% Multiple nodes: shard and distribute
Shards = shard_tensor(A, K, NumNodes),
%% Spawn tasks on each node
TaskRefs = lists:zipwith(
fun(Node, {ShardData, ShardRows}) ->
spawn_matmul_task(Node, ShardData, B, ShardRows, N, K)
end,
Nodes, Shards
),
%% Gather and combine results
gather_results(TaskRefs)
end.
%% ==========================================================================
%% Local Computation (called on each node)
%% ==========================================================================
%% Compute matmul locally using best available backend
compute_matmul(A, B, M, N, K) ->
%% Try Zig SIMD first, then Accelerate, then pure Erlang
case viva_tensor_zig:is_loaded() of
true ->
case viva_tensor_zig:simd_matmul(A, B, M, N, K) of
{ok, Result} -> Result;
_ -> fallback_matmul(A, B, M, N, K)
end;
false ->
case viva_tensor_nif:is_nif_loaded() of
true ->
case viva_tensor_nif:matmul(A, B, M, N, K) of
{ok, Result} -> Result;
_ -> fallback_matmul(A, B, M, N, K)
end;
false ->
fallback_matmul(A, B, M, N, K)
end
end.
%% Pure Erlang fallback
fallback_matmul(AList, BList, M, N, K) ->
A = array:from_list(AList),
B = array:from_list(BList),
Result = viva_tensor_ffi:array_matmul(A, B, M, N, K),
array:to_list(Result).