1use crate::Tensor;
13use ferric_core::Context;
14use std::io::{Read, Write};
15use std::net::{TcpListener, TcpStream};
16use std::sync::Arc;
17use std::time::Instant;
18
19pub enum Device {
21 Gpu(Arc<Context>),
22 Cpu,
23 Remote(String),
26 BrowserWorker(Arc<crate::ws::WsConn>),
29 Npu(Arc<dyn NpuBackend>),
33}
34
35pub trait NpuBackend: Send + Sync {
39 fn name(&self) -> String;
40 fn bmm(&self, a: &[f32], b: &[f32], batch: usize, m: usize, k: usize, n: usize) -> Vec<f32>;
41 fn linear_relu(&self, x: &[f32], rows: usize, w: &[f32], in_: usize, out: usize) -> Vec<f32> {
42 self.bmm(x, w, 1, rows, in_, out).iter().map(|v| v.max(0.0)).collect()
43 }
44}
45
46impl Device {
47 pub fn name(&self) -> String {
48 match self {
49 Device::Gpu(c) => format!("GPU:{}", c.adapter_name),
50 Device::Cpu => "CPU".into(),
51 Device::Remote(addr) => format!("Remote:{addr}"),
52 Device::BrowserWorker(_) => "BrowserWorker".into(),
53 Device::Npu(b) => format!("NPU:{}", b.name()),
54 }
55 }
56
57 pub fn bmm(&self, a: &[f32], b: &[f32], batch: usize, m: usize, k: usize, n: usize) -> Vec<f32> {
59 match self {
60 Device::Gpu(ctx) => {
61 let ta = Tensor::from_vec(ctx, a, &[batch, m, k]);
62 let tb = Tensor::from_vec(ctx, b, &[k, n]);
63 pollster::block_on(ta.matmul(&tb).to_vec())
64 }
65 Device::Remote(addr) => remote_call(addr, 0, &[batch as u32, m as u32, k as u32, n as u32], a, b),
66 Device::BrowserWorker(conn) => browser_call(conn, 0, &[batch as u32, m as u32, k as u32, n as u32], a, b),
67 Device::Npu(back) => back.bmm(a, b, batch, m, k, n),
68 Device::Cpu => cpu_bmm(a, b, batch, m, k, n), }
70 }
71
72 pub fn linear_relu(&self, x: &[f32], rows: usize, w: &[f32], in_: usize, out: usize) -> Vec<f32> {
74 match self {
75 Device::Gpu(ctx) => {
76 let tx = Tensor::from_vec(ctx, x, &[rows, in_]);
77 let tw = Tensor::from_vec(ctx, w, &[in_, out]);
78 pollster::block_on(tx.matmul(&tw).relu().to_vec())
79 }
80 Device::Remote(addr) => remote_call(addr, 1, &[rows as u32, in_ as u32, out as u32, 0], x, w),
81 Device::BrowserWorker(conn) => browser_call(conn, 1, &[rows as u32, in_ as u32, out as u32, 0], x, w),
82 Device::Npu(back) => back.linear_relu(x, rows, w, in_, out),
83 Device::Cpu => cpu_bmm(x, w, 1, rows, in_, out).iter().map(|v| v.max(0.0)).collect(),
84 }
85 }
86}
87
88fn cpu_bmm(a: &[f32], b: &[f32], batch: usize, m: usize, k: usize, n: usize) -> Vec<f32> {
90 let mut out = vec![0.0f32; batch * m * n];
91 let threads = std::thread::available_parallelism().map(|p| p.get()).unwrap_or(1).min(batch.max(1));
92 let chunk = batch.div_ceil(threads.max(1));
93 std::thread::scope(|s| {
94 for (ci, slab) in out.chunks_mut(chunk * m * n).enumerate() {
95 let lo = ci * chunk;
96 s.spawn(move || {
97 for bt_local in 0..slab.len() / (m * n) {
98 let bt = lo + bt_local;
99 for i in 0..m {
100 for j in 0..n {
101 let mut acc = 0.0;
102 for l in 0..k { acc += a[bt * m * k + i * k + l] * b[l * n + j]; }
103 slab[bt_local * m * n + i * n + j] = acc;
104 }
105 }
106 }
107 });
108 }
109 });
110 out
111}
112
113pub struct NpuInfo {
116 pub present: bool,
117 pub name: String,
118 pub reachable_via: String,
119 pub dispatchable: bool,
120}
121
122pub fn probe_npu() -> NpuInfo {
124 #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
125 { return NpuInfo { present: true, name: "Apple Neural Engine (ANE)".into(), reachable_via: "CoreML".into(), dispatchable: false }; }
126 #[cfg(target_os = "windows")]
127 { return NpuInfo { present: false, name: "Windows NPU (if present)".into(), reachable_via: "DirectML / QNN / OpenVINO EP".into(), dispatchable: false }; }
128 #[allow(unreachable_code)]
129 NpuInfo { present: false, name: "none".into(), reachable_via: "n/a".into(), dispatchable: false }
130}
131
132#[cfg(not(target_arch = "wasm32"))]
136pub async fn detect_devices() -> (Vec<Device>, NpuInfo) {
137 let mut devices = Vec::new();
138 for (idx, (_name, _backend, dt)) in Context::enumerate().await.into_iter().enumerate() {
139 if dt != wgpu::DeviceType::Cpu {
140 if let Ok(ctx) = Context::for_adapter(idx).await { devices.push(Device::Gpu(Arc::new(ctx))); }
141 }
142 }
143 devices.push(Device::Cpu);
144 (devices, probe_npu())
145}
146
147pub struct Fabric {
149 pub devices: Vec<Device>,
150}
151
152impl Fabric {
153 pub fn new(devices: Vec<Device>) -> Fabric { Fabric { devices } }
154
155 pub fn probe(&self) -> Vec<f32> {
157 let (batch, m, k, n) = (64usize, 32, 64, 32);
158 let a = vec![0.01f32; batch * m * k];
159 let b = vec![0.02f32; k * n];
160 let mut rates = vec![];
161 for d in &self.devices {
162 let t0 = Instant::now();
163 let _ = d.bmm(&a, &b, batch, m, k, n);
164 let secs = t0.elapsed().as_secs_f32().max(1e-6);
165 rates.push((batch * m * k * n) as f32 / secs); }
167 let sum: f32 = rates.iter().sum();
168 rates.iter().map(|r| r / sum).collect()
169 }
170
171 pub fn data_parallel_bmm(&self, a: &[f32], b: &[f32], batch: usize, m: usize, k: usize, n: usize, weights: &[f32]) -> (Vec<f32>, Vec<usize>) {
173 let mut counts: Vec<usize> = weights.iter().map(|w| (w * batch as f32).round() as usize).collect();
175 let assigned: isize = counts.iter().map(|&c| c as isize).sum();
176 counts[0] = (counts[0] as isize + (batch as isize - assigned)).max(0) as usize; let mut ranges = vec![]; let mut off = 0;
178 for &c in &counts { ranges.push((off, off + c)); off += c; }
179
180 let mut out = vec![0.0f32; batch * m * n];
181 std::thread::scope(|s| {
182 let mut handles = vec![];
183 for (di, &(lo, hi)) in ranges.iter().enumerate().skip(1) {
184 if hi <= lo { continue; }
185 let dev = &self.devices[di];
186 let aslice = &a[lo * m * k..hi * m * k];
187 handles.push((di, lo, hi, s.spawn(move || dev.bmm(aslice, b, hi - lo, m, k, n))));
188 }
189 let (lo0, hi0) = ranges[0];
191 if hi0 > lo0 {
192 let r = self.devices[0].bmm(&a[lo0 * m * k..hi0 * m * k], b, hi0 - lo0, m, k, n);
193 out[lo0 * m * n..hi0 * m * n].copy_from_slice(&r);
194 }
195 for (_di, lo, hi, h) in handles {
196 let r = h.join().unwrap();
197 out[lo * m * n..hi * m * n].copy_from_slice(&r);
198 }
199 });
200 (out, counts)
201 }
202
203 pub fn pipeline_mlp(&self, x: &[f32], rows: usize, layers: &[(Vec<f32>, usize, usize)]) -> (Vec<f32>, Vec<String>) {
206 let mut act = x.to_vec();
207 let mut trace = vec![];
208 for (li, (w, in_, out)) in layers.iter().enumerate() {
209 let dev = &self.devices[li % self.devices.len()];
210 act = dev.linear_relu(&act, rows, w, *in_, *out);
211 trace.push(dev.name());
212 }
213 (act, trace)
214 }
215}
216
217fn wr_u32(v: &mut Vec<u8>, x: u32) { v.extend_from_slice(&x.to_le_bytes()); }
223fn wr_f32s(v: &mut Vec<u8>, f: &[f32]) { wr_u32(v, f.len() as u32); v.extend_from_slice(bytemuck::cast_slice(f)); }
224fn rd_exact(s: &mut impl Read, n: usize) -> std::io::Result<Vec<u8>> {
225 let mut b = vec![0u8; n];
226 s.read_exact(&mut b)?;
227 Ok(b)
228}
229fn rd_u32(s: &mut impl Read) -> std::io::Result<u32> {
230 Ok(u32::from_le_bytes(rd_exact(s, 4)?.try_into().unwrap()))
231}
232fn rd_f32s(s: &mut impl Read) -> std::io::Result<Vec<f32>> {
233 let n = rd_u32(s)? as usize;
234 Ok(bytemuck::cast_slice(&rd_exact(s, n * 4)?).to_vec())
235}
236
237fn op_frame(op: u8, dims: &[u32; 4], a: &[f32], b: &[f32]) -> Vec<u8> {
239 let mut req = vec![op];
240 for &d in dims { wr_u32(&mut req, d); }
241 wr_f32s(&mut req, a);
242 wr_f32s(&mut req, b);
243 req
244}
245
246fn browser_call(conn: &crate::ws::WsConn, op: u8, dims: &[u32; 4], a: &[f32], b: &[f32]) -> Vec<f32> {
248 conn.send(&op_frame(op, dims, a, b)).expect("browser worker send");
249 let resp = conn.recv().expect("browser worker response");
250 bytemuck::cast_slice(&resp).to_vec()
251}
252
253fn remote_call(addr: &str, op: u8, dims: &[u32; 4], a: &[f32], b: &[f32]) -> Vec<f32> {
255 let mut req = vec![op];
256 for &d in dims { wr_u32(&mut req, d); }
257 wr_f32s(&mut req, a);
258 wr_f32s(&mut req, b);
259 let mut s = TcpStream::connect(addr).expect("remote worker unreachable");
260 s.write_all(&req).unwrap();
261 rd_f32s(&mut s).expect("remote worker response")
262}
263
264fn serve_one(s: &mut TcpStream, backend: &Device) -> std::io::Result<()> {
266 let op = rd_exact(s, 1)?[0];
267 let dims = [rd_u32(s)?, rd_u32(s)?, rd_u32(s)?, rd_u32(s)?];
268 let a = rd_f32s(s)?;
269 let b = rd_f32s(s)?;
270 let out = match op {
271 0 => backend.bmm(&a, &b, dims[0] as usize, dims[1] as usize, dims[2] as usize, dims[3] as usize),
272 _ => backend.linear_relu(&a, dims[0] as usize, &b, dims[1] as usize, dims[2] as usize),
273 };
274 let mut resp = Vec::new();
275 wr_f32s(&mut resp, &out);
276 s.write_all(&resp)
277}
278
279pub fn spawn_worker(backend: Device) -> String {
282 let listener = TcpListener::bind("127.0.0.1:0").expect("bind worker");
283 let addr = listener.local_addr().unwrap().to_string();
284 std::thread::spawn(move || {
285 for stream in listener.incoming() {
286 if let Ok(mut s) = stream { let _ = serve_one(&mut s, &backend); }
287 }
288 });
289 addr
290}