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@sparsity.erl
Raw

src/viva_tensor@sparsity.erl

-module(viva_tensor@sparsity).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/viva_tensor/sparsity.gleam").
-export([decompress/1, compute_metrics/2, prune_24_magnitude/1, prune_24_gradient/2, sparse_matmul/2, benchmark_sparsity/0, main/0]).
-export_type([sparse24_block/0, sparse24_tensor/0, prune_metrics/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" 2:4 Structured Sparsity\n"
"\n"
" NVIDIA Tensor Cores Structured Sparsity\n"
" Ampere+ Architecture (RTX 3000/4000, A100, H100)\n"
"\n"
" CONCEITO:\n"
" Em cada grupo de 4 elementos, apenas 2 são não-zero\n"
" = 50% dos elementos são zero, mas em padrão ESTRUTURADO\n"
"\n"
" POR QUE ESTRUTURADO > ALEATÓRIO:\n"
" - Sparsity aleatória: difícil de acelerar em hardware\n"
" - Sparsity estruturada: hardware pode pular zeros eficientemente\n"
"\n"
" FORMATO DE ARMAZENAMENTO:\n"
" - 2 valores FP16 (32 bits)\n"
" - 2-bit máscara indicando posições (4 bits para 4 posições)\n"
" - Total: 36 bits para 4 elementos = 9 bits/elemento vs 16 bits/elemento\n"
" - Compressão: ~1.8x\n"
"\n"
" PERFORMANCE:\n"
" - 2x throughput em Tensor Cores (pula multiplicações por zero)\n"
" - Combinado com INT8: 4x speedup total!\n"
).
-type sparse24_block() :: {sparse24_block,
{float(), float()},
{integer(), integer()}}.
-type sparse24_tensor() :: {sparse24_tensor,
list(sparse24_block()),
list(integer()),
integer(),
integer(),
float()}.
-type prune_metrics() :: {prune_metrics,
integer(),
integer(),
float(),
float(),
float()}.
-file("src/viva_tensor/sparsity.gleam", 112).
?DOC(" Poda um grupo de 4 elementos, retornando Sparse24Block\n").
-spec prune_group_magnitude(list(float())) -> sparse24_block().
prune_group_magnitude(Group) ->
Indexed = gleam@list:index_map(
Group,
fun(Val, Idx) -> {Idx, Val, gleam@float:absolute_value(Val)} end
),
Sorted = gleam@list:sort(
Indexed,
fun(A, B) ->
gleam@float:compare(erlang:element(3, B), erlang:element(3, A))
end
),
case Sorted of
[First, Second | _] ->
{Pos1, Val1, _} = First,
{Pos2, Val2, _} = Second,
{P1, V1, P2, V2} = case Pos1 < Pos2 of
true ->
{Pos1, Val1, Pos2, Val2};
false ->
{Pos2, Val2, Pos1, Val1}
end,
{sparse24_block, {V1, V2}, {P1, P2}};
_ ->
{sparse24_block, {+0.0, +0.0}, {0, 1}}
end.
-file("src/viva_tensor/sparsity.gleam", 143).
?DOC(" Pad grupo para ter exatamente 4 elementos\n").
-spec pad_group(list(float())) -> list(float()).
pad_group(Group) ->
Len = erlang:length(Group),
case Len < 4 of
true ->
lists:append(Group, gleam@list:repeat(+0.0, 4 - Len));
false ->
gleam@list:take(Group, 4)
end.
-file("src/viva_tensor/sparsity.gleam", 188).
-spec prune_group_by_importance(list(float()), list(float())) -> sparse24_block().
prune_group_by_importance(Weights, Importance) ->
Indexed = begin
_pipe = gleam@list:zip(
gleam@list:range(0, 3),
gleam@list:zip(Weights, Importance)
),
gleam@list:map(
_pipe,
fun(X) ->
{Idx, {W, I}} = X,
{Idx, W, I}
end
)
end,
Sorted = gleam@list:sort(
Indexed,
fun(A, B) ->
gleam@float:compare(erlang:element(3, B), erlang:element(3, A))
end
),
case Sorted of
[First, Second | _] ->
{Pos1, Val1, _} = First,
{Pos2, Val2, _} = Second,
{P1, V1, P2, V2} = case Pos1 < Pos2 of
true ->
{Pos1, Val1, Pos2, Val2};
false ->
{Pos2, Val2, Pos1, Val1}
end,
{sparse24_block, {V1, V2}, {P1, P2}};
_ ->
{sparse24_block, {+0.0, +0.0}, {0, 1}}
end.
-file("src/viva_tensor/sparsity.gleam", 220).
?DOC(" Reconstrói tensor denso a partir de 2:4 sparse\n").
-spec decompress(sparse24_tensor()) -> viva_tensor@tensor:tensor().
decompress(Sparse) ->
Data = gleam@list:flat_map(
erlang:element(2, Sparse),
fun(Block) ->
{V1, V2} = erlang:element(2, Block),
{P1, P2} = erlang:element(3, Block),
_pipe = gleam@list:range(0, 3),
gleam@list:map(_pipe, fun(I) -> case I =:= P1 of
true ->
V1;
false ->
case I =:= P2 of
true ->
V2;
false ->
+0.0
end
end end)
end
),
Truncated = gleam@list:take(Data, erlang:element(4, Sparse)),
{tensor, Truncated, erlang:element(3, Sparse)}.
-file("src/viva_tensor/sparsity.gleam", 298).
-spec transpose_matrix(list(list(float()))) -> list(list(float())).
transpose_matrix(M) ->
case M of
[] ->
[];
[First | _] ->
N_cols = erlang:length(First),
_pipe = gleam@list:range(0, N_cols - 1),
gleam@list:map(
_pipe,
fun(Col_idx) ->
gleam@list:filter_map(
M,
fun(Row) -> case gleam@list:drop(Row, Col_idx) of
[X | _] ->
{ok, X};
[] ->
{error, nil}
end end
)
end
)
end.
-file("src/viva_tensor/sparsity.gleam", 321).
?DOC(" Calcula métricas de poda\n").
-spec compute_metrics(viva_tensor@tensor:tensor(), sparse24_tensor()) -> prune_metrics().
compute_metrics(Original, Sparse) ->
Orig_data = viva_tensor@tensor:to_list(Original),
Decomp_data = viva_tensor@tensor:to_list(decompress(Sparse)),
Errors = gleam@list:map2(
Orig_data,
Decomp_data,
fun(O, D) -> gleam@float:absolute_value(O - D) end
),
Mean_error = case Errors of
[] ->
+0.0;
_ ->
case erlang:float(erlang:length(Errors)) of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator -> gleam@list:fold(
Errors,
+0.0,
fun gleam@float:add/2
)
/ Gleam@denominator
end
end,
Kept = gleam@list:filter_map(
gleam@list:zip(Orig_data, Decomp_data),
fun(Pair) ->
{O@1, D@1} = Pair,
case gleam@float:absolute_value(D@1) > +0.0 of
true ->
{ok, gleam@float:absolute_value(O@1)};
false ->
{error, nil}
end
end
),
Pruned = gleam@list:filter_map(
gleam@list:zip(Orig_data, Decomp_data),
fun(Pair@1) ->
{O@2, D@2} = Pair@1,
case gleam@float:absolute_value(D@2) > +0.0 of
false ->
{ok, gleam@float:absolute_value(O@2)};
true ->
{error, nil}
end
end
),
Kept_mean = case Kept of
[] ->
+0.0;
_ ->
case erlang:float(erlang:length(Kept)) of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator@1 -> gleam@list:fold(
Kept,
+0.0,
fun gleam@float:add/2
)
/ Gleam@denominator@1
end
end,
Pruned_mean = case Pruned of
[] ->
+0.0;
_ ->
case erlang:float(erlang:length(Pruned)) of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator@2 -> gleam@list:fold(
Pruned,
+0.0,
fun gleam@float:add/2
)
/ Gleam@denominator@2
end
end,
{prune_metrics,
erlang:length(Pruned),
erlang:length(Orig_data),
Mean_error,
Kept_mean,
Pruned_mean}.
-file("src/viva_tensor/sparsity.gleam", 529).
-spec float_to_string(float()) -> binary().
float_to_string(F) ->
Rounded = erlang:float(erlang:round(F * 10000.0)) / 10000.0,
gleam_stdlib:float_to_string(Rounded).
-file("src/viva_tensor/sparsity.gleam", 534).
-spec get_tensor_shape(viva_tensor@tensor:tensor()) -> list(integer()).
get_tensor_shape(T) ->
case T of
{tensor, _, Shape} ->
Shape;
{strided_tensor, _, Shape@1, _, _} ->
Shape@1
end.
-file("src/viva_tensor/sparsity.gleam", 82).
?DOC(
" Aplica poda 2:4: mantém os 2 maiores em cada grupo de 4\n"
" Estratégia: magnitude (abs) - padrão da NVIDIA\n"
).
-spec prune_24_magnitude(viva_tensor@tensor:tensor()) -> sparse24_tensor().
prune_24_magnitude(T) ->
Data = viva_tensor@tensor:to_list(T),
Shape = get_tensor_shape(T),
Num_elements = erlang:length(Data),
Groups = gleam@list:sized_chunk(Data, 4),
Blocks = gleam@list:map(
Groups,
fun(Group) -> prune_group_magnitude(pad_group(Group)) end
),
Num_blocks = erlang:length(Blocks),
Memory = Num_blocks * 5,
{sparse24_tensor, Blocks, Shape, Num_elements, Memory, 50.0}.
-file("src/viva_tensor/sparsity.gleam", 157).
?DOC(
" Poda baseada em gradiente (para treinamento)\n"
" Mantém elementos com maior |valor × gradiente|\n"
).
-spec prune_24_gradient(
viva_tensor@tensor:tensor(),
viva_tensor@tensor:tensor()
) -> sparse24_tensor().
prune_24_gradient(Weights, Gradients) ->
W_data = viva_tensor@tensor:to_list(Weights),
G_data = viva_tensor@tensor:to_list(Gradients),
Shape = get_tensor_shape(Weights),
Num_elements = erlang:length(W_data),
Importance = gleam@list:map2(
W_data,
G_data,
fun(W, G) -> gleam@float:absolute_value(W * G) end
),
W_groups = gleam@list:sized_chunk(W_data, 4),
I_groups = gleam@list:sized_chunk(Importance, 4),
Blocks = gleam@list:map2(
W_groups,
I_groups,
fun(W_group, I_group) ->
prune_group_by_importance(pad_group(W_group), I_group)
end
),
Num_blocks = erlang:length(Blocks),
{sparse24_tensor, Blocks, Shape, Num_elements, Num_blocks * 5, 50.0}.
-file("src/viva_tensor/sparsity.gleam", 267).
?DOC(" Matmul básico para tensors\n").
-spec tensor_matmul(viva_tensor@tensor:tensor(), viva_tensor@tensor:tensor()) -> viva_tensor@tensor:tensor().
tensor_matmul(A, B) ->
A_data = viva_tensor@tensor:to_list(A),
B_data = viva_tensor@tensor:to_list(B),
A_shape = get_tensor_shape(A),
B_shape = get_tensor_shape(B),
{M, K} = case A_shape of
[Rows, Cols] ->
{Rows, Cols};
_ ->
{1, erlang:length(A_data)}
end,
{_, N} = case B_shape of
[Rows@1, Cols@1] ->
{Rows@1, Cols@1};
_ ->
{erlang:length(B_data), 1}
end,
A_rows = gleam@list:sized_chunk(A_data, K),
B_cols = transpose_matrix(gleam@list:sized_chunk(B_data, N)),
Result = gleam@list:flat_map(
A_rows,
fun(A_row) ->
gleam@list:map(
B_cols,
fun(B_col) ->
_pipe = gleam@list:map2(
A_row,
B_col,
fun(X, Y) -> X * Y end
),
gleam@list:fold(_pipe, +0.0, fun gleam@float:add/2)
end
)
end
),
{tensor, Result, [M, N]}.
-file("src/viva_tensor/sparsity.gleam", 250).
?DOC(
" Matmul com matriz esparsa 2:4\n"
" Em hardware real (Tensor Cores), isso é 2x mais rápido!\n"
).
-spec sparse_matmul(sparse24_tensor(), viva_tensor@tensor:tensor()) -> {viva_tensor@tensor:tensor(),
float()}.
sparse_matmul(Sparse_a, Dense_b) ->
Dense_a = decompress(Sparse_a),
Result = tensor_matmul(Dense_a, Dense_b),
Theoretical_speedup = 2.0,
{Result, Theoretical_speedup}.
-file("src/viva_tensor/sparsity.gleam", 379).
-spec benchmark_sparsity() -> nil.
benchmark_sparsity() ->
gleam_stdlib:println(
<<"╔══════════════════════════════════════════════════════════════════╗"/utf8>>
),
gleam_stdlib:println(
<<"║ 2:4 STRUCTURED SPARSITY - NVIDIA Tensor Cores ║"/utf8>>
),
gleam_stdlib:println(
<<"║ Ampere+ Architecture (RTX 3000/4000, A100, H100) ║"/utf8>>
),
gleam_stdlib:println(
<<"╚══════════════════════════════════════════════════════════════════╝\n"/utf8>>
),
gleam_stdlib:println(<<"CONCEITO:"/utf8>>),
gleam_stdlib:println(
<<" - Em cada 4 elementos, mantém apenas 2 (50% sparsity)"/utf8>>
),
gleam_stdlib:println(
<<" - Padrão ESTRUTURADO permite aceleração em hardware"/utf8>>
),
gleam_stdlib:println(
<<" - Tensor Cores pulam multiplicações por zero"/utf8>>
),
gleam_stdlib:println(
<<" - Resultado: 2x throughput com ~1% perda de accuracy!\n"/utf8>>
),
gleam_stdlib:println(<<"FORMATO DE ARMAZENAMENTO:"/utf8>>),
gleam_stdlib:println(<<" - Original: 4 × FP16 = 64 bits"/utf8>>),
gleam_stdlib:println(<<" - Sparse: 2 × FP16 + 4-bit mask = 36 bits"/utf8>>),
gleam_stdlib:println(<<" - Compressão: 1.78x\n"/utf8>>),
T = viva_tensor@tensor:random_uniform([1024, 512]),
gleam_stdlib:println(<<"━━━ BENCHMARK: Tensor [1024, 512] ━━━"/utf8>>),
{Time_prune, Sparse} = timer:tc(fun() -> prune_24_magnitude(T) end),
Metrics = compute_metrics(T, Sparse),
Original_bytes = (1024 * 512) * 4,
Compression = case erlang:float(erlang:element(5, Sparse)) of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator -> erlang:float(Original_bytes) / Gleam@denominator
end,
gleam_stdlib:println(
<<<<" Tempo de poda: "/utf8,
(erlang:integer_to_binary(Time_prune div 1000))/binary>>/binary,
"ms"/utf8>>
),
gleam_stdlib:println(
<<<<" Memória original: "/utf8,
(erlang:integer_to_binary(Original_bytes div 1024))/binary>>/binary,
" KB"/utf8>>
),
gleam_stdlib:println(
<<<<" Memória sparse: "/utf8,
(erlang:integer_to_binary(erlang:element(5, Sparse) div 1024))/binary>>/binary,
" KB"/utf8>>
),
gleam_stdlib:println(
<<<<" Compressão: "/utf8,
(float_to_string(Compression))/binary>>/binary,
"x"/utf8>>
),
gleam_stdlib:println(
<<<<" Sparsity: "/utf8,
(float_to_string(erlang:element(6, Sparse)))/binary>>/binary,
"%"/utf8>>
),
gleam_stdlib:println(<<""/utf8>>),
gleam_stdlib:println(
<<<<<<" Elementos podados: "/utf8,
(erlang:integer_to_binary(erlang:element(2, Metrics)))/binary>>/binary,
"/"/utf8>>/binary,
(erlang:integer_to_binary(erlang:element(3, Metrics)))/binary>>
),
gleam_stdlib:println(
<<" Erro aproximação: "/utf8,
(float_to_string(erlang:element(4, Metrics)))/binary>>
),
gleam_stdlib:println(
<<" Magnitude mantida: "/utf8,
(float_to_string(erlang:element(5, Metrics)))/binary>>
),
gleam_stdlib:println(
<<" Magnitude podada: "/utf8,
(float_to_string(erlang:element(6, Metrics)))/binary>>
),
gleam_stdlib:println(<<"\n━━━ SPARSE MATMUL SIMULATION ━━━"/utf8>>),
B = viva_tensor@tensor:random_uniform([512, 256]),
{Time_dense, Dense_result} = timer:tc(
fun() -> tensor_matmul(decompress(Sparse), B) end
),
{Time_sparse, {Sparse_result, Speedup}} = timer:tc(
fun() -> sparse_matmul(Sparse, B) end
),
gleam_stdlib:println(
<<<<" Dense matmul: "/utf8,
(erlang:integer_to_binary(Time_dense div 1000))/binary>>/binary,
"ms"/utf8>>
),
gleam_stdlib:println(
<<<<" Sparse matmul: "/utf8,
(erlang:integer_to_binary(Time_sparse div 1000))/binary>>/binary,
"ms (simulado)"/utf8>>
),
gleam_stdlib:println(
<<<<" Speedup teórico: "/utf8, (float_to_string(Speedup))/binary>>/binary,
"x (hardware real)"/utf8>>
),
Dense_data = viva_tensor@tensor:to_list(Dense_result),
Sparse_data = viva_tensor@tensor:to_list(Sparse_result),
Diff = begin
_pipe = gleam@list:map2(
Dense_data,
Sparse_data,
fun(D, S) -> gleam@float:absolute_value(D - S) end
),
gleam@list:fold(_pipe, +0.0, fun gleam@float:max/2)
end,
gleam_stdlib:println(
<<<<" Diferença máxima: "/utf8, (float_to_string(Diff))/binary>>/binary,
" (deveria ser ~0)"/utf8>>
),
gleam_stdlib:println(<<"\n━━━ COMPARAÇÃO: COMBINANDO TÉCNICAS ━━━"/utf8>>),
gleam_stdlib:println(<<" FP16: 2x compressão"/utf8>>),
gleam_stdlib:println(<<" INT8: 4x compressão"/utf8>>),
gleam_stdlib:println(
<<" 2:4 Sparsity: 2x speedup (+ 1.78x compressão)"/utf8>>
),
gleam_stdlib:println(<<" NF4: 8x compressão"/utf8>>),
gleam_stdlib:println(<<" "/utf8>>),
gleam_stdlib:println(
<<" INT8 + 2:4: 4x × 1.78x = 7.12x compressão, 8x speedup!"/utf8>>
),
gleam_stdlib:println(
<<" NF4 + 2:4: 8x × 1.78x = 14.24x compressão!"/utf8>>
),
gleam_stdlib:println(
<<"\n╔══════════════════════════════════════════════════════════════════╗"/utf8>>
),
gleam_stdlib:println(
<<"║ POR QUE 2:4 SPARSITY É ESSENCIAL: ║"/utf8>>
),
gleam_stdlib:println(
<<"║ ║"/utf8>>
),
gleam_stdlib:println(
<<"║ 1. Hardware nativo em RTX 3000/4000/A100/H100 ║"/utf8>>
),
gleam_stdlib:println(
<<"║ 2. 2x throughput com ~1% perda de accuracy ║"/utf8>>
),
gleam_stdlib:println(
<<"║ 3. Combina com quantização para 4x+ total ║"/utf8>>
),
gleam_stdlib:println(
<<"║ 4. Padrão em modelos NVIDIA (Megatron-LM, etc) ║"/utf8>>
),
gleam_stdlib:println(
<<"║ ║"/utf8>>
),
gleam_stdlib:println(
<<"║ viva_tensor + 2:4 = Máximo uso dos Tensor Cores! ║"/utf8>>
),
gleam_stdlib:println(
<<"╚══════════════════════════════════════════════════════════════════╝"/utf8>>
).
-file("src/viva_tensor/sparsity.gleam", 375).
-spec main() -> nil.
main() ->
benchmark_sparsity().