1use std::collections::BTreeMap;
11
12use super::tensor_parallel::{SliceAxis, TpCollective, TpManifest, TpSlice};
13
14#[derive(Clone, Debug)]
16pub struct Int8Model {
17 pub d: usize,
18 pub heads: usize,
19 pub d_ff: usize,
20 pub vocab: usize,
21 pub ctx: usize,
23 pub requant: BTreeMap<String, (i64, u32)>,
25 pub weights: BTreeMap<String, Vec<i64>>,
28 pub embed: Vec<i64>,
30 pub positions: Vec<i64>,
32 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 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 pub fn step_reads() -> std::collections::BTreeSet<String> {
114 ["next".to_string()].into()
115 }
116
117 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 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 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 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 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 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 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 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 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
468fn 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
478fn 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}