1use wasm_bindgen::prelude::*;
7
8use crate::exec;
9use crate::vision::{self, ModelMeta, VisionModelId};
10
11fn grad_slices(outs: &[Vec<f32>]) -> Vec<&[f32]> {
12 outs.iter().skip(1).map(|v| v.as_slice()).collect()
13}
14
15#[wasm_bindgen]
17pub fn list_vision_models() -> Vec<String> {
18 VisionModelId::all()
19 .iter()
20 .map(|id| id.slug().to_string())
21 .collect()
22}
23
24#[wasm_bindgen]
26pub struct VisionModelInfo {
27 slug: String,
28 title: String,
29 input_dims: Vec<usize>,
30 num_classes: usize,
31 param_names: Vec<String>,
32 param_sizes: Vec<usize>,
33 webgl_train: bool,
34}
35
36#[wasm_bindgen]
37impl VisionModelInfo {
38 #[wasm_bindgen(getter)]
39 pub fn slug(&self) -> String {
40 self.slug.clone()
41 }
42
43 #[wasm_bindgen(getter)]
44 pub fn title(&self) -> String {
45 self.title.clone()
46 }
47
48 #[wasm_bindgen(getter)]
49 pub fn input_dims(&self) -> Vec<usize> {
50 self.input_dims.clone()
51 }
52
53 #[wasm_bindgen(getter)]
54 pub fn num_classes(&self) -> usize {
55 self.num_classes
56 }
57
58 #[wasm_bindgen(getter)]
59 pub fn param_names(&self) -> Vec<String> {
60 self.param_names.clone()
61 }
62
63 #[wasm_bindgen(getter)]
64 pub fn param_sizes(&self) -> Vec<usize> {
65 self.param_sizes.clone()
66 }
67
68 #[wasm_bindgen(getter)]
69 pub fn webgl_train(&self) -> bool {
70 self.webgl_train
71 }
72
73 #[wasm_bindgen(getter)]
74 pub fn input_len(&self) -> usize {
75 self.input_dims.iter().product()
76 }
77
78 #[wasm_bindgen(getter)]
79 pub fn param_flat_len(&self) -> usize {
80 self.param_sizes.iter().sum()
81 }
82}
83
84fn info_from_meta(meta: &ModelMeta) -> VisionModelInfo {
85 VisionModelInfo {
86 slug: meta.id.slug().to_string(),
87 title: meta.id.title().to_string(),
88 input_dims: meta.input_shape.to_vec(),
89 num_classes: meta.num_classes,
90 param_names: meta.param_names().into_iter().map(str::to_string).collect(),
91 param_sizes: meta.param_sizes(),
92 webgl_train: meta.id.webgl_train(),
93 }
94}
95
96#[wasm_bindgen]
98pub fn vision_model_info(slug: &str) -> Result<VisionModelInfo, JsValue> {
99 let id = VisionModelId::from_slug(slug).ok_or_else(|| {
100 JsValue::from_str(&format!(
101 "unknown vision model {slug:?}; use list_vision_models()"
102 ))
103 })?;
104 Ok(info_from_meta(&vision::model_meta(id)))
105}
106
107#[wasm_bindgen]
109pub struct VisionBench {
110 id: VisionModelId,
111 meta: ModelMeta,
112}
113
114#[wasm_bindgen]
115impl VisionBench {
116 #[wasm_bindgen(constructor)]
117 pub fn new(slug: &str) -> Result<VisionBench, JsValue> {
118 let id = VisionModelId::from_slug(slug)
119 .ok_or_else(|| JsValue::from_str(&format!("unknown vision model {slug:?}")))?;
120 let meta = vision::model_meta(id);
121 Ok(Self { id, meta })
122 }
123
124 pub fn info(&self) -> VisionModelInfo {
125 info_from_meta(&self.meta)
126 }
127
128 pub fn init_params(&self, seed: u32) -> Vec<f32> {
130 vision::init_params(&self.meta, seed)
131 }
132
133 pub fn synthetic_batch(&self, seed: u32) -> Vec<f32> {
135 let (x, label) = vision::synthetic_input(&self.meta, seed);
136 let mut out = x;
137 out.push(label);
138 out
139 }
140
141 pub fn forward_cpu(&self, x: &[f32], params: &[f32]) -> Result<Vec<f32>, JsValue> {
143 exec::run_forward_cpu(self.id, &self.meta, x, params).map_err(|e| JsValue::from_str(&e))
144 }
145
146 pub fn train_step_cpu(
148 &self,
149 x: &[f32],
150 label: f32,
151 params: &[f32],
152 lr: f32,
153 ) -> Result<Vec<f32>, JsValue> {
154 let (loss, outs) = exec::run_train_cpu(self.id, &self.meta, x, label, params)
155 .map_err(|e| JsValue::from_str(&e))?;
156 let grads = grad_slices(&outs);
157 let updated = vision::train_step_params(&self.meta, params, &grads, lr);
158 let mut flat = vec![loss];
159 flat.extend(updated);
160 Ok(flat)
161 }
162
163 pub fn bench_cpu(&self, steps: u32, seed: u32, lr: f32) -> Result<Vec<f32>, JsValue> {
166 let mut params = vision::init_params(&self.meta, seed);
167 let (x, label) = vision::synthetic_input(&self.meta, seed);
168 let l0 = exec::run_train_cpu(self.id, &self.meta, &x, label, ¶ms)
169 .map_err(|e| JsValue::from_str(&e))?
170 .0;
171
172 let t0 = bench_now();
173 for step in 0..steps {
174 let (loss, outs) = exec::run_train_cpu(self.id, &self.meta, &x, label, ¶ms)
175 .map_err(|e| JsValue::from_str(&e))?;
176 let grads = grad_slices(&outs);
177 params = vision::train_step_params(&self.meta, ¶ms, &grads, lr);
178 let _ = (step, loss);
179 }
180 let elapsed = bench_now() - t0;
181 let l1 = exec::run_train_cpu(self.id, &self.meta, &x, label, ¶ms)
182 .map_err(|e| JsValue::from_str(&e))?
183 .0;
184 let sps = if elapsed > 0.0 {
185 steps as f32 / elapsed
186 } else {
187 0.0
188 };
189 Ok(vec![l0, l1, sps])
190 }
191
192 #[cfg(all(feature = "webgpu", target_arch = "wasm32"))]
193 pub async fn forward_webgpu(&self, x: Vec<f32>, params: Vec<f32>) -> Result<Vec<f32>, JsValue> {
194 exec::run_forward_webgpu(self.id, &self.meta, &x, ¶ms)
195 .await
196 .map_err(|e| JsValue::from_str(&e))
197 }
198
199 #[cfg(all(feature = "webgpu", target_arch = "wasm32"))]
200 pub async fn train_step_webgpu(
201 &self,
202 x: Vec<f32>,
203 label: f32,
204 params: Vec<f32>,
205 lr: f32,
206 ) -> Result<Vec<f32>, JsValue> {
207 let (loss, outs) = exec::run_train_webgpu(self.id, &self.meta, &x, label, ¶ms)
208 .await
209 .map_err(|e| JsValue::from_str(&e))?;
210 let grads = grad_slices(&outs);
211 let updated = vision::train_step_params(&self.meta, ¶ms, &grads, lr);
212 let mut flat = vec![loss];
213 flat.extend(updated);
214 Ok(flat)
215 }
216
217 #[cfg(all(feature = "webgl", target_arch = "wasm32"))]
218 pub fn forward_webgl(&self, x: &[f32], params: &[f32]) -> Result<Vec<f32>, JsValue> {
219 exec::run_forward_webgl(self.id, &self.meta, x, params).map_err(|e| JsValue::from_str(&e))
220 }
221
222 #[cfg(all(feature = "webgl", target_arch = "wasm32"))]
223 pub fn train_step_webgl(
224 &self,
225 x: &[f32],
226 label: f32,
227 params: &[f32],
228 lr: f32,
229 ) -> Result<Vec<f32>, JsValue> {
230 let (loss, outs) = exec::run_train_webgl(self.id, &self.meta, x, label, params)
231 .map_err(|e| JsValue::from_str(&e))?;
232 let grads = grad_slices(&outs);
233 let updated = vision::train_step_params(&self.meta, params, &grads, lr);
234 let mut flat = vec![loss];
235 flat.extend(updated);
236 Ok(flat)
237 }
238}
239
240#[wasm_bindgen]
242pub fn parse_backend(name: &str) -> String {
243 exec::ExecBackend::parse(name).label().to_string()
244}
245
246#[wasm_bindgen]
248pub fn preferred_backend() -> String {
249 rlx_runtime::preferred_browser_device()
250 .map(|d| match d {
251 rlx_runtime::Device::WebGpu => "webgpu".to_string(),
252 rlx_runtime::Device::OpenGl => "webgl".to_string(),
253 rlx_runtime::Device::Cpu => "cpu".to_string(),
254 other => format!("{other:?}"),
255 })
256 .unwrap_or_else(|| "none".to_string())
257}
258
259fn bench_now() -> f32 {
260 #[cfg(target_arch = "wasm32")]
261 {
262 web_sys::window()
263 .and_then(|w| w.performance())
264 .map(|p| p.now() as f32 / 1000.0)
265 .unwrap_or(0.0)
266 }
267 #[cfg(not(target_arch = "wasm32"))]
268 {
269 use std::time::Instant;
270 static START: std::sync::OnceLock<Instant> = std::sync::OnceLock::new();
271 let start = START.get_or_init(Instant::now);
272 start.elapsed().as_secs_f32()
273 }
274}
275
276#[cfg(test)]
277mod api_tests {
278 use crate::{VisionBench, list_vision_models};
279
280 #[test]
281 fn lists_four_models() {
282 assert_eq!(list_vision_models().len(), 4);
283 }
284
285 #[test]
286 fn bench_cpu_runs() {
287 let bench = VisionBench::new("mnist-mlp").unwrap();
288 let out = bench.bench_cpu(5, 7, 0.01).unwrap();
289 assert_eq!(out.len(), 3);
290 }
291
292 #[test]
293 fn all_slugs_bench_decreases_loss() {
294 for slug in list_vision_models() {
295 let bench = VisionBench::new(&slug).unwrap();
296 let out = bench.bench_cpu(15, 11, 0.05).unwrap();
297 let (l0, l1, _sps) = (out[0], out[1], out[2]);
298 assert!(
299 l0.is_finite() && l1.is_finite(),
300 "{slug}: non-finite loss {l0} -> {l1}"
301 );
302 assert!(
303 l1 <= l0 + 1e-3,
304 "{slug}: expected loss not to rise: {l0} -> {l1}"
305 );
306 }
307 }
308}