1use crate::fuse::{FusedKernel, Instr};
17use crate::verb::{ScalarDyad, ScalarMonad, Tol};
18
19pub(crate) const WORKGROUP: usize = 256;
23
24const MAX_GROUPS: usize = 1024;
27
28pub(crate) const MAP: &str = "map";
30pub(crate) const REDUCE: &str = "reduce";
32
33#[derive(Clone, Copy, Debug, PartialEq, Eq)]
39pub enum Precision {
40 F64,
41 F32,
42}
43
44impl Precision {
45 pub fn size(self) -> usize {
47 match self {
48 Precision::F64 => 8,
49 Precision::F32 => 4,
50 }
51 }
52
53 pub fn name(self) -> &'static str {
55 match self {
56 Precision::F64 => "f64",
57 Precision::F32 => "f32",
58 }
59 }
60
61 pub fn from_name(s: &str) -> Option<Precision> {
62 match s.trim().to_ascii_lowercase().as_str() {
63 "f64" | "double" => Some(Precision::F64),
64 "f32" | "single" | "float" => Some(Precision::F32),
65 _ => None,
66 }
67 }
68
69 fn ty(self) -> &'static str {
70 self.name()
71 }
72
73 fn suffix(self) -> &'static str {
75 match self {
76 Precision::F64 => "lf",
77 Precision::F32 => "f",
78 }
79 }
80}
81
82pub(crate) fn groups_for(n: usize) -> usize {
89 (n / WORKGROUP).clamp(1, MAX_GROUPS)
90}
91
92pub(crate) fn write_bytes(dst: &mut [u8], v: &[f64], p: Precision) {
100 let w = p.size();
101 for (slot, x) in dst.chunks_exact_mut(w).zip(v) {
102 match p {
103 Precision::F64 => slot.copy_from_slice(&x.to_ne_bytes()),
104 Precision::F32 => slot.copy_from_slice(&(*x as f32).to_ne_bytes()),
105 }
106 }
107}
108
109pub(crate) fn from_bytes(b: &[u8], p: Precision, n: usize) -> Vec<f64> {
111 let w = p.size();
112 (0..n)
113 .map(|i| {
114 let s = &b[i * w..(i + 1) * w];
115 match p {
116 Precision::F64 => f64::from_ne_bytes(s.try_into().expect("8 bytes")),
117 Precision::F32 => f32::from_ne_bytes(s.try_into().expect("4 bytes")) as f64,
118 }
119 })
120 .collect()
121}
122
123#[derive(Default)]
129struct Needs {
130 tol_eq: bool,
131 tol_lt: bool,
132 tol_le: bool,
133 recip: bool,
134 divj: bool,
135 residue: bool,
136}
137
138pub(crate) fn wgsl(
141 k: &FusedKernel,
142 splat: &[bool],
143 p: Precision,
144) -> Result<String, &'static str> {
145 let mut needs = Needs::default();
146 let body = chain_body(k, splat, p, &mut needs)?;
147 let reduce = match k.reduce() {
148 None => None,
149 Some(op) => Some(fold_expr(op)?),
150 };
151
152 let t = p.ty();
153 let mut s = String::new();
154 s.push_str("// generated by libjay from a fused kernel\n");
155 s.push_str("struct JayGrid { n: u32, stride: u32 };\n");
156 s.push_str("@group(0) @binding(0) var<uniform> jg : JayGrid;\n");
157 s.push_str(&format!(
158 "@group(0) @binding(1) var<storage, read_write> jay_out : array<{t}>;\n"
159 ));
160 for i in 0..splat.len() {
161 s.push_str(&format!(
162 "@group(0) @binding({}) var<storage, read> jay_in{i} : array<{t}>;\n",
163 i + 2
164 ));
165 }
166 s.push('\n');
167 s.push_str(&helpers(&needs, k.tol(), p));
168 s.push_str(&format!("fn jay_chain(i: u32) -> {t} {{\n{body}}}\n\n"));
169
170 s.push_str(&format!("@compute @workgroup_size({WORKGROUP})\n"));
171 s.push_str(&format!("fn {MAP}(@builtin(global_invocation_id) gid: vec3<u32>) {{\n"));
172 s.push_str(" let i = gid.x;\n if (i >= jg.n) { return; }\n");
173 s.push_str(" jay_out[i] = jay_chain(i);\n}\n");
174
175 if let Some(fold) = reduce {
176 s.push_str(&format!("\nvar<workgroup> lane : array<{t}, {WORKGROUP}>;\n\n"));
177 s.push_str(&format!("@compute @workgroup_size({WORKGROUP})\n"));
178 s.push_str(&format!("fn {REDUCE}(\n"));
179 s.push_str(" @builtin(global_invocation_id) gid: vec3<u32>,\n");
180 s.push_str(" @builtin(local_invocation_id) lid: vec3<u32>,\n");
181 s.push_str(" @builtin(workgroup_id) wid: vec3<u32>,\n");
182 s.push_str(") {\n");
183 s.push_str(" var acc = jay_chain(gid.x);\n");
186 s.push_str(" var i = gid.x + jg.stride;\n");
187 s.push_str(" loop {\n if (i >= jg.n) { break; }\n");
188 s.push_str(&format!(" acc = {};\n", fold("acc", "jay_chain(i)")));
189 s.push_str(" i = i + jg.stride;\n }\n");
190 s.push_str(" lane[lid.x] = acc;\n workgroupBarrier();\n");
191 s.push_str(&format!(" var s = {}u;\n", WORKGROUP / 2));
194 s.push_str(" loop {\n if (s == 0u) { break; }\n");
195 s.push_str(&format!(
196 " if (lid.x < s) {{ lane[lid.x] = {}; }}\n",
197 fold("lane[lid.x]", "lane[lid.x + s]")
198 ));
199 s.push_str(" workgroupBarrier();\n s = s >> 1u;\n }\n");
200 s.push_str(" if (lid.x == 0u) { jay_out[wid.x] = lane[0]; }\n}\n");
201 }
202 Ok(s)
203}
204
205fn chain_body(
208 k: &FusedKernel,
209 splat: &[bool],
210 p: Precision,
211 needs: &mut Needs,
212) -> Result<String, &'static str> {
213 let mut out = String::new();
214 let mut stack: Vec<String> = Vec::new();
215 let mut lets: Vec<String> = Vec::new();
216 let mut temp = 0usize;
217 for ins in k.code() {
218 match ins {
219 Instr::Load(j) => {
220 let at = if *splat.get(*j).unwrap_or(&false) { "0u" } else { "i" };
221 stack.push(format!("jay_in{j}[{at}]"));
222 }
223 Instr::Let(j) => stack.push(lets.get(*j).ok_or("let")?.clone()),
224 Instr::Store(j) => {
225 let v = stack.pop().ok_or("store")?;
226 let name = format!("l{j}");
227 out.push_str(&format!(" let {name} = {v};\n"));
228 lets.push(name);
229 }
230 Instr::Monad(op) => {
231 let a = stack.pop().ok_or("monad")?;
232 let e = monad_expr(*op, &a, p, needs)?;
233 let name = format!("t{temp}");
234 temp += 1;
235 out.push_str(&format!(" let {name} = {e};\n"));
236 stack.push(name);
237 }
238 Instr::Dyad(op) => {
239 let b = stack.pop().ok_or("dyad")?;
240 let a = stack.pop().ok_or("dyad")?;
241 let e = dyad_expr(*op, &a, &b, p, needs)?;
242 let name = format!("t{temp}");
243 temp += 1;
244 out.push_str(&format!(" let {name} = {e};\n"));
245 stack.push(name);
246 }
247 }
248 }
249 let root = stack.pop().ok_or("empty kernel")?;
250 out.push_str(&format!(" return {root};\n"));
251 Ok(out)
252}
253
254fn lit(v: f64, p: Precision) -> String {
256 let mut s = format!("{v:?}");
257 if !s.contains('.') && !s.contains('e') {
258 s.push_str(".0");
259 }
260 s.push_str(p.suffix());
261 s
262}
263
264fn monad_expr(
265 op: ScalarMonad,
266 a: &str,
267 p: Precision,
268 needs: &mut Needs,
269) -> Result<String, &'static str> {
270 use ScalarMonad::*;
271 let one = lit(1.0, p);
272 Ok(match op {
273 Conj => format!("({a})"),
274 Neg => format!("-({a})"),
275 Abs => format!("abs({a})"),
276 Signum => format!("sign({a})"),
277 Recip => {
278 needs.recip = true;
279 format!("recip({a})")
280 }
281 Floor => format!("floor({a})"),
282 Ceil => format!("ceil({a})"),
283 Inc => format!("({a}) + {one}"),
284 Dec => format!("({a}) - {one}"),
285 Double => format!("({a}) + ({a})"),
286 Halve => format!("({a}) / {}", lit(2.0, p)),
287 Square => format!("({a}) * ({a})"),
288 OneMinus => format!("{one} - ({a})"),
289 Exp if p == Precision::F32 => format!("exp({a})"),
293 Exp => return Err("^"),
294 _ => return Err("this monad"),
295 })
296}
297
298fn dyad_expr(
299 op: ScalarDyad,
300 a: &str,
301 b: &str,
302 p: Precision,
303 needs: &mut Needs,
304) -> Result<String, &'static str> {
305 use ScalarDyad::*;
306 let bool_to_num =
309 |c: String| format!("select({}, {}, {c})", lit(0.0, p), lit(1.0, p));
310 Ok(match op {
311 Add => format!("({a}) + ({b})"),
312 Sub => format!("({a}) - ({b})"),
313 Mul => format!("({a}) * ({b})"),
314 Min => format!("min({a}, {b})"),
315 Max => format!("max({a}, {b})"),
316 DivJ => {
317 needs.divj = true;
318 format!("divj({a}, {b})")
319 }
320 Residue => {
321 needs.residue = true;
322 format!("residue({a}, {b})")
323 }
324 Eq => {
325 needs.tol_eq = true;
326 bool_to_num(format!("teq({a}, {b})"))
327 }
328 Ne => {
329 needs.tol_eq = true;
330 bool_to_num(format!("!teq({a}, {b})"))
331 }
332 Lt => {
333 needs.tol_lt = true;
334 bool_to_num(format!("tlt({a}, {b})"))
335 }
336 Le => {
337 needs.tol_le = true;
338 bool_to_num(format!("tle({a}, {b})"))
339 }
340 Gt => {
341 needs.tol_lt = true;
342 bool_to_num(format!("tlt({b}, {a})"))
343 }
344 Ge => {
345 needs.tol_le = true;
346 bool_to_num(format!("tle({b}, {a})"))
347 }
348 _ => return Err("this dyad"),
349 })
350}
351
352fn fold_expr(op: ScalarDyad) -> Result<fn(&str, &str) -> String, &'static str> {
354 use ScalarDyad::*;
355 Ok(match op {
356 Add => |a: &str, b: &str| format!("{a} + {b}"),
357 Mul => |a: &str, b: &str| format!("{a} * {b}"),
358 Min => |a: &str, b: &str| format!("min({a}, {b})"),
359 Max => |a: &str, b: &str| format!("max({a}, {b})"),
360 _ => return Err("this reduction"),
361 })
362}
363
364fn helpers(needs: &Needs, tol: Tol, p: Precision) -> String {
368 let t = p.ty();
369 let zero = lit(0.0, p);
370 let one = lit(1.0, p);
371 let mut s = String::new();
372 if needs.tol_eq || needs.tol_lt || needs.tol_le {
373 let scale = if tol.by_smaller { "min" } else { "max" };
374 s.push_str(&format!("fn teq(a: {t}, b: {t}) -> bool {{\n"));
375 s.push_str(" if (a == b) { return true; }\n");
376 s.push_str(&format!(" let s = {scale}(abs(a), abs(b));\n"));
377 s.push_str(&format!(" return abs(a - b) < {} * s;\n}}\n", lit(tol.ct, p)));
378 }
379 if needs.tol_lt {
380 s.push_str(&format!(
381 "fn tlt(a: {t}, b: {t}) -> bool {{ return a < b && !teq(a, b); }}\n"
382 ));
383 }
384 if needs.tol_le {
385 s.push_str(&format!(
386 "fn tle(a: {t}, b: {t}) -> bool {{ return a <= b || teq(a, b); }}\n"
387 ));
388 }
389 if needs.recip {
390 s.push_str(&format!("fn recip(x: {t}) -> {t} {{\n"));
394 s.push_str(&format!(" if (x == {zero}) {{ return {one} / abs(x); }}\n"));
395 s.push_str(&format!(" return {one} / x;\n}}\n"));
396 }
397 if needs.divj {
398 s.push_str(&format!("fn divj(x: {t}, y: {t}) -> {t} {{\n"));
399 s.push_str(&format!(" if (y == {zero}) {{\n"));
400 s.push_str(&format!(" if (x == {zero}) {{ return {zero}; }}\n"));
401 s.push_str(" return sign(x) / abs(y);\n }\n");
402 s.push_str(" return x / y;\n}\n");
403 }
404 if needs.residue {
405 s.push_str(&format!("fn residue(x: {t}, y: {t}) -> {t} {{\n"));
406 s.push_str(&format!(" if (x == {zero}) {{ return y; }}\n"));
407 s.push_str(" return y - x * floor(y / x);\n}\n");
408 }
409 if !s.is_empty() {
410 s.push('\n');
411 }
412 s
413}
414
415#[cfg(test)]
416mod tests {
417 use super::*;
418 use crate::frontend::{compile, Dialect, Lang};
419 use crate::ir::Expr;
420
421 fn kernel(src: &str) -> FusedKernel {
423 fn find(e: &Expr) -> Option<FusedKernel> {
424 match e {
425 Expr::Fused { kernel, .. } => Some(kernel.clone()),
426 Expr::Assign { value, .. } | Expr::PrintPass { value, .. } => find(value),
427 Expr::Monad { y, .. } => find(y),
428 Expr::Dyad { x, y, .. } => find(x).or_else(|| find(y)),
429 _ => None,
430 }
431 }
432 let p = compile(Lang::J, src, &Dialect::default()).expect("compile");
433 p.stmts.iter().find_map(find).unwrap_or_else(|| panic!("{src} did not fuse"))
434 }
435
436 fn validate(src: &str, p: Precision) {
440 let module = naga::front::wgsl::parse_str(src)
441 .unwrap_or_else(|e| panic!("{}\n\n{src}", e.emit_to_string(src)));
442 let caps = match p {
443 Precision::F64 => naga::valid::Capabilities::FLOAT64,
444 Precision::F32 => naga::valid::Capabilities::empty(),
445 };
446 naga::valid::Validator::new(naga::valid::ValidationFlags::all(), caps)
447 .validate(&module)
448 .unwrap_or_else(|e| panic!("{e:?}\n\n{src}"));
449 }
450
451 const CHAINS: &[&str] = &[
452 "+/ {w} * {x}",
453 "1 + 2 * {x}",
454 "+/ ({x} - 1) * ({x} - 1)",
455 "{w} - {x} - 1",
456 "%: 1 + 2 * {x}",
457 ">./ {w} * {x}",
458 "<./ {w} + {x}",
459 "*/ 1 + {x}",
460 "+/ ({x} > 1) * {x}",
461 "+/ ({x} <: 1) * {x}",
462 "+/ (2 | {x}) * {x}",
463 "+/ ({w} % {x}) + 1",
464 "+/ (% {x}) + 1",
465 "+/ (| {x}) * -: {x}",
466 "+/ (* {x}) + >: {x}",
467 ];
468
469 #[test]
470 fn every_chain_generates_valid_f32_wgsl() {
471 for src in CHAINS {
472 let k = kernel(src);
473 let splat = vec![false; 4];
474 let s = wgsl(&k, &splat, Precision::F32).unwrap_or_else(|e| panic!("{src}: {e}"));
475 validate(&s, Precision::F32);
476 }
477 }
478
479 #[test]
480 fn every_chain_generates_valid_f64_wgsl() {
481 for src in CHAINS {
482 let k = kernel(src);
483 let splat = vec![false; 4];
484 match wgsl(&k, &splat, Precision::F64) {
485 Ok(s) => validate(&s, Precision::F64),
486 Err(op) => assert_eq!(op, "^", "{src}"),
489 }
490 }
491 }
492
493 #[test]
494 fn the_exponential_declines_in_f64_and_runs_in_f32() {
495 let k = kernel("+/ ^ {x}");
496 assert_eq!(wgsl(&k, &[false], Precision::F64), Err("^"));
497 let s = wgsl(&k, &[false], Precision::F32).expect("f32");
498 validate(&s, Precision::F32);
499 assert!(s.contains("exp("));
500 }
501
502 #[test]
503 fn a_scalar_input_is_read_at_zero() {
504 let k = kernel("+/ 2 * {x}");
505 let s = wgsl(&k, &[true, false], Precision::F32).expect("wgsl");
506 assert!(s.contains("jay_in0[0u]"), "{s}");
507 assert!(s.contains("jay_in1[i]"), "{s}");
508 }
509
510 #[test]
511 fn the_grid_never_outnumbers_the_elements() {
512 for n in [1 << 19, 1 << 20, 1 << 24, 3_000_000] {
513 assert!(groups_for(n) * WORKGROUP <= n, "{n}");
514 assert!(groups_for(n) >= 1);
515 }
516 }
517
518 #[test]
519 fn elements_survive_the_round_trip() {
520 let v = vec![1.0, -2.5, 1e300, 0.0];
521 let bytes = |p: Precision| {
522 let mut b = vec![0u8; v.len() * p.size()];
523 write_bytes(&mut b, &v, p);
524 b
525 };
526 let b = bytes(Precision::F64);
527 assert_eq!(from_bytes(&b, Precision::F64, v.len()), v);
528 let b = bytes(Precision::F32);
529 let back = from_bytes(&b, Precision::F32, v.len());
530 assert_eq!(back[0], 1.0);
531 assert_eq!(back[1], -2.5);
532 assert!(back[2].is_infinite());
533 }
534
535 #[test]
536 fn precision_names_read_back() {
537 for p in [Precision::F64, Precision::F32] {
538 assert_eq!(Precision::from_name(p.name()), Some(p));
539 }
540 assert_eq!(Precision::from_name("f16"), None);
541 }
542}