Skip to main content

jay/device/
codegen.rs

1//! WGSL for a fused kernel, generated at run time.
2//!
3//! The fusion pass already reduced a chain of scalar verbs to a postfix
4//! program over a stack ([`crate::fuse::Instr`]). That program is the kernel
5//! description, and it is the only one: this module walks it and writes
6//! shader text, exactly as [`crate::fuse`]'s block executor walks it and
7//! calls block loops. Nothing here knows what J or APL primitive a step came
8//! from, and there is no per-primitive shader anywhere — adding a verb to
9//! the fusable set adds one arm to the two `expr` functions below and
10//! nothing else.
11//!
12//! Shaders are compiled by the driver when a program first runs on a
13//! device. The build produces no shader and does not know what adapters
14//! exist, which is what keeps compilation hermetic.
15
16use crate::fuse::{FusedKernel, Instr};
17use crate::verb::{ScalarDyad, ScalarMonad, Tol};
18
19/// Threads per workgroup. 256 is the size every current adapter runs at
20/// full occupancy; nothing here depends on the number beyond the workgroup
21/// array the reduction declares, which is sized from it.
22pub(crate) const WORKGROUP: usize = 256;
23
24/// Most workgroups one reduction dispatches. The partials come back to the
25/// host and are folded there, so this bounds that readback at a few kB.
26const MAX_GROUPS: usize = 1024;
27
28/// The entry point that writes one output per element.
29pub(crate) const MAP: &str = "map";
30/// The entry point that folds the mapped values, one partial per workgroup.
31pub(crate) const REDUCE: &str = "reduce";
32
33/// The type a device kernel computes in.
34///
35/// libjay's own arithmetic is f64. A device that has f64 in its shaders
36/// computes what the CPU computes; one that has not runs nothing unless the
37/// caller asks for [`Precision::F32`] in so many words.
38#[derive(Clone, Copy, Debug, PartialEq, Eq)]
39pub enum Precision {
40    F64,
41    F32,
42}
43
44impl Precision {
45    /// Bytes one element takes on the device.
46    pub fn size(self) -> usize {
47        match self {
48            Precision::F64 => 8,
49            Precision::F32 => 4,
50        }
51    }
52
53    /// The name `deploy(precision=...)` takes.
54    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    /// WGSL's suffix for a literal of this type.
74    fn suffix(self) -> &'static str {
75        match self {
76            Precision::F64 => "lf",
77            Precision::F32 => "f",
78        }
79    }
80}
81
82/// Workgroups a reduction over `n` elements dispatches.
83///
84/// Never more threads than there are elements: every thread then starts its
85/// grid-stride loop with a value of its own, so the fold needs no identity
86/// element — which for `>./` would have to be an infinity WGSL cannot
87/// spell.
88pub(crate) fn groups_for(n: usize) -> usize {
89    (n / WORKGROUP).clamp(1, MAX_GROUPS)
90}
91
92// ------------------------------------------------------------------ buffers
93
94/// Host floats as the element bytes a device buffer holds them in.
95///
96/// An iterator rather than a filled buffer: mapped device memory is written
97/// through a write-only reference that hands out one slot at a time, and the
98/// arrays this uploads are tens of megabytes, so an intermediate `Vec` would
99/// be one more pass over all of them for nothing.
100pub(crate) fn byte_iter(v: &[f64], p: Precision) -> impl Iterator<Item = u8> {
101    v.iter().flat_map(move |&x| {
102        let mut b = [0u8; 8];
103        match p {
104            Precision::F64 => b.copy_from_slice(&x.to_ne_bytes()),
105            Precision::F32 => b[..4].copy_from_slice(&(x as f32).to_ne_bytes()),
106        }
107        b.into_iter().take(p.size())
108    })
109}
110
111/// `n` device elements as host floats.
112pub(crate) fn from_bytes(b: &[u8], p: Precision, n: usize) -> Vec<f64> {
113    let w = p.size();
114    (0..n)
115        .map(|i| {
116            let s = &b[i * w..(i + 1) * w];
117            match p {
118                Precision::F64 => f64::from_ne_bytes(s.try_into().expect("8 bytes")),
119                Precision::F32 => f32::from_ne_bytes(s.try_into().expect("4 bytes")) as f64,
120            }
121        })
122        .collect()
123}
124
125// ---------------------------------------------------------------- generation
126
127/// Helper functions the chain turned out to need. Emitting only these keeps
128/// a shader to what it uses, which matters on a driver that type-checks
129/// every function it is handed whether or not anything calls it.
130#[derive(Default)]
131struct Needs {
132    tol_eq: bool,
133    tol_lt: bool,
134    tol_le: bool,
135    recip: bool,
136    divj: bool,
137    residue: bool,
138}
139
140/// The shader for this kernel, or the name of the operation that has no
141/// shader form.
142pub(crate) fn wgsl(
143    k: &FusedKernel,
144    splat: &[bool],
145    p: Precision,
146) -> Result<String, &'static str> {
147    let mut needs = Needs::default();
148    let body = chain_body(k, splat, p, &mut needs)?;
149    let reduce = match k.reduce() {
150        None => None,
151        Some(op) => Some(fold_expr(op)?),
152    };
153
154    let t = p.ty();
155    let mut s = String::new();
156    s.push_str("// generated by libjay from a fused kernel\n");
157    s.push_str("struct JayGrid { n: u32, stride: u32 };\n");
158    s.push_str("@group(0) @binding(0) var<uniform> jg : JayGrid;\n");
159    s.push_str(&format!(
160        "@group(0) @binding(1) var<storage, read_write> jay_out : array<{t}>;\n"
161    ));
162    for i in 0..splat.len() {
163        s.push_str(&format!(
164            "@group(0) @binding({}) var<storage, read> jay_in{i} : array<{t}>;\n",
165            i + 2
166        ));
167    }
168    s.push('\n');
169    s.push_str(&helpers(&needs, k.tol(), p));
170    s.push_str(&format!("fn jay_chain(i: u32) -> {t} {{\n{body}}}\n\n"));
171
172    s.push_str(&format!("@compute @workgroup_size({WORKGROUP})\n"));
173    s.push_str(&format!("fn {MAP}(@builtin(global_invocation_id) gid: vec3<u32>) {{\n"));
174    s.push_str("  let i = gid.x;\n  if (i >= jg.n) { return; }\n");
175    s.push_str("  jay_out[i] = jay_chain(i);\n}\n");
176
177    if let Some(fold) = reduce {
178        s.push_str(&format!("\nvar<workgroup> lane : array<{t}, {WORKGROUP}>;\n\n"));
179        s.push_str(&format!("@compute @workgroup_size({WORKGROUP})\n"));
180        s.push_str(&format!("fn {REDUCE}(\n"));
181        s.push_str("  @builtin(global_invocation_id) gid: vec3<u32>,\n");
182        s.push_str("  @builtin(local_invocation_id) lid: vec3<u32>,\n");
183        s.push_str("  @builtin(workgroup_id) wid: vec3<u32>,\n");
184        s.push_str(") {\n");
185        // The grid never holds more threads than there are elements, so the
186        // first value needs no test and the fold needs no identity.
187        s.push_str("  var acc = jay_chain(gid.x);\n");
188        s.push_str("  var i = gid.x + jg.stride;\n");
189        s.push_str("  loop {\n    if (i >= jg.n) { break; }\n");
190        s.push_str(&format!("    acc = {};\n", fold("acc", "jay_chain(i)")));
191        s.push_str("    i = i + jg.stride;\n  }\n");
192        s.push_str("  lane[lid.x] = acc;\n  workgroupBarrier();\n");
193        // A tree over the workgroup. `s` is the same for every lane, so the
194        // barrier is reached uniformly, which WGSL requires.
195        s.push_str(&format!("  var s = {}u;\n", WORKGROUP / 2));
196        s.push_str("  loop {\n    if (s == 0u) { break; }\n");
197        s.push_str(&format!(
198            "    if (lid.x < s) {{ lane[lid.x] = {}; }}\n",
199            fold("lane[lid.x]", "lane[lid.x + s]")
200        ));
201        s.push_str("    workgroupBarrier();\n    s = s >> 1u;\n  }\n");
202        s.push_str("  if (lid.x == 0u) { jay_out[wid.x] = lane[0]; }\n}\n");
203    }
204    Ok(s)
205}
206
207/// The straight-line body of `chain`: one `let` per step of the postfix
208/// program, in the order the program performs them.
209fn chain_body(
210    k: &FusedKernel,
211    splat: &[bool],
212    p: Precision,
213    needs: &mut Needs,
214) -> Result<String, &'static str> {
215    let mut out = String::new();
216    let mut stack: Vec<String> = Vec::new();
217    let mut lets: Vec<String> = Vec::new();
218    let mut temp = 0usize;
219    for ins in k.code() {
220        match ins {
221            Instr::Load(j) => {
222                let at = if *splat.get(*j).unwrap_or(&false) { "0u" } else { "i" };
223                stack.push(format!("jay_in{j}[{at}]"));
224            }
225            Instr::Let(j) => stack.push(lets.get(*j).ok_or("let")?.clone()),
226            Instr::Store(j) => {
227                let v = stack.pop().ok_or("store")?;
228                let name = format!("l{j}");
229                out.push_str(&format!("  let {name} = {v};\n"));
230                lets.push(name);
231            }
232            Instr::Monad(op) => {
233                let a = stack.pop().ok_or("monad")?;
234                let e = monad_expr(*op, &a, p, needs)?;
235                let name = format!("t{temp}");
236                temp += 1;
237                out.push_str(&format!("  let {name} = {e};\n"));
238                stack.push(name);
239            }
240            // A window step and a running fold read items the shader's own
241            // element does not: they stay on the CPU.
242            Instr::Window(..) => return Err("a moving window"),
243            Instr::Scan(_) => return Err("a running fold"),
244            Instr::Dyad(op) => {
245                let b = stack.pop().ok_or("dyad")?;
246                let a = stack.pop().ok_or("dyad")?;
247                let e = dyad_expr(*op, &a, &b, p, needs)?;
248                let name = format!("t{temp}");
249                temp += 1;
250                out.push_str(&format!("  let {name} = {e};\n"));
251                stack.push(name);
252            }
253        }
254    }
255    let root = stack.pop().ok_or("empty kernel")?;
256    out.push_str(&format!("  return {root};\n"));
257    Ok(out)
258}
259
260/// A literal of the shader's element type.
261fn lit(v: f64, p: Precision) -> String {
262    let mut s = format!("{v:?}");
263    if !s.contains('.') && !s.contains('e') {
264        s.push_str(".0");
265    }
266    s.push_str(p.suffix());
267    s
268}
269
270fn monad_expr(
271    op: ScalarMonad,
272    a: &str,
273    p: Precision,
274    needs: &mut Needs,
275) -> Result<String, &'static str> {
276    use ScalarMonad::*;
277    let one = lit(1.0, p);
278    Ok(match op {
279        Conj => format!("({a})"),
280        Neg => format!("-({a})"),
281        Abs => format!("abs({a})"),
282        Signum => format!("sign({a})"),
283        Recip => {
284            needs.recip = true;
285            format!("recip({a})")
286        }
287        Floor => format!("floor({a})"),
288        Ceil => format!("ceil({a})"),
289        Inc => format!("({a}) + {one}"),
290        Dec => format!("({a}) - {one}"),
291        Double => format!("({a}) + ({a})"),
292        Halve => format!("({a}) / {}", lit(2.0, p)),
293        Square => format!("({a}) * ({a})"),
294        OneMinus => format!("{one} - ({a})"),
295        // The exponential is a 32-bit builtin: SPIR-V's extended
296        // instruction set and MSL both define it for single precision only,
297        // so an f64 chain that reaches one stays on the CPU.
298        Exp if p == Precision::F32 => format!("exp({a})"),
299        Exp => return Err("^"),
300        _ => return Err("this monad"),
301    })
302}
303
304fn dyad_expr(
305    op: ScalarDyad,
306    a: &str,
307    b: &str,
308    p: Precision,
309    needs: &mut Needs,
310) -> Result<String, &'static str> {
311    use ScalarDyad::*;
312    // A comparison is a number inside a kernel, as it is in J; the dtype of
313    // a result made from one is the caller's business.
314    let bool_to_num =
315        |c: String| format!("select({}, {}, {c})", lit(0.0, p), lit(1.0, p));
316    Ok(match op {
317        Add => format!("({a}) + ({b})"),
318        Sub => format!("({a}) - ({b})"),
319        Mul => format!("({a}) * ({b})"),
320        Min => format!("min({a}, {b})"),
321        Max => format!("max({a}, {b})"),
322        DivJ => {
323            needs.divj = true;
324            format!("divj({a}, {b})")
325        }
326        Residue => {
327            needs.residue = true;
328            format!("residue({a}, {b})")
329        }
330        Eq => {
331            needs.tol_eq = true;
332            bool_to_num(format!("teq({a}, {b})"))
333        }
334        Ne => {
335            needs.tol_eq = true;
336            bool_to_num(format!("!teq({a}, {b})"))
337        }
338        Lt => {
339            needs.tol_lt = true;
340            bool_to_num(format!("tlt({a}, {b})"))
341        }
342        Le => {
343            needs.tol_le = true;
344            bool_to_num(format!("tle({a}, {b})"))
345        }
346        Gt => {
347            needs.tol_lt = true;
348            bool_to_num(format!("tlt({b}, {a})"))
349        }
350        Ge => {
351            needs.tol_le = true;
352            bool_to_num(format!("tle({b}, {a})"))
353        }
354        _ => return Err("this dyad"),
355    })
356}
357
358/// How an absorbed reduction combines two values.
359fn fold_expr(op: ScalarDyad) -> Result<fn(&str, &str) -> String, &'static str> {
360    use ScalarDyad::*;
361    Ok(match op {
362        Add => |a: &str, b: &str| format!("{a} + {b}"),
363        Mul => |a: &str, b: &str| format!("{a} * {b}"),
364        Min => |a: &str, b: &str| format!("min({a}, {b})"),
365        Max => |a: &str, b: &str| format!("max({a}, {b})"),
366        _ => return Err("this reduction"),
367    })
368}
369
370/// The helper functions the chain used, with the dialect's comparison
371/// tolerance compiled into them, so that a comparison on the device answers
372/// as the same comparison does anywhere else.
373fn helpers(needs: &Needs, tol: Tol, p: Precision) -> String {
374    let t = p.ty();
375    let zero = lit(0.0, p);
376    let one = lit(1.0, p);
377    let mut s = String::new();
378    if needs.tol_eq || needs.tol_lt || needs.tol_le {
379        let scale = if tol.by_smaller { "min" } else { "max" };
380        s.push_str(&format!("fn teq(a: {t}, b: {t}) -> bool {{\n"));
381        s.push_str("  if (a == b) { return true; }\n");
382        s.push_str(&format!("  let s = {scale}(abs(a), abs(b));\n"));
383        s.push_str(&format!("  return abs(a - b) < {} * s;\n}}\n", lit(tol.ct, p)));
384    }
385    if needs.tol_lt {
386        s.push_str(&format!(
387            "fn tlt(a: {t}, b: {t}) -> bool {{ return a < b && !teq(a, b); }}\n"
388        ));
389    }
390    if needs.tol_le {
391        s.push_str(&format!(
392            "fn tle(a: {t}, b: {t}) -> bool {{ return a <= b || teq(a, b); }}\n"
393        ));
394    }
395    if needs.recip {
396        // `% 0` is infinity, as it is unfused. Dividing by the magnitude
397        // rather than by the value keeps that out of the shader compiler's
398        // constant folding, and gives -0 the same +infinity J gives it.
399        s.push_str(&format!("fn recip(x: {t}) -> {t} {{\n"));
400        s.push_str(&format!("  if (x == {zero}) {{ return {one} / abs(x); }}\n"));
401        s.push_str(&format!("  return {one} / x;\n}}\n"));
402    }
403    if needs.divj {
404        s.push_str(&format!("fn divj(x: {t}, y: {t}) -> {t} {{\n"));
405        s.push_str(&format!("  if (y == {zero}) {{\n"));
406        s.push_str(&format!("    if (x == {zero}) {{ return {zero}; }}\n"));
407        s.push_str("    return sign(x) / abs(y);\n  }\n");
408        s.push_str("  return x / y;\n}\n");
409    }
410    if needs.residue {
411        s.push_str(&format!("fn residue(x: {t}, y: {t}) -> {t} {{\n"));
412        s.push_str(&format!("  if (x == {zero}) {{ return y; }}\n"));
413        s.push_str("  return y - x * floor(y / x);\n}\n");
414    }
415    if !s.is_empty() {
416        s.push('\n');
417    }
418    s
419}
420
421#[cfg(test)]
422mod tests {
423    use super::*;
424    use crate::frontend::{compile, Dialect, Lang};
425    use crate::ir::Expr;
426
427    /// The first fused node the program holds, wherever it sits.
428    fn kernel(src: &str) -> FusedKernel {
429        fn find(e: &Expr) -> Option<FusedKernel> {
430            match e {
431                Expr::Fused { kernel, .. } => Some(kernel.clone()),
432                Expr::Assign { value, .. } | Expr::PrintPass { value, .. } => find(value),
433                Expr::Monad { y, .. } => find(y),
434                Expr::Dyad { x, y, .. } => find(x).or_else(|| find(y)),
435                _ => None,
436            }
437        }
438        let p = compile(Lang::J, src, &Dialect::default()).expect("compile");
439        p.stmts.iter().find_map(find).unwrap_or_else(|| panic!("{src} did not fuse"))
440    }
441
442    /// Parse and type-check generated WGSL the way a driver would, without
443    /// an adapter. The f64 path cannot be executed on a Metal machine; this
444    /// is what holds it to being valid all the same.
445    fn validate(src: &str, p: Precision) {
446        let module = naga::front::wgsl::parse_str(src)
447            .unwrap_or_else(|e| panic!("{}\n\n{src}", e.emit_to_string(src)));
448        let caps = match p {
449            Precision::F64 => naga::valid::Capabilities::FLOAT64,
450            Precision::F32 => naga::valid::Capabilities::empty(),
451        };
452        naga::valid::Validator::new(naga::valid::ValidationFlags::all(), caps)
453            .validate(&module)
454            .unwrap_or_else(|e| panic!("{e:?}\n\n{src}"));
455    }
456
457    const CHAINS: &[&str] = &[
458        "+/ {w} * {x}",
459        "1 + 2 * {x}",
460        "+/ ({x} - 1) * ({x} - 1)",
461        "{w} - {x} - 1",
462        "%: 1 + 2 * {x}",
463        ">./ {w} * {x}",
464        "<./ {w} + {x}",
465        "*/ 1 + {x}",
466        "+/ ({x} > 1) * {x}",
467        "+/ ({x} <: 1) * {x}",
468        "+/ (2 | {x}) * {x}",
469        "+/ ({w} % {x}) + 1",
470        "+/ (% {x}) + 1",
471        "+/ (| {x}) * -: {x}",
472        "+/ (* {x}) + >: {x}",
473    ];
474
475    #[test]
476    fn every_chain_generates_valid_f32_wgsl() {
477        for src in CHAINS {
478            let k = kernel(src);
479            let splat = vec![false; 4];
480            let s = wgsl(&k, &splat, Precision::F32).unwrap_or_else(|e| panic!("{src}: {e}"));
481            validate(&s, Precision::F32);
482        }
483    }
484
485    #[test]
486    fn every_chain_generates_valid_f64_wgsl() {
487        for src in CHAINS {
488            let k = kernel(src);
489            let splat = vec![false; 4];
490            match wgsl(&k, &splat, Precision::F64) {
491                Ok(s) => validate(&s, Precision::F64),
492                // The exponential has no f64 form; that is the only thing
493                // the generator is allowed to turn away here.
494                Err(op) => assert_eq!(op, "^", "{src}"),
495            }
496        }
497    }
498
499    #[test]
500    fn the_exponential_declines_in_f64_and_runs_in_f32() {
501        let k = kernel("+/ ^ {x}");
502        assert_eq!(wgsl(&k, &[false], Precision::F64), Err("^"));
503        let s = wgsl(&k, &[false], Precision::F32).expect("f32");
504        validate(&s, Precision::F32);
505        assert!(s.contains("exp("));
506    }
507
508    #[test]
509    fn a_scalar_input_is_read_at_zero() {
510        let k = kernel("+/ 2 * {x}");
511        let s = wgsl(&k, &[true, false], Precision::F32).expect("wgsl");
512        assert!(s.contains("jay_in0[0u]"), "{s}");
513        assert!(s.contains("jay_in1[i]"), "{s}");
514    }
515
516    #[test]
517    fn the_grid_never_outnumbers_the_elements() {
518        for n in [1 << 19, 1 << 20, 1 << 24, 3_000_000] {
519            assert!(groups_for(n) * WORKGROUP <= n, "{n}");
520            assert!(groups_for(n) >= 1);
521        }
522    }
523
524    #[test]
525    fn elements_survive_the_round_trip() {
526        let v = vec![1.0, -2.5, 1e300, 0.0];
527        let bytes = |p: Precision| byte_iter(&v, p).collect::<Vec<u8>>();
528        let b = bytes(Precision::F64);
529        assert_eq!(from_bytes(&b, Precision::F64, v.len()), v);
530        let b = bytes(Precision::F32);
531        let back = from_bytes(&b, Precision::F32, v.len());
532        assert_eq!(back[0], 1.0);
533        assert_eq!(back[1], -2.5);
534        assert!(back[2].is_infinite());
535    }
536
537    #[test]
538    fn precision_names_read_back() {
539        for p in [Precision::F64, Precision::F32] {
540            assert_eq!(Precision::from_name(p.name()), Some(p));
541        }
542        assert_eq!(Precision::from_name("f16"), None);
543    }
544}