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/// Write host floats into a device buffer as its element bytes.
95///
96/// Straight into the destination: the arrays this uploads are tens of
97/// megabytes, and an intermediate `Vec` would be one more pass over all of
98/// them for nothing.
99pub(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
109/// `n` device elements as host floats.
110pub(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// ---------------------------------------------------------------- generation
124
125/// Helper functions the chain turned out to need. Emitting only these keeps
126/// a shader to what it uses, which matters on a driver that type-checks
127/// every function it is handed whether or not anything calls it.
128#[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
138/// The shader for this kernel, or the name of the operation that has no
139/// shader form.
140pub(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        // The grid never holds more threads than there are elements, so the
184        // first value needs no test and the fold needs no identity.
185        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        // A tree over the workgroup. `s` is the same for every lane, so the
192        // barrier is reached uniformly, which WGSL requires.
193        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
205/// The straight-line body of `chain`: one `let` per step of the postfix
206/// program, in the order the program performs them.
207fn 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
254/// A literal of the shader's element type.
255fn 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        // The exponential is a 32-bit builtin: SPIR-V's extended
290        // instruction set and MSL both define it for single precision only,
291        // so an f64 chain that reaches one stays on the CPU.
292        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    // A comparison is a number inside a kernel, as it is in J; the dtype of
307    // a result made from one is the caller's business.
308    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
352/// How an absorbed reduction combines two values.
353fn 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
364/// The helper functions the chain used, with the dialect's comparison
365/// tolerance compiled into them, so that a comparison on the device answers
366/// as the same comparison does anywhere else.
367fn 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        // `% 0` is infinity, as it is unfused. Dividing by the magnitude
391        // rather than by the value keeps that out of the shader compiler's
392        // constant folding, and gives -0 the same +infinity J gives it.
393        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    /// The first fused node the program holds, wherever it sits.
422    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    /// Parse and type-check generated WGSL the way a driver would, without
437    /// an adapter. The f64 path cannot be executed on a Metal machine; this
438    /// is what holds it to being valid all the same.
439    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                // The exponential has no f64 form; that is the only thing
487                // the generator is allowed to turn away here.
488                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}