1mod autotune;
20mod emit;
21mod ir;
22mod model;
23mod toolchain;
24
25use anyhow::{Context, Result};
26use ir::{plan_chunks, UnhandledOperand};
27use model::{ExpressionsInfo, StarkInfo};
28use rayon::prelude::*;
29use std::path::{Path, PathBuf};
30use toolchain::Toolchain;
31
32const DEFAULT_CAP: usize = 40000; const DEFAULT_CHUNK: usize = 512; const SLOTS_CAP: u64 = 1000; #[derive(Debug, Clone)]
38pub struct GenConfig {
39 pub cap: usize,
41 pub chunk: Option<usize>,
43 pub archspec: String,
45 pub stark_src: Option<PathBuf>,
47 pub keep_dir: Option<PathBuf>,
49 pub dry_run: bool,
53}
54
55impl Default for GenConfig {
56 fn default() -> Self {
57 GenConfig {
58 cap: DEFAULT_CAP,
59 chunk: None,
60 archspec: "auto".into(),
61 stark_src: None,
62 keep_dir: None,
63 dry_run: false,
64 }
65 }
66}
67
68#[derive(Debug, Clone)]
70pub struct GeneratedAir {
71 pub name: String,
72 pub base: String,
73 pub sym: String,
74 pub nbits: u64,
75 pub cexp: i64,
76 pub n_ops: usize,
77 pub slots: u64,
78}
79
80#[derive(Debug, Default)]
82pub struct GenSummary {
83 pub generated: Vec<GeneratedAir>,
84 pub skipped: Vec<(String, String)>,
85 pub placed: usize,
86 pub max_scratch_bytes: u64,
87}
88
89struct Candidate {
91 stark_info: StarkInfo,
92 expr_info: ExpressionsInfo,
93 sym: String,
94 nbits: u64,
95 cexp: i64,
96 name: String,
97 n_ops: usize,
98 base: String,
99}
100
101struct Placement {
103 name: String,
104 base: String,
105 sym: String,
106 air_dir: PathBuf,
107}
108
109fn find_starkinfos(root: &Path) -> Vec<PathBuf> {
111 let mut out = Vec::new();
112 let mut stack = vec![root.to_path_buf()];
113 while let Some(dir) = stack.pop() {
114 let Ok(entries) = std::fs::read_dir(&dir) else { continue };
115 for e in entries.flatten() {
116 let p = e.path();
117 if p.is_dir() {
118 stack.push(p);
119 } else if p.file_name().and_then(|s| s.to_str()).is_some_and(|s| s.ends_with(".starkinfo.json")) {
120 out.push(p);
121 }
122 }
123 }
124 out.sort();
125 out
126}
127
128fn proof_phase(air_dir: &Path) -> String {
132 let comp = air_dir.file_name().and_then(|s| s.to_str()).unwrap_or("");
133 match comp {
134 "air" => "basic",
135 "compressor" => "compressor",
136 "recursive1" | "recursive2" => "recursive",
137 c if c.starts_with("vadcop_final") => "final",
138 other => other, }
140 .to_string()
141}
142
143fn make_sym(si: &StarkInfo, phase: &str) -> String {
147 format!("{phase}_a{}_{}_b{}_e{}", si.airgroup_id, si.air_id, si.stark_struct.n_bits, si.c_exp_id)
148}
149
150fn load_candidate(
153 stark_info_path: &Path,
154 root: &Path,
155 cap: usize,
156) -> Result<Option<Candidate>, Option<(String, String)>> {
157 let air_dir = stark_info_path.parent().unwrap();
158 let fname = stark_info_path.file_name().unwrap().to_string_lossy();
159 let base = fname.strip_suffix(".starkinfo.json").unwrap().to_string();
160 let expr_info_path = air_dir.join(format!("{base}.expressionsinfo.json"));
161 if !expr_info_path.exists() {
162 return Err(None);
163 }
164 let name = air_dir.strip_prefix(root).unwrap_or(air_dir).to_string_lossy().to_string();
165
166 let stark_info: StarkInfo = match std::fs::read(stark_info_path).ok().and_then(|b| serde_json::from_slice(&b).ok())
167 {
168 Some(si) => si,
169 None => return Err(None), };
171 let expr_info: ExpressionsInfo =
172 match std::fs::read(&expr_info_path).ok().and_then(|b| serde_json::from_slice(&b).ok()) {
173 Some(ei) => ei,
174 None => return Err(None),
175 };
176
177 let cexp = stark_info.c_exp_id;
178 let Some(code) = expr_info.expressions_code.iter().find(|e| e.exp_id == cexp) else {
179 return Err(None); };
181 let n_ops = code.code.len();
182 let nbits = stark_info.stark_struct.n_bits;
183 if n_ops > cap {
184 return Err(Some((name, format!("{n_ops} ops > CAP"))));
185 }
186 let sym = make_sym(&stark_info, &proof_phase(air_dir));
187 Ok(Some(Candidate { stark_info, expr_info, sym, nbits, cexp, name, n_ops, base }))
188}
189
190pub fn generate_all(proving_key: &Path, cfg: &GenConfig) -> Result<GenSummary> {
193 if cfg.dry_run && cfg.keep_dir.is_none() {
194 anyhow::bail!("dry_run requires keep_dir (nowhere to write the .cu otherwise)");
195 }
196 let work = WorkDir::new(cfg.keep_dir.clone())?;
197 std::fs::write(work.path().join("gen_common.cuh"), emit::COMMON_CUH)?;
198 let tc = Toolchain::new(cfg.stark_src.clone(), &cfg.archspec, work.path())?;
199 eprintln!("[exps-codegen] generating kernels for {} (archs: {})", proving_key.display(), tc.arch_summary());
200
201 let mut candidates: Vec<Candidate> = Vec::new();
203 let mut placements: Vec<Placement> = Vec::new();
204 let mut skipped: Vec<(String, String)> = Vec::new();
205 let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
206 for si_path in find_starkinfos(proving_key) {
207 match load_candidate(&si_path, proving_key, cfg.cap) {
208 Ok(Some(c)) => {
209 placements.push(Placement {
210 name: c.name.clone(),
211 base: c.base.clone(),
212 sym: c.sym.clone(),
213 air_dir: si_path.parent().unwrap().to_path_buf(),
214 });
215 if seen.insert(c.sym.clone()) {
216 candidates.push(c);
217 }
218 }
219 Ok(None) => {}
220 Err(Some(skip)) => skipped.push(skip),
221 Err(None) => {}
222 }
223 }
224
225 let mut summary = run_pipeline(&tc, work.path(), &candidates, &placements, cfg)?;
226 summary.skipped.extend(skipped);
227 summary.skipped.sort();
228 print_summary(&summary, cfg);
229 Ok(summary)
230}
231
232pub fn generate_air(air_dir: &Path, cfg: &GenConfig) -> Result<PathBuf> {
236 let si_path = find_starkinfos(air_dir)
237 .into_iter()
238 .find(|p| p.parent() == Some(air_dir))
239 .with_context(|| format!("no *.starkinfo.json in {}", air_dir.display()))?;
240
241 let work = WorkDir::new(cfg.keep_dir.clone())?;
242 std::fs::write(work.path().join("gen_common.cuh"), emit::COMMON_CUH)?;
243 let tc = Toolchain::new(cfg.stark_src.clone(), &cfg.archspec, work.path())?;
244
245 let root = air_dir;
246 let candidate = match load_candidate(&si_path, root, cfg.cap) {
247 Ok(Some(c)) => c,
248 Ok(None) => anyhow::bail!("{} is not a codegen target", air_dir.display()),
249 Err(Some((_, why))) => anyhow::bail!("skipped: {why}"),
250 Err(None) => anyhow::bail!("{} missing/invalid expressionsinfo", air_dir.display()),
251 };
252 let placement = Placement {
253 name: candidate.name.clone(),
254 base: candidate.base.clone(),
255 sym: candidate.sym.clone(),
256 air_dir: air_dir.to_path_buf(),
257 };
258 let dest = air_dir.join(format!("{}.exps.so", candidate.base));
259
260 let summary =
261 run_pipeline(&tc, work.path(), std::slice::from_ref(&candidate), std::slice::from_ref(&placement), cfg)?;
262 if summary.placed == 1 {
263 Ok(dest)
264 } else {
265 let why = summary.skipped.first().map(|(_, w)| w.clone()).unwrap_or_else(|| "unknown".into());
266 anyhow::bail!("{}: not generated ({why})", candidate.name)
267 }
268}
269
270fn run_pipeline(
271 tc: &Toolchain,
272 work: &Path,
273 candidates: &[Candidate],
274 placements: &[Placement],
275 cfg: &GenConfig,
276) -> Result<GenSummary> {
277 let autotune = cfg.chunk.is_none();
278
279 let built: Vec<(&Candidate, std::result::Result<ir::Ir, String>)> = candidates
282 .iter()
283 .map(|c| {
284 let r = match ir::build_ir(&c.stark_info, &c.expr_info) {
285 Ok(ir) => Ok(ir),
286 Err(e) if e.downcast_ref::<UnhandledOperand>().is_some() => Err("unhandled operand".to_string()),
287 Err(e) => Err(format!("build_ir error: {e}")),
288 };
289 (c, r)
290 })
291 .collect();
292
293 let chunk_map: std::collections::HashMap<String, Option<usize>> = if autotune {
295 built
296 .par_iter()
297 .filter_map(|(c, r)| {
298 r.as_ref()
299 .ok()
300 .map(|ir| autotune::tune_chunk(tc, ir, &c.sym, c.n_ops, work).map(|ck| (c.sym.clone(), ck)))
301 })
302 .collect::<Result<std::collections::HashMap<_, _>>>()?
303 } else {
304 Default::default()
305 };
306
307 let mut slots_by_sym: std::collections::HashMap<String, u64> = std::collections::HashMap::new();
309 let mut exprs_by_sym: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
310 let mut generated: Vec<GeneratedAir> = Vec::new();
311 let mut skipped: Vec<(String, String)> = Vec::new();
312 let mut max_scratch: u64 = 0;
313 for (c, r) in &built {
314 let ir = match r {
315 Ok(ir) => ir,
316 Err(why) => {
317 skipped.push((c.name.clone(), why.clone()));
318 continue;
319 }
320 };
321 let chunk = if autotune {
322 match chunk_map.get(&c.sym).copied().flatten() {
323 Some(ck) => ck,
324 None => {
325 skipped.push((c.name.clone(), format!("{} ops: still spills at CHUNK_MIN", c.n_ops)));
326 continue;
327 }
328 }
329 } else {
330 cfg.chunk.unwrap_or(DEFAULT_CHUNK)
331 };
332 let plan = plan_chunks(ir, chunk, &c.sym)?;
333 if plan.total_slots > SLOTS_CAP {
334 skipped.push((c.name.clone(), format!("slots {} > SLOTS_CAP (wide cut)", plan.total_slots)));
335 continue;
336 }
337 for (fname, text) in emit::emit_air(ir, &plan, &c.sym) {
338 std::fs::write(work.join(&fname), text)?;
339 }
340 {
346 const EXPR_CAP: usize = 512;
347 let mut items: Vec<(i64, ir::Ir, u64)> = Vec::new();
348 for ec in &c.expr_info.expressions_code {
349 if ec.exp_id == c.cexp || ec.code.is_empty() || ec.code.len() > EXPR_CAP {
350 continue;
351 }
352 let Ok(eir) = ir::build_ir_expr(&c.stark_info, &c.expr_info, ec.exp_id, false) else {
353 continue;
354 };
355 if eir.uses_zi() {
356 continue;
357 }
358 let Some(od) = eir.out_dim() else { continue };
359 if od != 1 && od != 3 {
360 continue;
361 }
362 items.push((ec.exp_id, eir, od));
363 }
364 if !items.is_empty() {
365 let n_exprs = items.len();
366 std::fs::write(work.join(format!("gen_{}_cexprs.cu", c.sym)), emit::emit_exprs_tu(&c.sym, &items))?;
367 exprs_by_sym.insert(c.sym.clone(), n_exprs);
368 }
369 }
370 let n_ext = 1u64 << c.stark_info.stark_struct.n_bits_ext;
371 max_scratch = max_scratch.max(plan.total_slots * n_ext);
372 slots_by_sym.insert(c.sym.clone(), plan.total_slots);
373 generated.push(GeneratedAir {
374 name: c.name.clone(),
375 base: c.base.clone(),
376 sym: c.sym.clone(),
377 nbits: c.nbits,
378 cexp: c.cexp,
379 n_ops: c.n_ops,
380 slots: plan.total_slots,
381 });
382 }
383
384 {
389 let mut jobs: Vec<(PathBuf, PathBuf)> = Vec::new();
390 for sym in slots_by_sym.keys() {
391 for cu in collect_artifacts(work, sym, "cu") {
392 let obj = cu.with_extension("o");
393 if !obj.exists() {
394 jobs.push((cu, obj));
395 }
396 }
397 }
398 let par = std::thread::available_parallelism().map(|n| n.get()).unwrap_or(8);
402 for batch in jobs.chunks(par) {
403 let errs: Vec<String> = std::thread::scope(|scope| {
404 let handles: Vec<_> = batch
405 .iter()
406 .map(|(cu, obj)| {
407 scope.spawn(move || -> Option<String> {
408 match tc.compile_tu(cu, obj, Some(work)) {
409 Ok((true, _)) => None,
410 Ok((false, log)) => Some(format!("nvcc failed for {}: {log}", cu.display())),
411 Err(e) => Some(format!("nvcc spawn failed for {}: {e}", cu.display())),
412 }
413 })
414 })
415 .collect();
416 handles.into_iter().filter_map(|h| h.join().ok().flatten()).collect()
417 });
418 if let Some(e) = errs.into_iter().next() {
419 anyhow::bail!(e);
420 }
421 }
422 }
423
424 let placed: Vec<&Placement> = placements.iter().filter(|p| slots_by_sym.contains_key(&p.sym)).collect();
426 write_gen_log(work, &placed, &slots_by_sym)?;
427 if !cfg.dry_run {
428 placed.par_iter().try_for_each(|p| -> Result<()> {
429 let dest = p.air_dir.join(format!("{}.exps.so", p.base));
430 link_one(tc, work, &p.sym, &dest)
431 })?;
432 }
433
434 Ok(GenSummary { placed: placed.len(), generated, skipped, max_scratch_bytes: max_scratch * 8 })
435}
436
437fn link_one(tc: &Toolchain, work: &Path, sym: &str, dest: &Path) -> Result<()> {
439 let objs = collect_artifacts(work, sym, "o");
440 if !objs.is_empty() {
441 tc.link_objs(&objs, dest)
442 } else {
443 let cus = collect_artifacts(work, sym, "cu");
444 tc.compile_link_cus(&cus, dest)
445 }
446}
447
448fn collect_artifacts(work: &Path, sym: &str, ext: &str) -> Vec<PathBuf> {
450 let mut out = Vec::new();
451 let main = work.join(format!("gen_{sym}.{ext}"));
452 if main.exists() {
453 out.push(main);
454 }
455 let prefix = format!("gen_{sym}_c");
456 if let Ok(entries) = std::fs::read_dir(work) {
457 let mut chunks: Vec<PathBuf> = entries
458 .flatten()
459 .map(|e| e.path())
460 .filter(|p| {
461 p.file_name()
462 .and_then(|s| s.to_str())
463 .is_some_and(|s| s.starts_with(&prefix) && s.ends_with(&format!(".{ext}")))
464 })
465 .collect();
466 chunks.sort();
467 out.extend(chunks);
468 }
469 out
470}
471
472fn write_gen_log(
475 work: &Path,
476 placed: &[&Placement],
477 slots_by_sym: &std::collections::HashMap<String, u64>,
478) -> Result<()> {
479 let mut log = String::new();
480 for p in placed {
481 log.push_str(&format!("{}\t{}\t{}\t{}\n", p.name, p.base, p.sym, slots_by_sym[&p.sym]));
482 }
483 std::fs::write(work.join("gen.log"), log)?;
484 Ok(())
485}
486
487fn print_summary(s: &GenSummary, cfg: &GenConfig) {
488 let chunk_info = if let Some(chunk) = cfg.chunk { format!(", chunk={}", chunk) } else { String::new() };
489 eprintln!(
490 "generated {} kernels -> {} per-AIR .exps.so (CAP={}{}, max scratch {:.0}MB):",
491 s.generated.len(),
492 s.placed,
493 cfg.cap,
494 chunk_info,
495 s.max_scratch_bytes as f64 / 1e6
496 );
497 for g in &s.generated {
498 let chunked = if g.slots > 0 { format!("CHUNKED slots={}", g.slots) } else { "single".into() };
499 eprintln!(" {:40} {}.exps.so nBits={} cExp={} ops={} {}", g.name, g.base, g.nbits, g.cexp, g.n_ops, chunked);
500 }
501 for (name, why) in &s.skipped {
502 eprintln!(" SKIP {name:38} {why}");
503 }
504}
505
506struct WorkDir {
508 path: PathBuf,
509 temp: bool,
510}
511
512impl WorkDir {
513 fn new(keep_dir: Option<PathBuf>) -> Result<Self> {
514 match keep_dir {
515 Some(p) => {
516 std::fs::create_dir_all(&p)?;
517 eprintln!("[exps-codegen] keeping generated code in {}", p.display());
518 Ok(WorkDir { path: p, temp: false })
519 }
520 None => {
521 use std::sync::atomic::{AtomicU64, Ordering};
522 static SEQ: AtomicU64 = AtomicU64::new(0);
523 let seq = SEQ.fetch_add(1, Ordering::Relaxed);
524 let p = std::env::temp_dir().join(format!("genexps_{}_{}", std::process::id(), seq));
525 let _ = std::fs::remove_dir_all(&p);
526 std::fs::create_dir_all(&p)?;
527 Ok(WorkDir { path: p, temp: true })
528 }
529 }
530 }
531 fn path(&self) -> &Path {
532 &self.path
533 }
534}
535
536impl Drop for WorkDir {
537 fn drop(&mut self) {
538 if self.temp {
539 let _ = std::fs::remove_dir_all(&self.path);
540 }
541 }
542}