Skip to main content

ferric_web/
lib.rs

1//! Ferric web — proves the SAME pure-Rust kernels (ferric-core) run in the browser on WebGPU.
2//! The matmul WGSL, the Context, the readback — all identical to native; only the target changes.
3use ferric_core::{demo, matmul_cpu, max_abs_diff, Context};
4use wasm_bindgen::prelude::*;
5
6// Same deterministic matrices as the native example, so the browser result must match.
7fn gen(m: u32, k: u32, n: u32) -> (Vec<f32>, Vec<f32>) {
8    let a: Vec<f32> = (0..(m * k) as usize).map(|i| ((i * 7 % 13) as f32 - 6.0) * 0.1).collect();
9    let b: Vec<f32> = (0..(k * n) as usize).map(|i| ((i * 5 % 11) as f32 - 5.0) * 0.1).collect();
10    (a, b)
11}
12
13/// Runs the Ferric matmul on the browser's WebGPU and validates against the CPU reference.
14/// Returns "backend|maxdiff|first6".
15#[wasm_bindgen]
16pub async fn ferric_matmul_demo(m: u32, k: u32, n: u32) -> std::result::Result<String, JsValue> {
17    console_error_panic_hook::set_once();
18    let ctx = Context::new().await.map_err(|e| JsValue::from_str(&e))?;
19    let (a, b) = gen(m, k, n);
20    let gpu = ctx.matmul(&a, &b, m, k, n).await.map_err(|e| JsValue::from_str(&e))?;
21    let cpu = matmul_cpu(&a, &b, m as usize, k as usize, n as usize);
22    let diff = max_abs_diff(&gpu, &cpu);
23    Ok(format!("{:?}|{:.3e}|{:?}", ctx.backend, diff, &gpu[..6.min(gpu.len())]))
24}
25
26/// Runs the full Ferric transformer LM in the browser on WebGPU: greedy-generates `steps` tokens from
27/// a comma-separated prompt of token ids, and validates the prefill logits against the in-wasm CPU
28/// reference. Returns a JSON string {backend, prompt, generated, layers, logit_diff, ms}.
29#[wasm_bindgen]
30pub async fn ferric_lm_demo(prompt: String, steps: usize) -> std::result::Result<String, JsValue> {
31    console_error_panic_hook::set_once();
32    let ids: Vec<u32> = prompt.split(',').filter_map(|s| s.trim().parse().ok()).map(|v: u32| v % demo::VOCAB as u32).collect();
33    if ids.is_empty() {
34        return Err(JsValue::from_str("no valid token ids in prompt"));
35    }
36    let ctx = Context::new().await.map_err(|e| JsValue::from_str(&e))?;
37    let t0 = js_sys::Date::now();
38    let generated = demo::generate(&ctx, &ids, steps).await.map_err(|e| JsValue::from_str(&e))?;
39    let ms = js_sys::Date::now() - t0;
40    // correctness, in the browser: GPU prefill logits vs the CPU reference (same math, wasm CPU)
41    let gpu = demo::logits(&ctx, &ids).await.map_err(|e| JsValue::from_str(&e))?;
42    let diff = max_abs_diff(&gpu, &demo::logits_cpu(&ids));
43    let js = |v: &[u32]| v.iter().map(|x| x.to_string()).collect::<Vec<_>>().join(",");
44    Ok(format!(
45        "{{\"backend\":\"{:?}\",\"adapter\":\"{}\",\"prompt\":[{}],\"generated\":[{}],\"layers\":{},\"vocab\":{},\"logit_diff\":{:.3e},\"ms\":{:.1}}}",
46        ctx.backend, ctx.adapter_name, js(&ids), js(&generated), demo::N_LAYERS, demo::VOCAB, diff, ms
47    ))
48}