use crate::Tensor;
use ferric_core::Context;
use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};
use std::sync::Arc;
use std::time::Instant;
pub enum Device {
Gpu(Arc<Context>),
Cpu,
Remote(String),
BrowserWorker(Arc<crate::ws::WsConn>),
Npu(Arc<dyn NpuBackend>),
}
pub trait NpuBackend: Send + Sync {
fn name(&self) -> String;
fn bmm(&self, a: &[f32], b: &[f32], batch: usize, m: usize, k: usize, n: usize) -> Vec<f32>;
fn linear_relu(&self, x: &[f32], rows: usize, w: &[f32], in_: usize, out: usize) -> Vec<f32> {
self.bmm(x, w, 1, rows, in_, out).iter().map(|v| v.max(0.0)).collect()
}
}
impl Device {
pub fn name(&self) -> String {
match self {
Device::Gpu(c) => format!("GPU:{}", c.adapter_name),
Device::Cpu => "CPU".into(),
Device::Remote(addr) => format!("Remote:{addr}"),
Device::BrowserWorker(_) => "BrowserWorker".into(),
Device::Npu(b) => format!("NPU:{}", b.name()),
}
}
pub fn bmm(&self, a: &[f32], b: &[f32], batch: usize, m: usize, k: usize, n: usize) -> Vec<f32> {
match self {
Device::Gpu(ctx) => {
let ta = Tensor::from_vec(ctx, a, &[batch, m, k]);
let tb = Tensor::from_vec(ctx, b, &[k, n]);
pollster::block_on(ta.matmul(&tb).to_vec())
}
Device::Remote(addr) => remote_call(addr, 0, &[batch as u32, m as u32, k as u32, n as u32], a, b),
Device::BrowserWorker(conn) => browser_call(conn, 0, &[batch as u32, m as u32, k as u32, n as u32], a, b),
Device::Npu(back) => back.bmm(a, b, batch, m, k, n),
Device::Cpu => cpu_bmm(a, b, batch, m, k, n), }
}
pub fn linear_relu(&self, x: &[f32], rows: usize, w: &[f32], in_: usize, out: usize) -> Vec<f32> {
match self {
Device::Gpu(ctx) => {
let tx = Tensor::from_vec(ctx, x, &[rows, in_]);
let tw = Tensor::from_vec(ctx, w, &[in_, out]);
pollster::block_on(tx.matmul(&tw).relu().to_vec())
}
Device::Remote(addr) => remote_call(addr, 1, &[rows as u32, in_ as u32, out as u32, 0], x, w),
Device::BrowserWorker(conn) => browser_call(conn, 1, &[rows as u32, in_ as u32, out as u32, 0], x, w),
Device::Npu(back) => back.linear_relu(x, rows, w, in_, out),
Device::Cpu => cpu_bmm(x, w, 1, rows, in_, out).iter().map(|v| v.max(0.0)).collect(),
}
}
}
fn cpu_bmm(a: &[f32], b: &[f32], batch: usize, m: usize, k: usize, n: usize) -> Vec<f32> {
let mut out = vec![0.0f32; batch * m * n];
let threads = std::thread::available_parallelism().map(|p| p.get()).unwrap_or(1).min(batch.max(1));
let chunk = batch.div_ceil(threads.max(1));
std::thread::scope(|s| {
for (ci, slab) in out.chunks_mut(chunk * m * n).enumerate() {
let lo = ci * chunk;
s.spawn(move || {
for bt_local in 0..slab.len() / (m * n) {
let bt = lo + bt_local;
for i in 0..m {
for j in 0..n {
let mut acc = 0.0;
for l in 0..k { acc += a[bt * m * k + i * k + l] * b[l * n + j]; }
slab[bt_local * m * n + i * n + j] = acc;
}
}
}
});
}
});
out
}
pub struct NpuInfo {
pub present: bool,
pub name: String,
pub reachable_via: String,
pub dispatchable: bool,
}
pub fn probe_npu() -> NpuInfo {
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
{ return NpuInfo { present: true, name: "Apple Neural Engine (ANE)".into(), reachable_via: "CoreML".into(), dispatchable: false }; }
#[cfg(target_os = "windows")]
{ return NpuInfo { present: false, name: "Windows NPU (if present)".into(), reachable_via: "DirectML / QNN / OpenVINO EP".into(), dispatchable: false }; }
#[allow(unreachable_code)]
NpuInfo { present: false, name: "none".into(), reachable_via: "n/a".into(), dispatchable: false }
}
#[cfg(not(target_arch = "wasm32"))]
pub async fn detect_devices() -> (Vec<Device>, NpuInfo) {
let mut devices = Vec::new();
for (idx, (_name, _backend, dt)) in Context::enumerate().await.into_iter().enumerate() {
if dt != wgpu::DeviceType::Cpu {
if let Ok(ctx) = Context::for_adapter(idx).await { devices.push(Device::Gpu(Arc::new(ctx))); }
}
}
devices.push(Device::Cpu);
(devices, probe_npu())
}
pub struct Fabric {
pub devices: Vec<Device>,
}
impl Fabric {
pub fn new(devices: Vec<Device>) -> Fabric { Fabric { devices } }
pub fn probe(&self) -> Vec<f32> {
let (batch, m, k, n) = (64usize, 32, 64, 32);
let a = vec![0.01f32; batch * m * k];
let b = vec![0.02f32; k * n];
let mut rates = vec![];
for d in &self.devices {
let t0 = Instant::now();
let _ = d.bmm(&a, &b, batch, m, k, n);
let secs = t0.elapsed().as_secs_f32().max(1e-6);
rates.push((batch * m * k * n) as f32 / secs); }
let sum: f32 = rates.iter().sum();
rates.iter().map(|r| r / sum).collect()
}
pub fn data_parallel_bmm(&self, a: &[f32], b: &[f32], batch: usize, m: usize, k: usize, n: usize, weights: &[f32]) -> (Vec<f32>, Vec<usize>) {
let mut counts: Vec<usize> = weights.iter().map(|w| (w * batch as f32).round() as usize).collect();
let assigned: isize = counts.iter().map(|&c| c as isize).sum();
counts[0] = (counts[0] as isize + (batch as isize - assigned)).max(0) as usize; let mut ranges = vec![]; let mut off = 0;
for &c in &counts { ranges.push((off, off + c)); off += c; }
let mut out = vec![0.0f32; batch * m * n];
std::thread::scope(|s| {
let mut handles = vec![];
for (di, &(lo, hi)) in ranges.iter().enumerate().skip(1) {
if hi <= lo { continue; }
let dev = &self.devices[di];
let aslice = &a[lo * m * k..hi * m * k];
handles.push((di, lo, hi, s.spawn(move || dev.bmm(aslice, b, hi - lo, m, k, n))));
}
let (lo0, hi0) = ranges[0];
if hi0 > lo0 {
let r = self.devices[0].bmm(&a[lo0 * m * k..hi0 * m * k], b, hi0 - lo0, m, k, n);
out[lo0 * m * n..hi0 * m * n].copy_from_slice(&r);
}
for (_di, lo, hi, h) in handles {
let r = h.join().unwrap();
out[lo * m * n..hi * m * n].copy_from_slice(&r);
}
});
(out, counts)
}
pub fn pipeline_mlp(&self, x: &[f32], rows: usize, layers: &[(Vec<f32>, usize, usize)]) -> (Vec<f32>, Vec<String>) {
let mut act = x.to_vec();
let mut trace = vec![];
for (li, (w, in_, out)) in layers.iter().enumerate() {
let dev = &self.devices[li % self.devices.len()];
act = dev.linear_relu(&act, rows, w, *in_, *out);
trace.push(dev.name());
}
(act, trace)
}
}
fn wr_u32(v: &mut Vec<u8>, x: u32) { v.extend_from_slice(&x.to_le_bytes()); }
fn wr_f32s(v: &mut Vec<u8>, f: &[f32]) { wr_u32(v, f.len() as u32); v.extend_from_slice(bytemuck::cast_slice(f)); }
fn rd_exact(s: &mut impl Read, n: usize) -> std::io::Result<Vec<u8>> {
let mut b = vec![0u8; n];
s.read_exact(&mut b)?;
Ok(b)
}
fn rd_u32(s: &mut impl Read) -> std::io::Result<u32> {
Ok(u32::from_le_bytes(rd_exact(s, 4)?.try_into().unwrap()))
}
fn rd_f32s(s: &mut impl Read) -> std::io::Result<Vec<f32>> {
let n = rd_u32(s)? as usize;
Ok(bytemuck::cast_slice(&rd_exact(s, n * 4)?).to_vec())
}
fn op_frame(op: u8, dims: &[u32; 4], a: &[f32], b: &[f32]) -> Vec<u8> {
let mut req = vec![op];
for &d in dims { wr_u32(&mut req, d); }
wr_f32s(&mut req, a);
wr_f32s(&mut req, b);
req
}
fn browser_call(conn: &crate::ws::WsConn, op: u8, dims: &[u32; 4], a: &[f32], b: &[f32]) -> Vec<f32> {
conn.send(&op_frame(op, dims, a, b)).expect("browser worker send");
let resp = conn.recv().expect("browser worker response");
bytemuck::cast_slice(&resp).to_vec()
}
fn remote_call(addr: &str, op: u8, dims: &[u32; 4], a: &[f32], b: &[f32]) -> Vec<f32> {
let mut req = vec![op];
for &d in dims { wr_u32(&mut req, d); }
wr_f32s(&mut req, a);
wr_f32s(&mut req, b);
let mut s = TcpStream::connect(addr).expect("remote worker unreachable");
s.write_all(&req).unwrap();
rd_f32s(&mut s).expect("remote worker response")
}
fn serve_one(s: &mut TcpStream, backend: &Device) -> std::io::Result<()> {
let op = rd_exact(s, 1)?[0];
let dims = [rd_u32(s)?, rd_u32(s)?, rd_u32(s)?, rd_u32(s)?];
let a = rd_f32s(s)?;
let b = rd_f32s(s)?;
let out = match op {
0 => backend.bmm(&a, &b, dims[0] as usize, dims[1] as usize, dims[2] as usize, dims[3] as usize),
_ => backend.linear_relu(&a, dims[0] as usize, &b, dims[1] as usize, dims[2] as usize),
};
let mut resp = Vec::new();
wr_f32s(&mut resp, &out);
s.write_all(&resp)
}
pub fn spawn_worker(backend: Device) -> String {
let listener = TcpListener::bind("127.0.0.1:0").expect("bind worker");
let addr = listener.local_addr().unwrap().to_string();
std::thread::spawn(move || {
for stream in listener.incoming() {
if let Ok(mut s) = stream { let _ = serve_one(&mut s, &backend); }
}
});
addr
}