Skip to main content

logicaffeine_compile/spatial/
int8_model.rs

1//! An int8 transformer as a LOGOS decode step: one token in, its logits and the next token out, the KV cache carried.
2//!
3//! The model is integer arithmetic throughout — int8 weights, a requant `(acc · M) >> S` with int8 saturation between
4//! layers, an exp-table softmax whose context is a floored weighted mean, a ReLU FFN, a tied head and an arg-max — so
5//! the step is written in the integer tensor sugar (`X times M shifted right by S`, `X clamped between LO and HI`,
6//! `relu of X`, `lut attention of …`) beside the shared forms (`row tok of E`, `matmul`, `plus`, `Set row`,
7//! `argmax of`). The parser desugars every one into ordinary loops, so the VM's run of the step IS its
8//! meaning, and the spatial compiler lowers the same text onto the integer fabric.
9
10use std::collections::BTreeMap;
11
12use super::tensor_parallel::{SliceAxis, TpCollective, TpManifest, TpSlice};
13
14/// A one-layer int8 transformer: its shapes, its requant pairs, its tables.
15#[derive(Clone, Debug)]
16pub struct Int8Model {
17    pub d: usize,
18    pub heads: usize,
19    pub d_ff: usize,
20    pub vocab: usize,
21    /// Positions the cache holds — the positional table's rows.
22    pub ctx: usize,
23    /// Each requant's `(M, S)`, by its name in the model (`r_qkv`, `M_q`, `M_score`, …).
24    pub requant: BTreeMap<String, (i64, u32)>,
25    /// The int8 weights, row-major, by name: `Wq Wk Wv Wo` (`d × d`), `W1` (`d × d_ff`), `W2` (`d_ff × d`), `Eq`
26    /// (`vocab × d`, the tied embedding the head multiplies by its transpose).
27    pub weights: BTreeMap<String, Vec<i64>>,
28    /// The token embedding as the stream reads it (`vocab × d`).
29    pub embed: Vec<i64>,
30    /// The positional embedding (`ctx × d`).
31    pub positions: Vec<i64>,
32    /// The softmax's exp table, indexed by the requanted distance from the row's maximum.
33    pub exp_lut: Vec<i64>,
34}
35
36const REQUANTS: [&str; 12] = [
37    "r_qkv", "r_ffn", "r_out", "r_ctx", "r_hid", "M_q", "M_k", "M_v", "M_o", "M_h", "M_f", "M_score",
38];
39const WEIGHTS: [&str; 7] = ["Wq", "Wk", "Wv", "Wo", "W1", "W2", "Eq"];
40
41impl Int8Model {
42    /// The model `scripts/train_tiny_transformer.py` exports (`benchmarks/silicon/tiny_transformer.json`).
43    pub fn from_json(j: &serde_json::Value) -> Result<Int8Model, String> {
44        let size = |k: &str| -> Result<usize, String> {
45            j["config"][k]
46                .as_u64()
47                .map(|v| v as usize)
48                .ok_or_else(|| format!("config.{k} is not a size"))
49        };
50        let ints = |v: &serde_json::Value, what: &str| -> Result<Vec<i64>, String> {
51            v.as_array()
52                .ok_or_else(|| format!("{what} is not an array"))?
53                .iter()
54                .map(|x| x.as_i64().ok_or_else(|| format!("{what} holds a non-integer")))
55                .collect()
56        };
57        let (d, heads, d_ff, vocab, ctx) = (
58            size("d_model")?,
59            size("n_heads")?,
60            size("d_ff")?,
61            size("vocab")?,
62            size("seq_len")?,
63        );
64        if heads == 0 || d % heads != 0 {
65            return Err(format!("{heads} heads do not divide d_model {d}"));
66        }
67        let mut requant = BTreeMap::new();
68        for k in REQUANTS {
69            let p = ints(&j["requant"][k], &format!("requant.{k}"))?;
70            let [m, s] = p[..] else {
71                return Err(format!("requant.{k} is not an (M, S) pair"));
72            };
73            let s = u32::try_from(s)
74                .ok()
75                .filter(|s| *s <= 63)
76                .ok_or_else(|| format!("requant.{k}'s shift {s} is outside 0..=63"))?;
77            requant.insert(k.to_string(), (m, s));
78        }
79        let mut weights = BTreeMap::new();
80        for (k, len) in WEIGHTS.into_iter().zip([d * d, d * d, d * d, d * d, d * d_ff, d_ff * d, vocab * d]) {
81            let w = ints(&j["weights_int8"][k], &format!("weights_int8.{k}"))?;
82            if w.len() != len {
83                return Err(format!("weights_int8.{k} holds {} values, not {len}", w.len()));
84            }
85            weights.insert(k.to_string(), w);
86        }
87        let embed = ints(&j["embed_stream"]["EinA"], "embed_stream.EinA")?;
88        let positions = ints(&j["embed_stream"]["PinA"], "embed_stream.PinA")?;
89        if embed.len() != vocab * d || positions.len() != ctx * d {
90            return Err("the embedding tables are not vocab × d and seq_len × d".into());
91        }
92        let exp_lut = ints(&j["exp_lut"], "exp_lut")?;
93        if exp_lut.is_empty() {
94            return Err("an empty exp table".into());
95        }
96        Ok(Int8Model {
97            d,
98            heads,
99            d_ff,
100            vocab,
101            ctx,
102            requant,
103            weights,
104            embed,
105            positions,
106            exp_lut,
107        })
108    }
109
110    /// What a decode step's host reads, besides the caches the step carries: `next`, the token it feeds back as the next
111    /// step's `tok`. A step lowered returning these ([`crate::spatial::pipeline::compile_program_to_spatial_returning`])
112    /// forwards every other value only its next statement reads.
113    pub fn step_reads() -> std::collections::BTreeSet<String> {
114        ["next".to_string()].into()
115    }
116
117    /// The hand-written CUDA reference of the decode step: one warp generating every token of the trace for `prompt` on the
118    /// device in one launch, the `generated` tokens past the prompt printed (`GEN i token`), then the whole trace repeated
119    /// (its first argument's count, 2,000 by default) between two events and the time per token printed (`US_PER_TOKEN`).
120    /// The GPU's best for this model: the kernel written for the part by hand.
121    pub fn reference_cuda(&self, prompt: &[i64], generated: usize) -> Result<String, String> {
122        let (d, h, dff, v, ctx) = (self.d, self.heads, self.d_ff, self.vocab, self.ctx);
123        if !(d <= 32 && dff <= 32 && v <= 32 && ctx <= 32) {
124            return Err(format!("one warp holds every row: d {d}, d_ff {dff}, vocab {v} and ctx {ctx} are each at most 32"));
125        }
126        let table = |name: &str, v: &[i64]| format!("__constant__ int {name}[{}] = {{{}}};\n", v.len().max(1), v.iter().map(|x| x.to_string()).collect::<Vec<_>>().join(","));
127        let rq = |k: &str| -> String {
128            let (mm, s) = self.requant[k];
129            format!("{mm}LL, {s}")
130        };
131        let eq = &self.weights["Eq"];
132        let wout: Vec<i64> = (0..d * v).map(|i| eq[(i % v) * d + i / v]).collect();
133        let mut cu = String::from("#include <cstdio>\n#include <cstdlib>\n");
134        cu += &format!(
135            "#define D {d}\n#define H {h}\n#define HD {}\n#define DFF {dff}\n#define V {v}\n#define CTX {ctx}\n#define NLUT {}\n",
136            d / h,
137            self.exp_lut.len()
138        );
139        for (k, name) in [
140            ("r_qkv", "R_QKV"),
141            ("M_q", "M_Q"),
142            ("M_k", "M_K"),
143            ("M_v", "M_V"),
144            ("M_score", "M_SCORE"),
145            ("r_ctx", "R_CTX"),
146            ("M_o", "M_O"),
147            ("r_ffn", "R_FFN"),
148            ("M_h", "M_H"),
149            ("r_hid", "R_HID"),
150            ("M_f", "M_F"),
151            ("r_out", "R_OUT"),
152        ] {
153            cu += &format!("#define {name} {}\n", rq(k));
154        }
155        cu += &format!(
156            "#define NP {}\n#define TOTAL {}\n#define PROMPT {}\n",
157            prompt.len(),
158            prompt.len() + generated - 1,
159            prompt.iter().map(|t| t.to_string()).collect::<Vec<_>>().join(",")
160        );
161        for (name, vals) in [
162            ("EinA", &self.embed),
163            ("PinA", &self.positions),
164            ("Wq", &self.weights["Wq"]),
165            ("Wk", &self.weights["Wk"]),
166            ("Wv", &self.weights["Wv"]),
167            ("Wo", &self.weights["Wo"]),
168            ("W1", &self.weights["W1"]),
169            ("W2", &self.weights["W2"]),
170            ("E", &self.exp_lut),
171            ("Wout", &wout),
172        ] {
173            cu += &table(name, vals);
174        }
175        cu += r#"
176__device__ __forceinline__ long long rq(long long x, long long mm, int s) { return (x * mm) >> s; }
177__device__ __forceinline__ long long c8(long long x) { return x < -128 ? -128 : (x > 127 ? 127 : x); }
178__device__ __forceinline__ long long fdiv(long long a, long long b) { long long q = a / b; if ((a % b != 0) && ((a < 0) != (b < 0))) q -= 1; return q; }
179__device__ __forceinline__ long long mv(const long long* in, const int* W, int n, int cols, int lane) {
180  long long s = 0; if (lane < cols) for (int e = 0; e < n; ++e) s += in[e] * (long long)W[e * cols + lane]; return s;
181}
182__global__ void decode(const int* prompt, int np, int total, int* out, int reps) {
183  __shared__ long long K[CTX][D], Vc[CTX][D], x[D], xi[D], q[D], ci[D], hi[DFF], fi[DFF], oi[D], s1[D];
184  __shared__ int nextv;
185  const int lane = threadIdx.x;
186  for (int rep = 0; rep < reps; ++rep) {
187    int tok = prompt[0];
188    for (int step = 0; step < total; ++step) {
189      const int pos = step;
190      if (lane < D) { x[lane] = (long long)EinA[tok * D + lane] + PinA[pos * D + lane]; xi[lane] = c8(rq(x[lane], R_QKV)); }
191      __syncwarp();
192      long long qa = mv(xi, Wq, D, D, lane), ka = mv(xi, Wk, D, D, lane), va = mv(xi, Wv, D, D, lane);
193      if (lane < D) { q[lane] = c8(rq(qa, M_Q)); K[pos][lane] = c8(rq(ka, M_K)); Vc[pos][lane] = c8(rq(va, M_V)); }
194      __syncwarp();
195      for (int g = 0; g < H; ++g) {
196        long long s = 0;
197        if (lane <= pos) for (int e = 0; e < HD; ++e) s += q[g * HD + e] * K[lane][g * HD + e];
198        long long mx = (lane <= pos) ? s : (long long)-0x7fffffffffffffffLL;
199        for (int o = 16; o > 0; o >>= 1) { long long t = __shfl_xor_sync(0xffffffffu, mx, o); mx = t > mx ? t : mx; }
200        long long w = 0;
201        if (lane <= pos) { long long idx = rq(mx - s, M_SCORE); idx = idx < 0 ? 0 : (idx > NLUT - 1 ? NLUT - 1 : idx); w = E[idx]; }
202        long long den = w;
203        for (int o = 16; o > 0; o >>= 1) den += __shfl_xor_sync(0xffffffffu, den, o);
204        for (int e = 0; e < HD; ++e) {
205          long long num = (lane <= pos) ? w * Vc[lane][g * HD + e] : 0;
206          for (int o = 16; o > 0; o >>= 1) num += __shfl_xor_sync(0xffffffffu, num, o);
207          if (lane == 0) ci[g * HD + e] = c8(rq(fdiv(num, den), R_CTX));
208        }
209      }
210      __syncwarp();
211      long long oa = mv(ci, Wo, D, D, lane);
212      if (lane < D) { s1[lane] = x[lane] + rq(oa, M_O); hi[lane] = c8(rq(s1[lane], R_FFN)); }
213      __syncwarp();
214      long long ha = mv(hi, W1, D, DFF, lane);
215      if (lane < DFF) { long long r = rq(ha, M_H); r = r < 0 ? 0 : r; fi[lane] = c8(rq(r, R_HID)); }
216      __syncwarp();
217      long long fa = mv(fi, W2, DFF, D, lane);
218      if (lane < D) oi[lane] = c8(rq(s1[lane] + rq(fa, M_F), R_OUT));
219      __syncwarp();
220      long long lg = mv(oi, Wout, D, V, lane);
221      long long best = lane < V ? lg : (long long)-0x7fffffffffffffffLL; int bi = lane;
222      for (int o = 16; o > 0; o >>= 1) {
223        long long t = __shfl_xor_sync(0xffffffffu, best, o); int ti = __shfl_xor_sync(0xffffffffu, bi, o);
224        if (t > best || (t == best && ti < bi)) { best = t; bi = ti; }
225      }
226      if (lane == 0) nextv = bi;
227      __syncwarp();
228      if (step + 1 >= np && rep == 0 && lane == 0) out[step + 1 - np] = nextv;
229      tok = (step + 1 < np) ? prompt[step + 1] : nextv;
230    }
231  }
232}
233int main(int argc, char** argv) {
234  const int np = NP, total = TOTAL, ng = total + 1 - np;
235  int hp[NP] = {PROMPT};
236  int *dp, *dout; cudaMalloc(&dp, sizeof(hp)); cudaMalloc(&dout, sizeof(int) * ng);
237  cudaMemcpy(dp, hp, sizeof(hp), cudaMemcpyHostToDevice);
238  decode<<<1, 32>>>(dp, np, total, dout, 1);
239  int* got = (int*)malloc(sizeof(int) * ng); cudaMemcpy(got, dout, sizeof(int) * ng, cudaMemcpyDeviceToHost);
240  for (int i = 0; i < ng; ++i) printf("GEN %d %d\n", i, got[i]);
241  const int reps = (argc > 1) ? atoi(argv[1]) : 2000;
242  cudaEvent_t a, b; cudaEventCreate(&a); cudaEventCreate(&b);
243  decode<<<1, 32>>>(dp, np, total, dout, 10); cudaDeviceSynchronize();
244  cudaEventRecord(a); decode<<<1, 32>>>(dp, np, total, dout, reps); cudaEventRecord(b); cudaEventSynchronize(b);
245  float ms; cudaEventElapsedTime(&ms, a, b);
246  printf("US_PER_TOKEN %.3f\n", ms * 1000.0 / (reps * (double)total));
247  return 0;
248}
249"#    ;
250        Ok(cu)
251    }
252
253    /// The next token, 0-based, each step over `prompt` predicts on `target`: the decode step at each position lowered
254    /// and run on the spatial evaluator, the cache each step writes carried into the next. What a board stepped through
255    /// the prompt replies, and the token after the prompt's last is the trainer's first generated one.
256    pub fn prompt_next_tokens(&self, prompt: &[usize], target: &super::target::SpatialTarget) -> Result<Vec<usize>, String> {
257        use super::eval::{eval_choreography, Inputs, Value};
258        let (mut kc, mut vc, mut next) = (Vec::new(), Vec::new(), Vec::new());
259        for (pos, tok) in prompt.iter().enumerate() {
260            let lowered = super::pipeline::compile_program_to_spatial(&self.decode_step_lg(*tok, pos, &kc, &vc), target)
261                .map_err(|e| format!("the step at position {pos}: {e:?}"))?;
262            let p = lowered
263                .program
264                .ok_or_else(|| format!("the step at position {pos} does not lower whole: {:?}", lowered.missing.first()))?;
265            let out = eval_choreography(&p, &Inputs::from_fills(&p)?).map_err(|e| format!("the step at position {pos}: {e:?}"))?;
266            let read = |name: &str| -> Result<Vec<i64>, String> {
267                let t = p.tensors.iter().find(|t| t.name == name).ok_or_else(|| format!("the step has no `{name}`"))?;
268                match &out.tensors[&t.id] {
269                    Value::Int(v) => Ok(v.clone()),
270                    Value::Float(_) => Err(format!("`{name}` evaluated through floats")),
271                }
272            };
273            kc = read("Kc")?[..(pos + 1) * self.d].to_vec();
274            vc = read("Vc")?[..(pos + 1) * self.d].to_vec();
275            let n = read("next")?[0];
276            next.push(usize::try_from(n - 1).map_err(|_| format!("the step at position {pos} predicts token {n}"))?);
277        }
278        Ok(next)
279    }
280
281    /// The decode step for token `tok` at position `pos` (both 0-based), the cache holding the key and value rows of
282    /// positions `0..pos` (`pos · d` values each, row-major). The step binds `logits` (`1 × vocab`) and `next`, the
283    /// 1-based index of the first maximal logit, and writes this token's rows into `Kc` and `Vc` as row `pos + 1`.
284    pub fn decode_step_lg(&self, tok: usize, pos: usize, kc: &[i64], vc: &[i64]) -> String {
285        let (d, h, dff, v) = (self.d, self.heads, self.d_ff, self.vocab);
286        let mut s = String::from("## Main\n");
287        s.push_str(&format!("Let tok be {}.\nLet pos be {}.\n", tok + 1, pos + 1));
288        table_lg(&mut s, "EinA", v, d, &self.embed);
289        table_lg(&mut s, "PinA", self.ctx, d, &self.positions);
290        for w in ["Wq", "Wk", "Wv", "Wo"] {
291            table_lg(&mut s, w, d, d, &self.weights[w]);
292        }
293        table_lg(&mut s, "W1", d, dff, &self.weights["W1"]);
294        table_lg(&mut s, "W2", dff, d, &self.weights["W2"]);
295        table_lg(&mut s, "Wout", d, v, &self.head_weights());
296        table_lg(&mut s, "E", 1, self.exp_lut.len(), &self.exp_lut);
297        table_lg(&mut s, "Kc", self.ctx, d, kc);
298        table_lg(&mut s, "Vc", self.ctx, d, vc);
299        s.push_str("Let xe be row tok of EinA.\nLet xp be row pos of PinA.\nLet x be xe plus xp.\n");
300        s.push_str(&self.to8_lg("xi", "x", "r_qkv"));
301        for (out, w, k) in [("q", "Wq", "M_q"), ("kt", "Wk", "M_k"), ("vt", "Wv", "M_v")] {
302            s.push_str(&format!("Let {out}_a be xi matmul {w}.\n"));
303            s.push_str(&self.to8_lg(out, &format!("{out}_a"), k));
304        }
305        s.push_str("Set row pos of Kc to kt.\nSet row pos of Vc to vt.\n");
306        s.push_str(&self.attention_lg("c", "q", "Kc", "Vc", h, "E"));
307        s.push_str(&self.to8_lg("ci", "c", "r_ctx"));
308        s.push_str("Let o_a be ci matmul Wo.\n");
309        s.push_str(&self.rq_lg("o", "o_a", "M_o"));
310        s.push_str("Let s1 be x plus o.\n");
311        s.push_str(&self.to8_lg("hi", "s1", "r_ffn"));
312        s.push_str("Let hid_a be hi matmul W1.\n");
313        s.push_str(&self.rq_lg("hid_r", "hid_a", "M_h"));
314        s.push_str("Let hid be relu of hid_r.\n");
315        s.push_str(&self.to8_lg("fi", "hid", "r_hid"));
316        s.push_str("Let f_a be fi matmul W2.\n");
317        s.push_str(&self.rq_lg("f", "f_a", "M_f"));
318        s.push_str("Let s2 be s1 plus f.\n");
319        s.push_str(&self.to8_lg("oi", "s2", "r_out"));
320        s.push_str("Let logits be oi matmul Wout.\nLet next be argmax of logits.\n");
321        s
322    }
323
324    /// The decode step of [`Int8Model::decode_step_lg`] split over `tp` chips, Megatron's way, as one program and the
325    /// manifest of where each of its tensors lives.
326    ///
327    /// Chip `c` owns heads `c·H/tp ..` whole: its columns of `Wq`, `Wk`, `Wv` and of the KV cache, its heads' rows of
328    /// `Wo`, an exp table of its own, and FFN columns `c·d_ff/tp ..` of `W1` with their rows of `W2`. Every chip-local
329    /// table is named `<table>_c<chip>` and is a [`TpSlice`] of the model's. A chip's output projection is a partial
330    /// accumulator (`o_a_c<chip>`, `f_a_c<chip>`), and an all-reduce, spelled as the fold of `plus` it means, sums the
331    /// partials before the requant, so the step is the unsplit step bit for bit. The embedding, the residuals and the
332    /// head are the replicated spine. On one chip the accumulators are whole and nothing is all-reduced.
333    pub fn decode_step_tp_lg(
334        &self,
335        tok: usize,
336        pos: usize,
337        kc: &[i64],
338        vc: &[i64],
339        tp: usize,
340    ) -> Result<(String, TpManifest), String> {
341        let (d, h, dff, v) = (self.d, self.heads, self.d_ff, self.vocab);
342        if tp == 0 {
343            return Err("a split over no chips".into());
344        }
345        if h % tp != 0 {
346            return Err(format!("{h} heads do not split over {tp} chips"));
347        }
348        if dff % tp != 0 {
349            return Err(format!("{dff} FFN columns do not split over {tp} chips"));
350        }
351        let (hc, w, fc) = (h / tp, d / tp, dff / tp);
352        let mut m = TpManifest::new(tp);
353        let mut s = String::from("## Main\n");
354        s.push_str(&format!("Let tok be {}.\nLet pos be {}.\n", tok + 1, pos + 1));
355        table_lg(&mut s, "EinA", v, d, &self.embed);
356        table_lg(&mut s, "PinA", self.ctx, d, &self.positions);
357        let pad = |rows: &[i64]| -> Vec<i64> {
358            let mut full = rows.to_vec();
359            full.resize(self.ctx * d, 0);
360            full
361        };
362        let (kc_full, vc_full) = (pad(kc), pad(vc));
363        for c in 0..tp {
364            let heads = vec![(c * w, w)];
365            let cols = vec![(c * fc, fc)];
366            for (of, shape, axis, ranges, full) in [
367                ("Wq", (d, d), SliceAxis::Cols, &heads, &self.weights["Wq"]),
368                ("Wk", (d, d), SliceAxis::Cols, &heads, &self.weights["Wk"]),
369                ("Wv", (d, d), SliceAxis::Cols, &heads, &self.weights["Wv"]),
370                ("Wo", (d, d), SliceAxis::Rows, &heads, &self.weights["Wo"]),
371                ("W1", (d, dff), SliceAxis::Cols, &cols, &self.weights["W1"]),
372                ("W2", (dff, d), SliceAxis::Rows, &cols, &self.weights["W2"]),
373                ("E", (1, self.exp_lut.len()), SliceAxis::Cols, &vec![(0, self.exp_lut.len())], &self.exp_lut),
374                ("Kc", (self.ctx, d), SliceAxis::Cols, &heads, &kc_full),
375                ("Vc", (self.ctx, d), SliceAxis::Cols, &heads, &vc_full),
376            ] {
377                let slice = TpSlice { of: of.to_string(), of_shape: shape, axis, ranges: ranges.clone() };
378                let (rows, cols) = slice.shape();
379                let vals: Vec<i64> = (0..rows * cols).map(|i| full[slice.full_index(i / cols, i % cols)]).collect();
380                let name = format!("{of}_c{c}");
381                table_lg(&mut s, &name, rows, cols, &vals);
382                m.place(&name, c, Some(slice));
383            }
384        }
385        table_lg(&mut s, "Wout", d, v, &self.head_weights());
386        // A chip's accumulator of a row-parallel projection: a partial over several chips, the whole on one.
387        let partial = |out: &str, c: usize| if tp == 1 { out.to_string() } else { format!("{out}_c{c}") };
388        s.push_str("Let xe be row tok of EinA.\nLet xp be row pos of PinA.\nLet x be xe plus xp.\n");
389        s.push_str(&self.to8_lg("xi", "x", "r_qkv"));
390        let mut o_parts = Vec::new();
391        for c in 0..tp {
392            let mut local = Vec::new();
393            for (out, wt, k) in [("q", "Wq", "M_q"), ("kt", "Wk", "M_k"), ("vt", "Wv", "M_v")] {
394                let (o, a) = (format!("{out}_c{c}"), format!("{out}_a_c{c}"));
395                s.push_str(&format!("Let {a} be xi matmul {wt}_c{c}.\n"));
396                s.push_str(&self.to8_lg(&o, &a, k));
397                local.extend([a, format!("{o}_r"), o]);
398            }
399            s.push_str(&format!("Set row pos of Kc_c{c} to kt_c{c}.\nSet row pos of Vc_c{c} to vt_c{c}.\n"));
400            let (ctx_c, ci) = (format!("c_c{c}"), format!("ci_c{c}"));
401            s.push_str(&self.attention_lg(&ctx_c, &format!("q_c{c}"), &format!("Kc_c{c}"), &format!("Vc_c{c}"), hc, &format!("E_c{c}")));
402            s.push_str(&self.to8_lg(&ci, &ctx_c, "r_ctx"));
403            let o_a = partial("o_a", c);
404            s.push_str(&format!("Let {o_a} be {ci} matmul Wo_c{c}.\n"));
405            local.extend([ctx_c, format!("{ci}_r"), ci, o_a.clone()]);
406            o_parts.push(o_a);
407            for name in local {
408                m.place(&name, c, None);
409            }
410        }
411        fold_lg(&mut s, &mut m, "o_a", o_parts);
412        s.push_str(&self.rq_lg("o", "o_a", "M_o"));
413        s.push_str("Let s1 be x plus o.\n");
414        s.push_str(&self.to8_lg("hi", "s1", "r_ffn"));
415        let mut f_parts = Vec::new();
416        for c in 0..tp {
417            let (ha, hr, hd, fi) = (format!("hid_a_c{c}"), format!("hid_r_c{c}"), format!("hid_c{c}"), format!("fi_c{c}"));
418            s.push_str(&format!("Let {ha} be hi matmul W1_c{c}.\n"));
419            s.push_str(&self.rq_lg(&hr, &ha, "M_h"));
420            s.push_str(&format!("Let {hd} be relu of {hr}.\n"));
421            s.push_str(&self.to8_lg(&fi, &hd, "r_hid"));
422            let f_a = partial("f_a", c);
423            s.push_str(&format!("Let {f_a} be {fi} matmul W2_c{c}.\n"));
424            for name in [ha, hr, hd, format!("{fi}_r"), fi, f_a.clone()] {
425                m.place(&name, c, None);
426            }
427            f_parts.push(f_a);
428        }
429        fold_lg(&mut s, &mut m, "f_a", f_parts);
430        s.push_str(&self.rq_lg("f", "f_a", "M_f"));
431        s.push_str("Let s2 be s1 plus f.\n");
432        s.push_str(&self.to8_lg("oi", "s2", "r_out"));
433        s.push_str("Let logits be oi matmul Wout.\nLet next be argmax of logits.\n");
434        Ok((s, m))
435    }
436
437    /// The head's weights: the tied embedding's transpose, stored that way, as the matmul reads it, rather than
438    /// transposed again by every step.
439    fn head_weights(&self) -> Vec<i64> {
440        let (d, v) = (self.d, self.vocab);
441        let eq = &self.weights["Eq"];
442        (0..d * v).map(|i| eq[(i % v) * d + i / v]).collect()
443    }
444
445    /// `out = x · M >> S` for the requant `k`.
446    fn rq_lg(&self, out: &str, x: &str, k: &str) -> String {
447        let (m, sh) = self.requant[k];
448        format!("Let {out} be {x} times {m} shifted right by {sh}.\n")
449    }
450
451    /// `out` = the requant `k` of `x`, saturated to int8 (through `out_r`).
452    fn to8_lg(&self, out: &str, x: &str, k: &str) -> String {
453        format!(
454            "{}Let {out} be {out}_r clamped between -128 and 127.\n",
455            self.rq_lg(&format!("{out}_r"), x, k)
456        )
457    }
458
459    /// `out` = the exp-table attention of `q` over `kc` and `vc` with `heads` heads, up to `pos`, by table `e`.
460    fn attention_lg(&self, out: &str, q: &str, kc: &str, vc: &str, heads: usize, e: &str) -> String {
461        let (m, sh) = self.requant["M_score"];
462        format!(
463            "Let {out} be lut attention of {q} over {kc} and {vc} with {heads} heads {heads} groups up to pos with table {e} and scale {m} shifted right by {sh}.\n"
464        )
465    }
466}
467
468/// `Let name be a R by C tensor of Int.`, then a `Set item` for every nonzero value.
469fn table_lg(s: &mut String, name: &str, rows: usize, cols: usize, vals: &[i64]) {
470    s.push_str(&format!("Let {name} be a {rows} by {cols} tensor of Int.\n"));
471    for (i, x) in vals.iter().enumerate() {
472        if *x != 0 {
473            s.push_str(&format!("Set item {} of {name} to {x}.\n", i + 1));
474        }
475    }
476}
477
478/// `out` = the sum of `partials`, one per chip in chip order, as the left fold of `plus` an all-reduce means, recorded
479/// in the manifest. One partial is the sum itself: nothing is folded.
480fn fold_lg(s: &mut String, m: &mut TpManifest, out: &str, partials: Vec<String>) {
481    if partials.len() < 2 {
482        return;
483    }
484    let mut folds = Vec::new();
485    let mut acc = partials[0].clone();
486    for (i, p) in partials.iter().enumerate().skip(1) {
487        let binder = if i + 1 == partials.len() { out.to_string() } else { format!("{out}_f{i}") };
488        s.push_str(&format!("Let {binder} be {acc} plus {p}.\n"));
489        folds.push(binder.clone());
490        acc = binder;
491    }
492    m.collectives.push(TpCollective { out: out.to_string(), partials, folds });
493}