1use anyhow::Result;
9use clap::{Parser, Subcommand};
10
11use crate::{analysis, doctor, profilers};
12
13pub const CGP_BACKEND_VALUES: [&str; 7] =
19 ["scalar", "avx2", "avx512", "neon", "cuda", "cublas", "wgpu"];
20
21#[derive(Parser, Clone, Debug)]
27#[command(name = "cgp", version, about, long_about = None)]
28pub struct Cli {
29 #[arg(long, global = true)]
31 pub json: bool,
32
33 #[command(subcommand)]
34 pub command: Commands,
35}
36
37#[derive(Subcommand, Clone, Debug)]
39pub enum Commands {
40 Profile {
42 #[command(subcommand)]
43 target: ProfileTarget,
44 },
45 Bench {
47 #[arg(long)]
49 bench: String,
50 #[arg(long)]
52 counters: Option<String>,
53 #[arg(long)]
55 check_regression: bool,
56 #[arg(long, default_value = "5")]
58 threshold: f64,
59 #[arg(long)]
61 roofline: bool,
62 },
63 Roofline {
65 #[arg(long)]
67 target: String,
68 #[arg(long)]
70 kernels: Option<String>,
71 #[arg(long)]
73 export: Option<String>,
74 #[arg(long)]
76 empirical: bool,
77 },
78 Diff {
80 #[arg(long)]
82 baseline: Option<String>,
83 #[arg(long)]
85 current: Option<String>,
86 #[arg(long)]
88 before: Option<String>,
89 #[arg(long)]
91 after: Option<String>,
92 },
93 Contract {
95 #[command(subcommand)]
96 action: ContractAction,
97 },
98 Trace {
100 binary: String,
102 #[arg(long)]
104 duration: Option<String>,
105 },
106 Explain {
108 target: String,
110 #[arg(long)]
112 kernel: Option<String>,
113 },
114 Tui,
116 Baseline {
118 #[arg(long)]
120 save: Option<String>,
121 #[arg(long)]
123 load: Option<String>,
124 },
125 Doctor,
127 Compete {
129 workload: String,
131 #[arg(long)]
133 ours: String,
134 #[arg(long)]
136 theirs: Vec<String>,
137 #[arg(long)]
139 label: Option<String>,
140 },
141}
142
143#[derive(Subcommand, Clone, Debug)]
145pub enum ProfileTarget {
146 Kernel {
148 #[arg(long)]
150 name: String,
151 #[arg(long)]
153 size: u32,
154 #[arg(long)]
156 roofline: bool,
157 #[arg(long)]
159 metrics: Option<String>,
160 },
161 Cublas {
163 #[arg(long)]
165 op: String,
166 #[arg(long)]
168 size: u32,
169 },
170 Wgpu {
172 #[arg(long)]
174 shader: String,
175 #[arg(long)]
177 dispatch: Option<String>,
178 #[arg(long)]
180 target: Option<String>,
181 },
182 Metal {
184 #[arg(long)]
186 shader: String,
187 #[arg(long)]
189 dispatch: Option<u32>,
190 },
191 Simd {
193 #[arg(long)]
195 function: String,
196 #[arg(long)]
198 size: u32,
199 #[arg(long)]
201 arch: String,
202 },
203 Wasm {
205 #[arg(long)]
207 function: String,
208 #[arg(long)]
210 size: u32,
211 },
212 Quant {
214 #[arg(long, required_unless_present = "all")]
216 kernel: Option<String>,
217 #[arg(long, required_unless_present = "all")]
219 size: Option<String>,
220 #[arg(long)]
222 all: bool,
223 },
224 Scalar {
226 #[arg(long)]
228 function: String,
229 #[arg(long)]
231 size: u32,
232 },
233 Parallel {
235 #[arg(long)]
237 function: String,
238 #[arg(long)]
240 size: u32,
241 #[arg(long)]
243 threads: Option<String>,
244 },
245 Compare {
247 #[arg(long)]
249 kernel: String,
250 #[arg(long)]
252 size: u32,
253 #[arg(long, value_delimiter = ',', value_parser = CGP_BACKEND_VALUES)]
262 backends: Vec<String>,
263 },
264 Scaling {
266 #[arg(long)]
268 size: u32,
269 #[arg(long)]
271 max_threads: Option<usize>,
272 #[arg(long, default_value = "3")]
274 runs: usize,
275 },
276 Binary {
278 path: String,
280 #[arg(long)]
282 kernel_filter: Option<String>,
283 #[arg(long)]
285 trace: bool,
286 #[arg(long)]
288 duration: Option<String>,
289 },
290 Python {
292 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
294 args: Vec<String>,
295 },
296 Library {
298 #[arg(long)]
300 so: String,
301 #[arg(long)]
303 symbol: String,
304 #[arg(long)]
306 args: Option<String>,
307 },
308}
309
310#[derive(Subcommand, Clone, Debug)]
312pub enum ContractAction {
313 Verify {
315 #[arg(long)]
317 contracts_dir: Option<String>,
318 #[arg(long)]
320 contract: Option<String>,
321 #[arg(long)]
323 fail_on_regression: bool,
324 #[arg(long, name = "self")]
326 self_verify: bool,
327 },
328 Generate {
330 #[arg(long)]
332 kernel: String,
333 #[arg(long)]
335 size: u32,
336 #[arg(long, default_value = "10")]
338 tolerance: f64,
339 },
340}
341
342pub fn run() -> Result<()> {
355 let cli = Cli::parse();
356 dispatch(cli.command, cli.json)
357}
358
359pub fn dispatch(command: Commands, json: bool) -> Result<()> {
368 match command {
369 Commands::Doctor => doctor::run_doctor(json),
370 Commands::Profile { target } => dispatch_profile(target, json),
371 Commands::Roofline {
372 target,
373 kernels,
374 export,
375 empirical,
376 } => analysis::roofline::run_roofline(
377 &target,
378 kernels.as_deref(),
379 export.as_deref(),
380 empirical,
381 json,
382 ),
383 Commands::Bench {
384 bench,
385 counters,
386 check_regression,
387 threshold,
388 roofline,
389 } => analysis::bench::run_bench(
390 &bench,
391 counters.as_deref(),
392 check_regression,
393 threshold,
394 roofline,
395 ),
396 Commands::Diff {
397 baseline,
398 current,
399 before,
400 after,
401 } => analysis::diff::run_diff(
402 baseline.as_deref(),
403 current.as_deref(),
404 before.as_deref(),
405 after.as_deref(),
406 json,
407 ),
408 Commands::Contract { action } => dispatch_contract(action),
409 Commands::Trace { binary, duration } => {
410 profilers::cuda::run_trace(&binary, duration.as_deref())
411 }
412 Commands::Explain { target, kernel } => {
413 analysis::explain::run_explain(&target, kernel.as_deref())
414 }
415 Commands::Tui => {
416 println!("cgp tui: interactive mode (requires presentar)");
417 println!(" (Not yet implemented — use stdout commands for now)");
418 Ok(())
419 }
420 Commands::Baseline { save, load } => {
421 analysis::baseline::run_baseline(save.as_deref(), load.as_deref())
422 }
423 Commands::Compete {
424 workload,
425 ours,
426 theirs,
427 label,
428 } => analysis::compete::run_compete(&workload, &ours, &theirs, label.as_deref(), json),
429 }
430}
431
432pub fn dispatch_profile(target: ProfileTarget, json: bool) -> Result<()> {
438 match target {
439 ProfileTarget::Kernel {
440 name,
441 size,
442 roofline,
443 metrics,
444 } => profilers::cuda::profile_kernel(&name, size, roofline, metrics.as_deref()),
445 ProfileTarget::Cublas { op, size } => profilers::cuda::profile_cublas(&op, size),
446 ProfileTarget::Wgpu {
447 shader,
448 dispatch,
449 target,
450 } => {
451 profilers::wgpu_profiler::profile_wgpu(&shader, dispatch.as_deref(), target.as_deref())
452 }
453 ProfileTarget::Metal { shader, dispatch } => {
454 #[cfg(target_os = "macos")]
455 {
456 println!("cgp profile metal: shader={shader} dispatch={dispatch:?}");
457 Ok(())
458 }
459 #[cfg(not(target_os = "macos"))]
460 {
461 let _ = (&shader, dispatch);
462 anyhow::bail!("Metal backend requires macOS -- use --backend wgpu for Vulkan")
463 }
464 }
465 ProfileTarget::Simd {
466 function,
467 size,
468 arch,
469 } => profilers::simd::profile_simd(&function, size, &arch),
470 ProfileTarget::Wasm { function, size } => profilers::wasm::profile_wasm(&function, size),
471 ProfileTarget::Quant { kernel, size, all } => {
472 if all {
473 profilers::quant::profile_quant_all()
474 } else {
475 profilers::quant::profile_quant(
476 kernel.as_deref().unwrap_or("q4k_gemv"),
477 size.as_deref().unwrap_or("4096x1x4096"),
478 )
479 }
480 }
481 ProfileTarget::Scalar { function, size } => {
482 profilers::scalar::profile_scalar(&function, size)
483 }
484 ProfileTarget::Parallel {
485 function,
486 size,
487 threads,
488 } => profilers::rayon_parallel::profile_parallel(&function, size, threads.as_deref()),
489 ProfileTarget::Compare {
490 kernel,
491 size,
492 backends,
493 } => analysis::compare::run_compare(&kernel, size, &backends.join(","), json),
494 ProfileTarget::Scaling {
495 size,
496 max_threads,
497 runs,
498 } => profilers::rayon_parallel::profile_scaling(size, max_threads, runs, json),
499 ProfileTarget::Binary {
500 path,
501 kernel_filter,
502 trace,
503 duration,
504 } => profilers::cuda::profile_binary(
505 &path,
506 kernel_filter.as_deref(),
507 trace,
508 duration.as_deref(),
509 ),
510 ProfileTarget::Python { args } => profilers::cuda::profile_python(&args),
511 ProfileTarget::Library { so, symbol, args } => {
512 println!("cgp profile library: {so}::{symbol} args={args:?}");
513 Ok(())
514 }
515 }
516}
517
518pub fn dispatch_contract(action: ContractAction) -> Result<()> {
524 match action {
525 ContractAction::Verify {
526 contracts_dir,
527 contract,
528 fail_on_regression,
529 self_verify,
530 } => analysis::contracts::run_verify(
531 contracts_dir.as_deref(),
532 contract.as_deref(),
533 self_verify,
534 fail_on_regression,
535 ),
536 ContractAction::Generate {
537 kernel,
538 size,
539 tolerance,
540 } => analysis::contracts::run_generate(&kernel, size, tolerance),
541 }
542}
543
544#[cfg(test)]
545mod tests {
546 use super::*;
547 use clap::CommandFactory;
548
549 #[test]
550 fn cli_definition_is_valid() {
551 Cli::command().debug_assert();
552 }
553
554 #[test]
555 fn parses_doctor_subcommand() {
556 let cli = Cli::parse_from(["cgp", "doctor"]);
557 assert!(!cli.json);
558 assert!(
559 matches!(cli.command, Commands::Doctor),
560 "expected Commands::Doctor, got {:?}",
561 cli.command
562 );
563 }
564
565 #[test]
566 fn global_json_flag_is_captured() {
567 let cli = Cli::parse_from(["cgp", "doctor", "--json"]);
568 assert!(cli.json);
569 }
570
571 #[test]
572 fn commands_is_clone_and_debug() {
573 let cli = Cli::parse_from(["cgp", "tui"]);
574 let cloned = cli.command.clone();
575 assert!(
576 matches!(cloned, Commands::Tui),
577 "clone changed the variant: {cloned:?}"
578 );
579 }
580}