use wasm_bindgen::prelude::*;
use crate::exec;
use crate::vision::{self, ModelMeta, VisionModelId};
fn grad_slices(outs: &[Vec<f32>]) -> Vec<&[f32]> {
outs.iter().skip(1).map(|v| v.as_slice()).collect()
}
#[wasm_bindgen]
pub fn list_vision_models() -> Vec<String> {
VisionModelId::all()
.iter()
.map(|id| id.slug().to_string())
.collect()
}
#[wasm_bindgen]
pub struct VisionModelInfo {
slug: String,
title: String,
input_dims: Vec<usize>,
num_classes: usize,
param_names: Vec<String>,
param_sizes: Vec<usize>,
webgl_train: bool,
}
#[wasm_bindgen]
impl VisionModelInfo {
#[wasm_bindgen(getter)]
pub fn slug(&self) -> String {
self.slug.clone()
}
#[wasm_bindgen(getter)]
pub fn title(&self) -> String {
self.title.clone()
}
#[wasm_bindgen(getter)]
pub fn input_dims(&self) -> Vec<usize> {
self.input_dims.clone()
}
#[wasm_bindgen(getter)]
pub fn num_classes(&self) -> usize {
self.num_classes
}
#[wasm_bindgen(getter)]
pub fn param_names(&self) -> Vec<String> {
self.param_names.clone()
}
#[wasm_bindgen(getter)]
pub fn param_sizes(&self) -> Vec<usize> {
self.param_sizes.clone()
}
#[wasm_bindgen(getter)]
pub fn webgl_train(&self) -> bool {
self.webgl_train
}
#[wasm_bindgen(getter)]
pub fn input_len(&self) -> usize {
self.input_dims.iter().product()
}
#[wasm_bindgen(getter)]
pub fn param_flat_len(&self) -> usize {
self.param_sizes.iter().sum()
}
}
fn info_from_meta(meta: &ModelMeta) -> VisionModelInfo {
VisionModelInfo {
slug: meta.id.slug().to_string(),
title: meta.id.title().to_string(),
input_dims: meta.input_shape.to_vec(),
num_classes: meta.num_classes,
param_names: meta.param_names().into_iter().map(str::to_string).collect(),
param_sizes: meta.param_sizes(),
webgl_train: meta.id.webgl_train(),
}
}
#[wasm_bindgen]
pub fn vision_model_info(slug: &str) -> Result<VisionModelInfo, JsValue> {
let id = VisionModelId::from_slug(slug).ok_or_else(|| {
JsValue::from_str(&format!(
"unknown vision model {slug:?}; use list_vision_models()"
))
})?;
Ok(info_from_meta(&vision::model_meta(id)))
}
#[wasm_bindgen]
pub struct VisionBench {
id: VisionModelId,
meta: ModelMeta,
}
#[wasm_bindgen]
impl VisionBench {
#[wasm_bindgen(constructor)]
pub fn new(slug: &str) -> Result<VisionBench, JsValue> {
let id = VisionModelId::from_slug(slug)
.ok_or_else(|| JsValue::from_str(&format!("unknown vision model {slug:?}")))?;
let meta = vision::model_meta(id);
Ok(Self { id, meta })
}
pub fn info(&self) -> VisionModelInfo {
info_from_meta(&self.meta)
}
pub fn init_params(&self, seed: u32) -> Vec<f32> {
vision::init_params(&self.meta, seed)
}
pub fn synthetic_batch(&self, seed: u32) -> Vec<f32> {
let (x, label) = vision::synthetic_input(&self.meta, seed);
let mut out = x;
out.push(label);
out
}
pub fn forward_cpu(&self, x: &[f32], params: &[f32]) -> Result<Vec<f32>, JsValue> {
exec::run_forward_cpu(self.id, &self.meta, x, params).map_err(|e| JsValue::from_str(&e))
}
pub fn train_step_cpu(
&self,
x: &[f32],
label: f32,
params: &[f32],
lr: f32,
) -> Result<Vec<f32>, JsValue> {
let (loss, outs) = exec::run_train_cpu(self.id, &self.meta, x, label, params)
.map_err(|e| JsValue::from_str(&e))?;
let grads = grad_slices(&outs);
let updated = vision::train_step_params(&self.meta, params, &grads, lr);
let mut flat = vec![loss];
flat.extend(updated);
Ok(flat)
}
pub fn bench_cpu(&self, steps: u32, seed: u32, lr: f32) -> Result<Vec<f32>, JsValue> {
let mut params = vision::init_params(&self.meta, seed);
let (x, label) = vision::synthetic_input(&self.meta, seed);
let l0 = exec::run_train_cpu(self.id, &self.meta, &x, label, ¶ms)
.map_err(|e| JsValue::from_str(&e))?
.0;
let t0 = bench_now();
for step in 0..steps {
let (loss, outs) = exec::run_train_cpu(self.id, &self.meta, &x, label, ¶ms)
.map_err(|e| JsValue::from_str(&e))?;
let grads = grad_slices(&outs);
params = vision::train_step_params(&self.meta, ¶ms, &grads, lr);
let _ = (step, loss);
}
let elapsed = bench_now() - t0;
let l1 = exec::run_train_cpu(self.id, &self.meta, &x, label, ¶ms)
.map_err(|e| JsValue::from_str(&e))?
.0;
let sps = if elapsed > 0.0 {
steps as f32 / elapsed
} else {
0.0
};
Ok(vec![l0, l1, sps])
}
#[cfg(all(feature = "webgpu", target_arch = "wasm32"))]
pub async fn forward_webgpu(&self, x: Vec<f32>, params: Vec<f32>) -> Result<Vec<f32>, JsValue> {
exec::run_forward_webgpu(self.id, &self.meta, &x, ¶ms)
.await
.map_err(|e| JsValue::from_str(&e))
}
#[cfg(all(feature = "webgpu", target_arch = "wasm32"))]
pub async fn train_step_webgpu(
&self,
x: Vec<f32>,
label: f32,
params: Vec<f32>,
lr: f32,
) -> Result<Vec<f32>, JsValue> {
let (loss, outs) = exec::run_train_webgpu(self.id, &self.meta, &x, label, ¶ms)
.await
.map_err(|e| JsValue::from_str(&e))?;
let grads = grad_slices(&outs);
let updated = vision::train_step_params(&self.meta, ¶ms, &grads, lr);
let mut flat = vec![loss];
flat.extend(updated);
Ok(flat)
}
#[cfg(all(feature = "webgl", target_arch = "wasm32"))]
pub fn forward_webgl(&self, x: &[f32], params: &[f32]) -> Result<Vec<f32>, JsValue> {
exec::run_forward_webgl(self.id, &self.meta, x, params).map_err(|e| JsValue::from_str(&e))
}
#[cfg(all(feature = "webgl", target_arch = "wasm32"))]
pub fn train_step_webgl(
&self,
x: &[f32],
label: f32,
params: &[f32],
lr: f32,
) -> Result<Vec<f32>, JsValue> {
let (loss, outs) = exec::run_train_webgl(self.id, &self.meta, x, label, params)
.map_err(|e| JsValue::from_str(&e))?;
let grads = grad_slices(&outs);
let updated = vision::train_step_params(&self.meta, params, &grads, lr);
let mut flat = vec![loss];
flat.extend(updated);
Ok(flat)
}
}
#[wasm_bindgen]
pub fn parse_backend(name: &str) -> String {
exec::ExecBackend::parse(name).label().to_string()
}
#[wasm_bindgen]
pub fn preferred_backend() -> String {
rlx_runtime::preferred_browser_device()
.map(|d| match d {
rlx_runtime::Device::WebGpu => "webgpu".to_string(),
rlx_runtime::Device::OpenGl => "webgl".to_string(),
rlx_runtime::Device::Cpu => "cpu".to_string(),
other => format!("{other:?}"),
})
.unwrap_or_else(|| "none".to_string())
}
fn bench_now() -> f32 {
#[cfg(target_arch = "wasm32")]
{
web_sys::window()
.and_then(|w| w.performance())
.map(|p| p.now() as f32 / 1000.0)
.unwrap_or(0.0)
}
#[cfg(not(target_arch = "wasm32"))]
{
use std::time::Instant;
static START: std::sync::OnceLock<Instant> = std::sync::OnceLock::new();
let start = START.get_or_init(Instant::now);
start.elapsed().as_secs_f32()
}
}
#[cfg(test)]
mod api_tests {
use crate::{VisionBench, list_vision_models};
#[test]
fn lists_four_models() {
assert_eq!(list_vision_models().len(), 4);
}
#[test]
fn bench_cpu_runs() {
let bench = VisionBench::new("mnist-mlp").unwrap();
let out = bench.bench_cpu(5, 7, 0.01).unwrap();
assert_eq!(out.len(), 3);
}
#[test]
fn all_slugs_bench_decreases_loss() {
for slug in list_vision_models() {
let bench = VisionBench::new(&slug).unwrap();
let out = bench.bench_cpu(15, 11, 0.05).unwrap();
let (l0, l1, _sps) = (out[0], out[1], out[2]);
assert!(
l0.is_finite() && l1.is_finite(),
"{slug}: non-finite loss {l0} -> {l1}"
);
assert!(
l1 <= l0 + 1e-3,
"{slug}: expected loss not to rise: {l0} -> {l1}"
);
}
}
}