1use rlx_autodiff::grad_with_loss;
40use rlx_ir::Graph;
41use rlx_runtime::{Device, Session};
42use wasm_bindgen::prelude::*;
43
44mod api;
45mod exec;
46mod mlp;
47mod transformer;
48mod vision;
49use mlp::MlpDims;
50use transformer::TfConfig;
51
52#[cfg(all(feature = "webgpu", target_arch = "wasm32"))]
53pub mod webgpu;
54
55#[cfg(all(feature = "webgl", target_arch = "wasm32"))]
56pub mod webgl;
57
58#[wasm_bindgen(start)]
61pub fn start() {
62 #[cfg(target_arch = "wasm32")]
63 install_panic_hook();
64}
65
66#[cfg(target_arch = "wasm32")]
67fn install_panic_hook() {
68 use std::sync::Once;
69 static HOOK: Once = Once::new();
70 HOOK.call_once(|| {
71 std::panic::set_hook(Box::new(|info| {
72 web_sys::console::error_1(&JsValue::from_str(&format!("rlx-web panic: {info}")));
73 }));
74 });
75}
76
77#[wasm_bindgen]
79pub fn backend() -> String {
80 "cpu".to_string()
81}
82
83#[wasm_bindgen]
91#[allow(clippy::too_many_arguments)]
92pub fn transformer_next_logits(
93 tokens: &[f32],
94 vocab: usize,
95 dim: usize,
96 n_layers: usize,
97 n_heads: usize,
98 head_dim: usize,
99 ffn: usize,
100 seed: u32,
101) -> Vec<f32> {
102 let cfg = TfConfig {
103 vocab,
104 dim,
105 n_layers,
106 n_heads,
107 head_dim,
108 ffn,
109 seq: tokens.len().max(1),
110 eps: 1e-5,
111 theta: 10000.0,
112 };
113 let all = transformer::transformer_logits(&cfg, tokens, seed as u64);
114 let start = (cfg.seq - 1) * vocab;
116 all.get(start..start + vocab)
117 .map(|s| s.to_vec())
118 .unwrap_or_default()
119}
120
121#[wasm_bindgen]
129pub fn mlp_forward(
130 x: &[f32],
131 in_dim: usize,
132 hidden: usize,
133 out_dim: usize,
134 w1: &[f32],
135 b1: &[f32],
136 w2: &[f32],
137 b2: &[f32],
138) -> Vec<f32> {
139 let dims = MlpDims {
140 in_dim,
141 hidden,
142 out_dim,
143 };
144 let (g, _params) = mlp::build_forward(dims);
145
146 let mut compiled = Session::new(Device::Cpu).compile(g);
147 set_params(&mut compiled, w1, b1, w2, b2);
148 compiled
149 .run(&[("x", x)])
150 .into_iter()
151 .next()
152 .unwrap_or_default()
153}
154
155#[wasm_bindgen]
158pub fn mlp_loss(
159 x: &[f32],
160 target: &[f32],
161 in_dim: usize,
162 hidden: usize,
163 out_dim: usize,
164 w1: &[f32],
165 b1: &[f32],
166 w2: &[f32],
167 b2: &[f32],
168) -> f32 {
169 let dims = MlpDims {
170 in_dim,
171 hidden,
172 out_dim,
173 };
174 let (g, _params) = mlp::build_loss(dims);
175
176 let mut compiled = Session::new(Device::Cpu).compile(g);
177 set_params(&mut compiled, w1, b1, w2, b2);
178 let outs = compiled.run(&[("x", x), ("target", target)]);
179 outs.first().and_then(|o| o.first().copied()).unwrap_or(0.0)
180}
181
182#[wasm_bindgen]
188pub fn mlp_grads(
189 x: &[f32],
190 target: &[f32],
191 in_dim: usize,
192 hidden: usize,
193 out_dim: usize,
194 w1: &[f32],
195 b1: &[f32],
196 w2: &[f32],
197 b2: &[f32],
198) -> Vec<f32> {
199 let dims = MlpDims {
200 in_dim,
201 hidden,
202 out_dim,
203 };
204 let g = grads_graph(dims);
205
206 let mut compiled = Session::new(Device::Cpu).compile(g);
207 set_params(&mut compiled, w1, b1, w2, b2);
208 let outs = compiled.run(&[("x", x), ("target", target), ("d_output", &[1.0])]);
209
210 let mut flat = Vec::new();
212 for o in &outs {
213 flat.extend_from_slice(o);
214 }
215 flat
216}
217
218#[wasm_bindgen]
221#[allow(clippy::too_many_arguments)]
222pub fn mlp_train_step(
223 x: &[f32],
224 target: &[f32],
225 in_dim: usize,
226 hidden: usize,
227 out_dim: usize,
228 w1: &[f32],
229 b1: &[f32],
230 w2: &[f32],
231 b2: &[f32],
232 lr: f32,
233) -> Vec<f32> {
234 let dims = MlpDims {
235 in_dim,
236 hidden,
237 out_dim,
238 };
239 let g = grads_graph(dims);
240
241 let mut compiled = Session::new(Device::Cpu).compile(g);
242 set_params(&mut compiled, w1, b1, w2, b2);
243 let outs = compiled.run(&[("x", x), ("target", target), ("d_output", &[1.0])]);
244
245 let mut updated = Vec::new();
247 for (param, grad) in [w1, b1, w2, b2].iter().zip(&outs[1..]) {
248 updated.extend(param.iter().zip(grad.iter()).map(|(p, gd)| p - lr * gd));
249 }
250 updated
251}
252
253fn grads_graph(dims: MlpDims) -> Graph {
258 let (fwd, params) = mlp::build_loss(dims);
259 grad_with_loss(&fwd, ¶ms)
260}
261
262fn set_params(
263 compiled: &mut rlx_runtime::CompiledGraph,
264 w1: &[f32],
265 b1: &[f32],
266 w2: &[f32],
267 b2: &[f32],
268) {
269 compiled.set_param("w1", w1);
270 compiled.set_param("b1", b1);
271 compiled.set_param("w2", w2);
272 compiled.set_param("b2", b2);
273}
274
275#[cfg(all(feature = "webgpu", target_arch = "wasm32"))]
279#[wasm_bindgen]
280pub async fn init_webgpu() -> bool {
281 rlx_runtime::init_webgpu().await
282}
283
284pub use api::{
285 VisionBench, VisionModelInfo, list_vision_models, parse_backend, preferred_backend,
286 vision_model_info,
287};
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292
293 fn approx(a: f32, b: f32, tol: f32) -> bool {
294 (a - b).abs() <= tol * (1.0 + a.abs().max(b.abs()))
295 }
296
297 #[test]
298 fn mlp_forward_runs_on_cpu() {
299 let out = mlp_forward(
301 &[1.0, -2.0],
302 2,
303 2,
304 1,
305 &[1.0, 0.0, 0.0, 1.0],
306 &[0.0, 0.0],
307 &[1.0, 1.0],
308 &[0.0],
309 );
310 assert_eq!(out.len(), 1);
311 assert!((out[0] - 1.0).abs() < 1e-5, "got {out:?}");
312 }
313
314 #[test]
315 fn mlp_grads_match_finite_differences() {
316 let (in_dim, hidden, out_dim) = (2usize, 3usize, 2usize);
317 let x = vec![0.5_f32, -0.3];
318 let target = vec![0.2_f32, 0.7];
319 let w1: Vec<f32> = (0..in_dim * hidden)
322 .map(|i| 0.1 * (i as f32 + 1.0) - 0.2)
323 .collect();
324 let b1 = vec![0.05_f32, -0.1, 0.2];
325 let w2: Vec<f32> = (0..hidden * out_dim)
326 .map(|i| 0.15 * (i as f32) - 0.1)
327 .collect();
328 let b2 = vec![0.0_f32, 0.1];
329
330 let g = mlp_grads(&x, &target, in_dim, hidden, out_dim, &w1, &b1, &w2, &b2);
331 assert_eq!(g.len(), 1 + 6 + 3 + 6 + 2);
333 let analytic = &g[1..];
334
335 let mut params = [w1.clone(), b1.clone(), w2.clone(), b2.clone()];
337 let eps = 1e-3_f32;
338 let mut idx = 0;
339 for p in 0..4 {
340 for e in 0..params[p].len() {
341 let orig = params[p][e];
342 params[p][e] = orig + eps;
343 let lp = mlp_loss(
344 &x, &target, in_dim, hidden, out_dim, ¶ms[0], ¶ms[1], ¶ms[2],
345 ¶ms[3],
346 );
347 params[p][e] = orig - eps;
348 let lm = mlp_loss(
349 &x, &target, in_dim, hidden, out_dim, ¶ms[0], ¶ms[1], ¶ms[2],
350 ¶ms[3],
351 );
352 params[p][e] = orig;
353
354 let fd = (lp - lm) / (2.0 * eps);
355 assert!(
356 approx(analytic[idx], fd, 2e-2),
357 "param {p}[{e}]: analytic={} fd={}",
358 analytic[idx],
359 fd
360 );
361 idx += 1;
362 }
363 }
364 }
365
366 #[test]
367 fn train_step_decreases_loss() {
368 let (in_dim, hidden, out_dim) = (3usize, 4usize, 2usize);
369 let x = vec![0.4_f32, -0.2, 0.9];
370 let target = vec![1.0_f32, -0.5];
371 let mut w1: Vec<f32> = (0..in_dim * hidden)
372 .map(|i| 0.1 * (i as f32 % 5.0) - 0.2)
373 .collect();
374 let mut b1 = vec![0.0_f32; hidden];
375 let mut w2: Vec<f32> = (0..hidden * out_dim)
376 .map(|i| 0.05 * (i as f32) - 0.1)
377 .collect();
378 let mut b2 = vec![0.0_f32; out_dim];
379
380 let l0 = mlp_loss(&x, &target, in_dim, hidden, out_dim, &w1, &b1, &w2, &b2);
381 for _ in 0..50 {
382 let up = mlp_train_step(
383 &x, &target, in_dim, hidden, out_dim, &w1, &b1, &w2, &b2, 0.05,
384 );
385 let (a, b) = up.split_at(in_dim * hidden);
386 w1 = a.to_vec();
387 let (a, b) = b.split_at(hidden);
388 b1 = a.to_vec();
389 let (a, b) = b.split_at(hidden * out_dim);
390 w2 = a.to_vec();
391 b2 = b.to_vec();
392 }
393 let l1 = mlp_loss(&x, &target, in_dim, hidden, out_dim, &w1, &b1, &w2, &b2);
394 assert!(l1 < l0 * 0.5, "loss did not drop enough: {l0} -> {l1}");
395 }
396}