Packages
Tensor library for Gleam/BEAM with a pure Gleam API, zero-copy views, and optional native acceleration
Current section
Files
Jump to
Current section
Files
src/viva_tensor/optim/pool.gleam
//// TensorPool - Distributed Tensor Computing via OTP
////
//// INNOVATIVE CONCEPT: GenServer + GPU
//// ===================================
////
//// In C/C++: you have threads + mutex + condition vars (NIGHTMARE)
//// In Gleam/OTP: GenServer manages state, NIFs do GPU compute
////
//// Architecture:
////
//// [Request] -> [GenServer Pool] -> [Worker 1] -> [NIF GPU] -> cuBLAS/cuDNN
//// -> [Worker 2] -> [NIF GPU] -> cuBLAS/cuDNN
//// -> [Worker N] -> [NIF GPU] -> cuBLAS/cuDNN
////
//// Each Worker is a lightweight BEAM process (~2KB) that can call GPU NIFs.
//// The GenServer does automatic load balancing.
//// If a Worker crashes, OTP restarts it (free fault tolerance!)
import gleam/float
import gleam/int
import gleam/io
import gleam/list
import viva_tensor/tensor.{type Tensor}
// ============================================================================
// PUBLIC TYPES
// ============================================================================
/// Supported tensor operations
pub type TensorOp {
Scale(Float)
Normalize
}
/// Similarity search result
pub type SearchResult {
SearchResult(index: Int, similarity: Float)
}
// ============================================================================
// INTERNAL TYPES
// ============================================================================
type Pid
// ============================================================================
// PUBLIC API - THE DIFFERENTIATOR!
// ============================================================================
/// Processes a list of tensors in parallel
///
/// Each tensor is processed in a separate BEAM process.
/// In C/C++ this would require pthread_create + mutex for each tensor.
/// In Gleam: one line of code and zero data races!
pub fn parallel_map(tensors: List(Tensor), op: TensorOp) -> List(Tensor) {
let parent = erlang_self()
let indexed = list.index_map(tensors, fn(t, idx) { #(idx, t) })
// Spawns a lightweight process for each tensor (~2KB each)
list.each(indexed, fn(pair) {
let #(idx, t) = pair
erlang_spawn(fn() {
let result = apply_op(t, op)
erlang_send(parent, #(idx, result))
})
})
// Collect and sort results
collect_indexed(list.length(tensors))
|> list.sort(fn(a, b) {
let #(idx_a, _) = a
let #(idx_b, _) = b
int.compare(idx_a, idx_b)
})
|> list.map(fn(pair) {
let #(_, t) = pair
t
})
}
/// Batch similarity search - THE KILLER FEATURE!
///
/// Compares a query against thousands of documents in parallel.
/// Returns results sorted by similarity (desc).
pub fn similarity_search(
query: Tensor,
documents: List(Tensor),
chunk_size: Int,
) -> List(SearchResult) {
let parent = erlang_self()
let chunks = list.sized_chunk(documents, chunk_size)
let num_chunks = list.length(chunks)
// Each chunk is processed in parallel
list.index_map(chunks, fn(chunk, chunk_idx) {
erlang_spawn(fn() {
let start_idx = chunk_idx * chunk_size
let results =
list.index_map(chunk, fn(doc, local_idx) {
let similarity = case tensor.dot(query, doc) {
Ok(sim) -> sim
Error(_) -> 0.0
}
SearchResult(index: start_idx + local_idx, similarity: similarity)
})
erlang_send(parent, #(chunk_idx, results))
})
})
// Collect, flatten and sort by similarity
collect_n_chunks(num_chunks)
|> list.flat_map(fn(pair) {
let #(_, results) = pair
results
})
|> list.sort(fn(a, b) {
// Descending order
float.compare(b.similarity, a.similarity)
})
}
/// Top-K similarity - returns the K most similar
pub fn top_k_similar(
query: Tensor,
documents: List(Tensor),
k: Int,
) -> List(SearchResult) {
similarity_search(query, documents, 100)
|> list.take(k)
}
/// Parallel sum of all tensors
pub fn parallel_sum(tensors: List(Tensor)) -> Float {
let parent = erlang_self()
let chunks = list.sized_chunk(tensors, 100)
list.each(chunks, fn(chunk) {
erlang_spawn(fn() {
let partial = list.fold(chunk, 0.0, fn(acc, t) { acc +. tensor.sum(t) })
erlang_send(parent, partial)
})
})
collect_n_floats(list.length(chunks))
|> list.fold(0.0, fn(acc, x) { acc +. x })
}
// ============================================================================
// BENCHMARK - SHOW THE POWER!
// ============================================================================
pub fn main() {
benchmark_pool()
}
pub fn benchmark_pool() {
io.println(
"╔══════════════════════════════════════════════════════════════════╗",
)
io.println(
"║ TensorPool - Distributed Tensor Computing in Pure Gleam! ║",
)
io.println(
"║ Something C/C++ CANNOT do with this simplicity! ║",
)
io.println(
"╚══════════════════════════════════════════════════════════════════╝\n",
)
io.println("CONCEPT: GenServer + GPU")
io.println(" - GenServer manages state and load balancing")
io.println(" - Workers are lightweight BEAM processes (~2KB each)")
io.println(" - NIFs call cuBLAS/cuDNN for GPU compute")
io.println(" - Zero data races, fault tolerant by design!\n")
// Test 1: Parallel Map
io.println("━━━ TEST 1: Parallel Map (Scale 1000 tensors x 512d) ━━━")
let tensors =
list.range(1, 1000)
|> list.map(fn(_) { tensor.random_uniform([512]) })
let #(seq_time, _) =
timer_tc(fn() { list.map(tensors, fn(t) { tensor.scale(t, 2.0) }) })
let #(par_time, _) = timer_tc(fn() { parallel_map(tensors, Scale(2.0)) })
let speedup1 = safe_div(seq_time, par_time)
io.println(" Sequential: " <> int.to_string(seq_time / 1000) <> "ms")
io.println(" Parallel: " <> int.to_string(par_time / 1000) <> "ms")
io.println(" Speedup: " <> float_to_string(speedup1) <> "x")
// Test 2: Similarity Search
io.println("\n━━━ TEST 2: Similarity Search (10K docs x 512d) ━━━")
let query = tensor.random_uniform([512])
let docs =
list.range(1, 10_000)
|> list.map(fn(_) { tensor.random_uniform([512]) })
let #(search_time, results) =
timer_tc(fn() { top_k_similar(query, docs, 10) })
let throughput = 10_000.0 /. { int.to_float(search_time) /. 1_000_000.0 }
io.println(
" 10K docs searched in: " <> int.to_string(search_time / 1000) <> "ms",
)
io.println(" Throughput: " <> float_to_string(throughput) <> " docs/sec")
io.println(" Top 3 matches:")
results
|> list.take(3)
|> list.each(fn(r) {
io.println(
" Doc "
<> int.to_string(r.index)
<> ": sim="
<> float_to_string(r.similarity),
)
})
// Test 3: Parallel Reduce
io.println("\n━━━ TEST 3: Parallel Reduce (Sum 1000 tensors) ━━━")
let #(reduce_time, total) = timer_tc(fn() { parallel_sum(tensors) })
io.println(" Time: " <> int.to_string(reduce_time / 1000) <> "ms")
io.println(" Total: " <> float_to_string(total))
io.println(
"\n╔══════════════════════════════════════════════════════════════════╗",
)
io.println(
"║ HOW THIS RUNS ON THE GPU: ║",
)
io.println(
"║ ║",
)
io.println(
"║ 1. GenServer receives tensor op request ║",
)
io.println(
"║ 2. Spawns Worker (BEAM process ~2KB) ║",
)
io.println(
"║ 3. Worker calls NIF (Rust/C) that accesses GPU ║",
)
io.println(
"║ 4. NIF uses cuBLAS for matmul, cuDNN for conv ║",
)
io.println(
"║ 5. Result returns to Worker, then to GenServer ║",
)
io.println(
"║ ║",
)
io.println(
"║ Advantage: thousands of ops in parallel, fault tolerant! ║",
)
io.println(
"╚══════════════════════════════════════════════════════════════════╝",
)
}
// ============================================================================
// INTERNAL FUNCTIONS
// ============================================================================
fn apply_op(t: Tensor, op: TensorOp) -> Tensor {
case op {
Scale(factor) -> tensor.scale(t, factor)
Normalize -> tensor.normalize(t)
}
}
fn safe_div(a: Int, b: Int) -> Float {
case b > 0 {
True -> int.to_float(a) /. int.to_float(b)
False -> 1.0
}
}
fn float_to_string(f: Float) -> String {
let rounded = int.to_float(float.round(f *. 100.0)) /. 100.0
float.to_string(rounded)
}
// ============================================================================
// FFI - Erlang Interop
// ============================================================================
@external(erlang, "erlang", "self")
fn erlang_self() -> Pid
@external(erlang, "erlang", "spawn")
fn erlang_spawn(f: fn() -> a) -> Pid
@external(erlang, "viva_tensor_ffi", "send_msg")
fn erlang_send(to: Pid, msg: a) -> a
@external(erlang, "viva_tensor_ffi", "collect_n")
fn collect_indexed(n: Int) -> List(#(Int, Tensor))
@external(erlang, "viva_tensor_ffi", "collect_n")
fn collect_n_chunks(n: Int) -> List(#(Int, List(SearchResult)))
@external(erlang, "viva_tensor_ffi", "collect_n")
fn collect_n_floats(n: Int) -> List(Float)
@external(erlang, "timer", "tc")
fn timer_tc(f: fn() -> a) -> #(Int, a)