1use std::path::PathBuf;
2use std::process::ExitCode;
3
4use clap::Parser;
5use miette::{Context, IntoDiagnostic};
6
7use nxpu_backend_core::{
8 BackendOptions, BackendRegistry, ConstantTensor, OutputContent, Precision, PrecisionPolicy,
9};
10use nxpu_opt::{OptLevel, PassManager};
11
12#[derive(Parser)]
14#[command(version, about)]
15#[allow(clippy::struct_excessive_bools)]
16struct Cli {
17 input: Option<PathBuf>,
19
20 #[arg(short, long, default_value = "ir-dump")]
22 target: String,
23
24 #[arg(short, long)]
26 output: Option<PathBuf>,
27
28 #[arg(long, default_value = "1", value_parser = parse_opt_level)]
30 opt_level: OptLevel,
31
32 #[arg(long)]
34 emit_ir: bool,
35
36 #[arg(long)]
38 emit_memory_plan: bool,
39
40 #[arg(long)]
42 emit_schedule: bool,
43
44 #[arg(long)]
46 dry_run: bool,
47
48 #[arg(long, default_value = "auto", value_parser = parse_precision)]
50 precision: PrecisionPolicy,
51
52 #[arg(long)]
54 dynamic_batch: bool,
55
56 #[arg(long, value_name = "N")]
64 symbolic_dim: Option<u32>,
65
66 #[arg(long, value_name = "DIR")]
83 weights: Option<PathBuf>,
84
85 #[arg(long)]
87 calibration_data: Option<PathBuf>,
88
89 #[arg(long, default_value = "minmax", value_parser = parse_calibration_method)]
91 calibration_method: nxpu_opt::CalibrationMethod,
92
93 #[arg(long)]
95 estimate_latency: bool,
96
97 #[arg(short, long)]
99 verbose: bool,
100
101 #[arg(long)]
103 list_targets: bool,
104}
105
106fn load_constant_tensors(dir: &std::path::Path) -> miette::Result<Vec<ConstantTensor>> {
118 let entries = std::fs::read_dir(dir)
119 .map_err(|e| miette::miette!("cannot read the weights directory {}: {e}", dir.display()))?;
120 let mut tensors = Vec::new();
121 for entry in entries {
122 let path = entry
123 .map_err(|e| miette::miette!("cannot read the weights directory: {e}"))?
124 .path();
125 if path.extension().and_then(|e| e.to_str()) != Some("bin") {
126 continue;
127 }
128 let Some(name) = path.file_stem().and_then(|s| s.to_str()) else {
129 continue;
130 };
131 let data = std::fs::read(&path)
132 .map_err(|e| miette::miette!("cannot read {}: {e}", path.display()))?;
133 tensors.push(ConstantTensor {
134 name: name.to_string(),
135 data,
136 });
137 }
138 tensors.sort_by(|a, b| a.name.cmp(&b.name));
141 Ok(tensors)
142}
143
144fn parse_calibration_method(s: &str) -> Result<nxpu_opt::CalibrationMethod, String> {
145 nxpu_opt::CalibrationMethod::from_str_name(s).ok_or_else(|| {
146 format!("invalid calibration method '{s}', expected minmax, percentile, or kl-divergence")
147 })
148}
149
150fn parse_precision(s: &str) -> Result<PrecisionPolicy, String> {
151 match s {
152 "keep" => Ok(PrecisionPolicy::Keep),
153 "f16" => Ok(PrecisionPolicy::Explicit(Precision::F16)),
154 "bf16" => Ok(PrecisionPolicy::Explicit(Precision::BF16)),
155 "int8" => Ok(PrecisionPolicy::Explicit(Precision::Int8)),
156 "auto" => Ok(PrecisionPolicy::Auto),
157 _ => Err(format!(
158 "invalid precision '{s}', expected keep, f16, bf16, int8, or auto"
159 )),
160 }
161}
162
163fn parse_opt_level(s: &str) -> Result<OptLevel, String> {
164 match s {
165 "0" => Ok(OptLevel::O0),
166 "1" => Ok(OptLevel::O1),
167 "2" => Ok(OptLevel::O2),
168 _ => Err(format!(
169 "invalid optimization level '{s}', expected 0, 1, or 2"
170 )),
171 }
172}
173
174fn main() -> ExitCode {
175 match run() {
176 Ok(()) => ExitCode::SUCCESS,
177 Err(err) => {
178 eprintln!("Error: {err:?}");
179 ExitCode::FAILURE
180 }
181 }
182}
183
184fn build_registry() -> BackendRegistry {
185 #[allow(unused_mut)]
186 let mut registry = BackendRegistry::with_builtins();
187 #[cfg(feature = "backend-onnx")]
188 registry.register(Box::new(nxpu_backend_onnx::OnnxBackend));
189 #[cfg(feature = "backend-tflite")]
190 registry.register(Box::new(nxpu_backend_tflite::TfLiteBackend));
191 #[cfg(feature = "backend-coreml")]
192 registry.register(Box::new(nxpu_backend_coreml::CoreMlBackend));
193 #[cfg(feature = "backend-stablehlo")]
194 registry.register(Box::new(nxpu_backend_stablehlo::StableHloBackend));
195 #[cfg(feature = "backend-samsung")]
196 registry.register(Box::new(nxpu_backend_samsung::SamsungBackend));
197 #[cfg(feature = "backend-mediatek")]
198 registry.register(Box::new(nxpu_backend_mediatek::MediaTekBackend));
199 #[cfg(feature = "backend-intel")]
200 registry.register(Box::new(nxpu_backend_intel::IntelBackend));
201 #[cfg(feature = "backend-amd")]
202 registry.register(Box::new(nxpu_backend_amd::AmdBackend));
203 #[cfg(feature = "backend-qualcomm")]
204 registry.register(Box::new(nxpu_backend_qualcomm::QualcommBackend));
205 #[cfg(feature = "backend-arm-ethos")]
206 registry.register(Box::new(nxpu_backend_arm_ethos::ArmEthosBackend));
207 #[cfg(feature = "backend-ceva")]
208 registry.register(Box::new(nxpu_backend_ceva::CevaBackend));
209 #[cfg(feature = "backend-rockchip")]
210 registry.register(Box::new(nxpu_backend_rockchip::RockchipBackend));
211 registry
212}
213
214fn run() -> miette::Result<()> {
215 env_logger::try_init().ok();
216
217 let cli = Cli::parse();
218
219 if cli.list_targets {
221 let registry = build_registry();
222 for target in registry.list_targets() {
223 println!("{target}");
224 }
225 return Ok(());
226 }
227
228 let input = cli.input.ok_or_else(|| {
229 miette::miette!("input file is required (use --list-targets to list backends)")
230 })?;
231
232 let source = std::fs::read_to_string(&input)
234 .into_diagnostic()
235 .wrap_err_with(|| format!("failed to read {}", input.display()))?;
236
237 let mut module = nxpu_parser::parse(&source)
239 .map_err(|e| miette::miette!("{e}"))
240 .wrap_err("WGSL parse failed")?;
241
242 PassManager::for_level(cli.opt_level).run(&mut module);
244
245 if cli.dynamic_batch {
248 apply_dynamic_batch(&mut module);
249 }
250
251 if cli.emit_ir {
253 eprintln!("{}", nxpu_ir::dump_module(&module));
254 }
255
256 let memory_plan = nxpu_opt::plan_memory(&module);
258 if cli.emit_memory_plan {
259 eprint!("{memory_plan}");
260 }
261
262 if cli.emit_schedule {
264 let schedules = nxpu_opt::compute_schedules(&module);
265 for (name, dfg, schedule) in &schedules {
266 eprintln!("{}", nxpu_opt::format_schedule(name, dfg, schedule));
267 }
268 }
269
270 if cli.estimate_latency {
272 let profiles = nxpu_analysis::default_profiles();
273 for (i, ep) in module.entry_points.iter().enumerate() {
274 let pattern = match nxpu_analysis::classify_entry_point(&module, i) {
275 Ok(p) => p,
276 Err(e) => {
277 eprintln!("Entry point '{}': classification failed: {e}", ep.name);
278 continue;
279 }
280 };
281 let cost = nxpu_analysis::estimate_kernel_cost(&pattern);
282 eprintln!("Entry point '{}': {}", ep.name, pattern);
283 eprintln!(" {cost}");
284 eprintln!(
285 " Arithmetic intensity: {:.2} FLOP/byte",
286 cost.arithmetic_intensity()
287 );
288 for (target, profile) in &profiles {
289 let latency = profile.predict_latency_secs(&cost);
290 let bottleneck = profile.bottleneck(&cost);
291 eprintln!(
292 " [{target}] {}: {latency:.6}s ({bottleneck})",
293 profile.name
294 );
295 }
296 }
297 }
298
299 if cli.dry_run {
301 return Ok(());
302 }
303
304 let registry = build_registry();
306 let backend = registry.find(&cli.target).ok_or_else(|| {
307 let available = registry.list_targets().join(", ");
308 miette::miette!("unknown target '{}' (available: {})", cli.target, available)
309 })?;
310
311 let mut quantization_params: Vec<nxpu_backend_core::QuantParam> = Vec::new();
313 let mut per_channel_params: Vec<nxpu_backend_core::PerChannelParam> = Vec::new();
314 let resolved_precision = match cli.precision {
315 PrecisionPolicy::Keep => None,
316 PrecisionPolicy::Explicit(p) => Some(p),
317 PrecisionPolicy::Auto => {
318 let pref = backend.preferred_precision();
319 if pref == Precision::F32 {
320 None
321 } else {
322 Some(pref)
323 }
324 }
325 };
326
327 if let Some(precision) = resolved_precision {
328 use nxpu_opt::Pass;
329 match precision {
330 Precision::F16 => {
331 nxpu_opt::F32ToF16.run(&mut module);
332 }
333 Precision::BF16 => {
334 nxpu_opt::F32ToBf16.run(&mut module);
335 }
336 Precision::Int8 => {
337 if let Some(cal_dir) = &cli.calibration_data {
339 let dataset = nxpu_opt::CalibrationDataset::load_from_dir(cal_dir)
340 .map_err(|e| miette::miette!("calibration failed: {e}"))?;
341
342 if cli.verbose {
343 eprintln!(
344 "Loaded {} calibration samples from {}",
345 dataset.num_samples(),
346 cal_dir.display()
347 );
348 }
349
350 let cal_result = nxpu_opt::run_calibration(
351 &dataset,
352 &cli.calibration_method,
353 true, )
355 .map_err(|e| miette::miette!("calibration failed: {e}"))?;
356
357 if cli.verbose {
358 eprintln!("Calibration results:");
359 for (name, params) in &cal_result.tensor_params {
360 eprintln!(
361 " {}: scale={:.6}, zero_point={}",
362 name, params.scale, params.zero_point
363 );
364 }
365 }
366
367 for (name, params) in &cal_result.tensor_params {
369 quantization_params.push(nxpu_backend_core::QuantParam {
370 name: name.clone(),
371 scale: params.scale,
372 zero_point: params.zero_point,
373 });
374 }
375
376 for (name, pcp) in &cal_result.weight_params {
378 per_channel_params.push(nxpu_backend_core::PerChannelParam {
379 name: name.clone(),
380 scales: pcp.scales.clone(),
381 zero_points: pcp.zero_points.clone(),
382 channel_axis: pcp.channel_axis,
383 });
384 }
385
386 nxpu_opt::F32ToInt8::with_calibration_result(cal_result).run(&mut module);
387 } else {
388 nxpu_opt::F32ToInt8::default().run(&mut module);
389 }
390 }
391 Precision::F32 => {}
392 }
393 }
394
395 let mut tiling_plans = Vec::new();
397 let mut vectorization_hints = Vec::new();
398 for i in 0..module.entry_points.len() {
399 if let Ok(pattern) = nxpu_analysis::classify_entry_point(&module, i) {
400 match &pattern {
402 nxpu_analysis::KernelPattern::MatMul { shape, .. } => {
403 let m = shape.m.parse::<u32>().unwrap_or(256);
404 let n = shape.n.parse::<u32>().unwrap_or(256);
405 let k = shape.k.parse::<u32>().unwrap_or(256);
406 let defaults = nxpu_opt::TilingDefaults::default();
407 let plan =
408 nxpu_opt::tile_matmul(&nxpu_opt::TilingMatMulShape { m, n, k }, &defaults);
409 tiling_plans.push(nxpu_backend_core::TilingPlanInfo {
410 op_name: plan.op_name,
411 tiles: plan
412 .tiles
413 .iter()
414 .map(|t| (t.dim_name.clone(), t.tile_size))
415 .collect(),
416 reuse_factor: plan.reuse_factor,
417 });
418 }
419 nxpu_analysis::KernelPattern::Conv2D { shape, .. } => {
420 let oh = shape.height.parse::<u32>().unwrap_or(32);
421 let ow = shape.width.parse::<u32>().unwrap_or(32);
422 let defaults = nxpu_opt::TilingDefaults::default();
423 let plan = nxpu_opt::tile_conv2d(
424 &nxpu_opt::TilingConv2DShape {
425 oh,
426 ow,
427 kh: shape.kernel_h_val as u32,
428 kw: shape.kernel_w_val as u32,
429 },
430 &defaults,
431 );
432 tiling_plans.push(nxpu_backend_core::TilingPlanInfo {
433 op_name: plan.op_name,
434 tiles: plan
435 .tiles
436 .iter()
437 .map(|t| (t.dim_name.clone(), t.tile_size))
438 .collect(),
439 reuse_factor: plan.reuse_factor,
440 });
441 }
442 _ => {}
443 }
444 for hint in nxpu_opt::analyze_vectorization(&pattern, 128) {
446 vectorization_hints.push(nxpu_backend_core::VectorizationHintInfo {
447 op_name: hint.op_name,
448 dim_name: hint.dim_name,
449 lanes: hint.vector_width.lanes,
450 is_reduction: hint.is_reduction,
451 is_contiguous: hint.is_contiguous,
452 });
453 }
454 }
455 }
456
457 let constant_tensors = match &cli.weights {
460 None => Vec::new(),
461 Some(dir) => load_constant_tensors(dir)?,
462 };
463
464 let opts = BackendOptions {
465 opt_level: match cli.opt_level {
466 OptLevel::O0 => 0,
467 OptLevel::O1 => 1,
468 OptLevel::O2 => 2,
469 },
470 precision: cli.precision,
471 memory_plan: Some(memory_plan),
472 quantization_params,
473 per_channel_params,
474 tiling_plans,
475 vectorization_hints,
476 symbolic_extent: cli.symbolic_dim,
477 constant_tensors,
478 };
479
480 let output = backend
481 .compile(&module, &opts)
482 .map_err(|e| miette::miette!("{e}"))
483 .wrap_err("backend compilation failed")?;
484
485 for diag in &output.diagnostics {
487 eprintln!("{:?}: {}", diag.level, diag.message);
488 }
489
490 if let Some(base) = &cli.output {
492 if output.files.len() > 1 {
493 let stem = base
495 .file_stem()
496 .map(|s| s.to_string_lossy().into_owned())
497 .unwrap_or_else(|| "output".into());
498 let parent = base.parent().unwrap_or_else(|| std::path::Path::new("."));
499
500 for file in &output.files {
501 let dest = parent.join(format!("{stem}_{}", file.name));
502 write_output_file(&dest, &file.content)?;
503 }
504 } else {
505 for file in &output.files {
506 write_output_file(base, &file.content)?;
507 }
508 }
509 } else {
510 for file in &output.files {
511 match &file.content {
512 OutputContent::Text(text) => print!("{text}"),
513 OutputContent::Binary(_) => {
514 return Err(miette::miette!(
515 "backend produced binary output but no --output path was specified"
516 ));
517 }
518 }
519 }
520 }
521
522 Ok(())
523}
524
525fn apply_dynamic_batch(module: &mut nxpu_ir::Module) {
533 use nxpu_ir::{AddressSpace, Dimension, Scalar, TensorShape, Type, TypeInner};
534
535 let targets: Vec<(nxpu_ir::Handle<nxpu_ir::GlobalVariable>, Scalar)> = module
537 .global_variables
538 .iter()
539 .filter_map(|(handle, gv)| {
540 if let AddressSpace::Storage { .. } = &gv.space {
541 match &module.types[gv.ty].inner {
542 TypeInner::Array { base, .. } => {
543 if let TypeInner::Scalar(s) = &module.types[*base].inner {
544 return Some((handle, *s));
545 }
546 }
547 TypeInner::Tensor { scalar, .. } => {
548 return Some((handle, *scalar));
549 }
550 _ => {}
551 }
552 }
553 None
554 })
555 .collect();
556
557 for (handle, scalar) in targets {
558 let gv = &module.global_variables[handle];
559 let old_ty = gv.ty;
560 let new_ty = match &module.types[old_ty].inner {
561 TypeInner::Array { .. } => {
562 module.types.insert(Type {
564 name: None,
565 inner: TypeInner::Tensor {
566 scalar,
567 shape: TensorShape {
568 dims: vec![
569 Dimension::Symbolic("batch".into()),
570 Dimension::Dynamic(None),
571 ],
572 },
573 },
574 })
575 }
576 TypeInner::Tensor { shape, .. } => {
577 let mut new_dims = shape.dims.clone();
579 if !new_dims.is_empty() {
580 new_dims[0] = Dimension::Symbolic("batch".into());
581 }
582 module.types.insert(Type {
583 name: None,
584 inner: TypeInner::Tensor {
585 scalar,
586 shape: TensorShape { dims: new_dims },
587 },
588 })
589 }
590 _ => continue,
591 };
592 module.global_variables[handle].ty = new_ty;
594 }
595}
596
597fn write_output_file(path: &std::path::Path, content: &OutputContent) -> miette::Result<()> {
598 match content {
599 OutputContent::Text(text) => std::fs::write(path, text),
600 OutputContent::Binary(data) => std::fs::write(path, data),
601 }
602 .into_diagnostic()
603 .wrap_err_with(|| format!("failed to write {}", path.display()))
604}
605
606#[cfg(test)]
607mod tests {
608 use super::*;
609 use clap::Parser;
610
611 #[test]
614 fn cli_defaults() {
615 let cli = Cli::try_parse_from(["nxpu", "input.wgsl"]).unwrap();
616 assert_eq!(cli.input.unwrap(), PathBuf::from("input.wgsl"));
617 assert_eq!(cli.target, "ir-dump");
618 assert!(cli.output.is_none());
619 assert_eq!(cli.opt_level, OptLevel::O1);
620 assert!(!cli.emit_ir);
621 assert!(!cli.emit_memory_plan);
622 assert!(!cli.emit_schedule);
623 assert!(!cli.dry_run);
624 assert!(!cli.dynamic_batch);
625 assert_eq!(cli.precision, PrecisionPolicy::Auto);
626 assert!(cli.calibration_data.is_none());
627 assert_eq!(cli.calibration_method, nxpu_opt::CalibrationMethod::MinMax);
628 assert!(!cli.verbose);
629 assert!(!cli.list_targets);
630 }
631
632 #[test]
633 fn cli_all_flags() {
634 let cli = Cli::try_parse_from([
635 "nxpu",
636 "model.wgsl",
637 "--target",
638 "onnx",
639 "--output",
640 "out.onnx",
641 "--opt-level",
642 "2",
643 "--emit-ir",
644 "--emit-memory-plan",
645 "--emit-schedule",
646 "--precision",
647 "f16",
648 ])
649 .unwrap();
650 assert_eq!(cli.input.unwrap(), PathBuf::from("model.wgsl"));
651 assert_eq!(cli.target, "onnx");
652 assert_eq!(cli.output.unwrap(), PathBuf::from("out.onnx"));
653 assert_eq!(cli.opt_level, OptLevel::O2);
654 assert!(cli.emit_ir);
655 assert!(cli.emit_memory_plan);
656 assert!(cli.emit_schedule);
657 assert_eq!(cli.precision, PrecisionPolicy::Explicit(Precision::F16));
658 }
659
660 #[test]
661 fn cli_short_flags() {
662 let cli =
663 Cli::try_parse_from(["nxpu", "in.wgsl", "-t", "tflite", "-o", "out.tflite"]).unwrap();
664 assert_eq!(cli.target, "tflite");
665 assert_eq!(cli.output.unwrap(), PathBuf::from("out.tflite"));
666 }
667
668 #[test]
669 fn cli_dynamic_batch_flag() {
670 let cli = Cli::try_parse_from(["nxpu", "model.wgsl", "--dynamic-batch"]).unwrap();
671 assert!(cli.dynamic_batch);
672 }
673
674 #[test]
675 fn cli_estimate_latency_flag() {
676 let cli = Cli::try_parse_from(["nxpu", "model.wgsl", "--estimate-latency"]).unwrap();
677 assert!(cli.estimate_latency);
678 }
679
680 #[test]
681 fn cli_estimate_latency_default_off() {
682 let cli = Cli::try_parse_from(["nxpu", "model.wgsl"]).unwrap();
683 assert!(!cli.estimate_latency);
684 }
685
686 #[test]
687 fn cli_list_targets_no_input() {
688 let cli = Cli::try_parse_from(["nxpu", "--list-targets"]).unwrap();
689 assert!(cli.list_targets);
690 assert!(cli.input.is_none());
691 }
692
693 #[test]
694 fn cli_invalid_opt_level() {
695 let result = Cli::try_parse_from(["nxpu", "in.wgsl", "--opt-level", "3"]);
696 assert!(result.is_err());
697 }
698
699 #[test]
700 fn cli_invalid_precision() {
701 let result = Cli::try_parse_from(["nxpu", "in.wgsl", "--precision", "f64"]);
702 assert!(result.is_err());
703 }
704
705 #[test]
708 fn precision_valid_values() {
709 assert_eq!(parse_precision("keep").unwrap(), PrecisionPolicy::Keep);
710 assert_eq!(
711 parse_precision("f16").unwrap(),
712 PrecisionPolicy::Explicit(Precision::F16)
713 );
714 assert_eq!(
715 parse_precision("bf16").unwrap(),
716 PrecisionPolicy::Explicit(Precision::BF16)
717 );
718 assert_eq!(
719 parse_precision("int8").unwrap(),
720 PrecisionPolicy::Explicit(Precision::Int8)
721 );
722 assert_eq!(parse_precision("auto").unwrap(), PrecisionPolicy::Auto);
723 }
724
725 #[test]
726 fn precision_invalid_value() {
727 let err = parse_precision("f64").unwrap_err();
728 assert!(err.contains("invalid precision"));
729 assert!(err.contains("f64"));
730 }
731
732 #[test]
735 fn opt_level_valid_values() {
736 assert_eq!(parse_opt_level("0").unwrap(), OptLevel::O0);
737 assert_eq!(parse_opt_level("1").unwrap(), OptLevel::O1);
738 assert_eq!(parse_opt_level("2").unwrap(), OptLevel::O2);
739 }
740
741 #[test]
742 fn opt_level_invalid_value() {
743 let err = parse_opt_level("3").unwrap_err();
744 assert!(err.contains("invalid optimization level"));
745 assert!(err.contains('3'));
746 }
747
748 #[test]
751 fn registry_always_has_ir_dump() {
752 let registry = build_registry();
753 assert!(
754 registry.find("ir-dump").is_some(),
755 "ir-dump should always be available"
756 );
757 }
758
759 #[test]
760 fn registry_unknown_target_returns_none() {
761 let registry = build_registry();
762 assert!(registry.find("nonexistent-backend").is_none());
763 }
764
765 #[test]
766 fn registry_list_targets_includes_ir_dump() {
767 let registry = build_registry();
768 let targets = registry.list_targets();
769 assert!(targets.contains(&"ir-dump"));
770 }
771
772 #[cfg(feature = "backend-onnx")]
773 #[test]
774 fn registry_has_onnx_when_enabled() {
775 let registry = build_registry();
776 assert!(registry.find("onnx").is_some());
777 }
778
779 #[cfg(feature = "backend-tflite")]
780 #[test]
781 fn registry_has_tflite_when_enabled() {
782 let registry = build_registry();
783 assert!(registry.find("tflite").is_some());
784 }
785
786 #[test]
789 fn multi_file_output_path_derivation() {
790 let base = PathBuf::from("/tmp/model.onnx");
791 let stem = base
792 .file_stem()
793 .map(|s| s.to_string_lossy().into_owned())
794 .unwrap_or_else(|| "output".into());
795 let parent = base.parent().unwrap_or_else(|| std::path::Path::new("."));
796
797 let dest = parent.join(format!("{stem}_{}", "weights.bin"));
798 assert_eq!(dest, PathBuf::from("/tmp/model_weights.bin"));
799 }
800
801 #[test]
802 fn multi_file_output_path_no_extension() {
803 let base = PathBuf::from("output");
804 let stem = base
805 .file_stem()
806 .map(|s| s.to_string_lossy().into_owned())
807 .unwrap_or_else(|| "output".into());
808 let parent = base.parent().unwrap_or_else(|| std::path::Path::new("."));
809
810 let dest = parent.join(format!("{stem}_{}", "data.bin"));
811 assert_eq!(dest, PathBuf::from("output_data.bin"));
812 }
813
814 #[test]
817 fn unknown_target_error_lists_available() {
818 let registry = build_registry();
819 let result = registry.find("bogus");
820 assert!(result.is_none());
821 let available = registry.list_targets().join(", ");
822 let msg = format!("unknown target 'bogus' (available: {available})");
823 assert!(msg.contains("bogus"));
824 assert!(msg.contains("ir-dump"));
825 }
826
827 #[test]
830 fn apply_dynamic_batch_rewrites_array_to_tensor() {
831 use nxpu_ir::*;
832
833 let mut module = Module::default();
834 let f32_ty = module.types.insert(Type {
835 name: None,
836 inner: TypeInner::Scalar(Scalar::F32),
837 });
838 let array_ty = module.types.insert(Type {
839 name: None,
840 inner: TypeInner::Array {
841 base: f32_ty,
842 size: ArraySize::Dynamic,
843 stride: 4,
844 },
845 });
846 let handle = module.global_variables.append(GlobalVariable {
847 name: Some("input".into()),
848 space: AddressSpace::Storage {
849 access: StorageAccess::LOAD,
850 },
851 binding: None,
852 ty: array_ty,
853 init: None,
854 layout: None,
855 });
856
857 apply_dynamic_batch(&mut module);
858
859 let new_ty = &module.types[module.global_variables[handle].ty].inner;
860 match new_ty {
861 TypeInner::Tensor { scalar, shape } => {
862 assert_eq!(*scalar, Scalar::F32);
863 assert_eq!(shape.rank(), 2);
864 assert_eq!(shape.dims[0], Dimension::Symbolic("batch".into()));
865 assert_eq!(shape.dims[1], Dimension::Dynamic(None));
866 }
867 _ => panic!("expected Tensor type after apply_dynamic_batch"),
868 }
869 }
870
871 #[test]
872 fn apply_dynamic_batch_rewrites_existing_tensor() {
873 use nxpu_ir::*;
874
875 let mut module = Module::default();
876 let tensor_ty = module.types.insert(Type {
877 name: None,
878 inner: TypeInner::Tensor {
879 scalar: Scalar::F32,
880 shape: TensorShape {
881 dims: vec![
882 Dimension::Fixed(1),
883 Dimension::Fixed(224),
884 Dimension::Fixed(224),
885 Dimension::Fixed(3),
886 ],
887 },
888 },
889 });
890 let handle = module.global_variables.append(GlobalVariable {
891 name: Some("image".into()),
892 space: AddressSpace::Storage {
893 access: StorageAccess::LOAD,
894 },
895 binding: None,
896 ty: tensor_ty,
897 init: None,
898 layout: None,
899 });
900
901 apply_dynamic_batch(&mut module);
902
903 let new_ty = &module.types[module.global_variables[handle].ty].inner;
904 match new_ty {
905 TypeInner::Tensor { shape, .. } => {
906 assert_eq!(shape.dims[0], Dimension::Symbolic("batch".into()));
907 assert_eq!(shape.dims[1], Dimension::Fixed(224));
908 assert_eq!(shape.dims[2], Dimension::Fixed(224));
909 assert_eq!(shape.dims[3], Dimension::Fixed(3));
910 }
911 _ => panic!("expected Tensor type"),
912 }
913 }
914
915 #[test]
916 fn apply_dynamic_batch_skips_uniform() {
917 use nxpu_ir::*;
918
919 let mut module = Module::default();
920 let u32_ty = module.types.insert(Type {
921 name: None,
922 inner: TypeInner::Scalar(Scalar::U32),
923 });
924 let params_ty = module.types.insert(Type {
925 name: Some("Params".into()),
926 inner: TypeInner::Struct {
927 members: vec![StructMember {
928 name: Some("N".into()),
929 ty: u32_ty,
930 offset: 0,
931 }],
932 span: 4,
933 },
934 });
935 let handle = module.global_variables.append(GlobalVariable {
936 name: Some("params".into()),
937 space: AddressSpace::Uniform,
938 binding: None,
939 ty: params_ty,
940 init: None,
941 layout: None,
942 });
943
944 apply_dynamic_batch(&mut module);
945
946 assert_eq!(module.global_variables[handle].ty, params_ty);
948 }
949
950 #[test]
951 fn missing_input_error_message() {
952 let err = miette::miette!("input file is required (use --list-targets to list backends)");
953 let msg = format!("{err}");
954 assert!(msg.contains("input file is required"));
955 assert!(msg.contains("--list-targets"));
956 }
957
958 #[test]
961 fn cli_calibration_defaults() {
962 let cli = Cli::try_parse_from(["nxpu", "input.wgsl"]).unwrap();
963 assert!(cli.calibration_data.is_none());
964 assert_eq!(cli.calibration_method, nxpu_opt::CalibrationMethod::MinMax);
965 assert!(!cli.verbose);
966 }
967
968 #[test]
969 fn cli_calibration_data_flag() {
970 let cli =
971 Cli::try_parse_from(["nxpu", "input.wgsl", "--calibration-data", "/tmp/cal_data"])
972 .unwrap();
973 assert_eq!(
974 cli.calibration_data.unwrap(),
975 PathBuf::from("/tmp/cal_data")
976 );
977 }
978
979 #[test]
980 fn cli_calibration_method_minmax() {
981 let cli =
982 Cli::try_parse_from(["nxpu", "input.wgsl", "--calibration-method", "minmax"]).unwrap();
983 assert_eq!(cli.calibration_method, nxpu_opt::CalibrationMethod::MinMax);
984 }
985
986 #[test]
987 fn cli_calibration_method_percentile() {
988 let cli = Cli::try_parse_from(["nxpu", "input.wgsl", "--calibration-method", "percentile"])
989 .unwrap();
990 assert_eq!(
991 cli.calibration_method,
992 nxpu_opt::CalibrationMethod::Percentile(99.99)
993 );
994 }
995
996 #[test]
997 fn cli_calibration_method_kl() {
998 let cli = Cli::try_parse_from([
999 "nxpu",
1000 "input.wgsl",
1001 "--calibration-method",
1002 "kl-divergence",
1003 ])
1004 .unwrap();
1005 assert_eq!(
1006 cli.calibration_method,
1007 nxpu_opt::CalibrationMethod::KlDivergence
1008 );
1009 }
1010
1011 #[test]
1012 fn cli_calibration_method_invalid() {
1013 let result = Cli::try_parse_from(["nxpu", "input.wgsl", "--calibration-method", "invalid"]);
1014 assert!(result.is_err());
1015 }
1016
1017 #[test]
1018 fn cli_verbose_flag() {
1019 let cli = Cli::try_parse_from(["nxpu", "input.wgsl", "--verbose"]).unwrap();
1020 assert!(cli.verbose);
1021 }
1022
1023 #[test]
1024 fn cli_verbose_short_flag() {
1025 let cli = Cli::try_parse_from(["nxpu", "input.wgsl", "-v"]).unwrap();
1026 assert!(cli.verbose);
1027 }
1028
1029 #[test]
1032 fn calibration_method_valid_values() {
1033 assert_eq!(
1034 parse_calibration_method("minmax").unwrap(),
1035 nxpu_opt::CalibrationMethod::MinMax
1036 );
1037 assert_eq!(
1038 parse_calibration_method("percentile").unwrap(),
1039 nxpu_opt::CalibrationMethod::Percentile(99.99)
1040 );
1041 assert_eq!(
1042 parse_calibration_method("kl-divergence").unwrap(),
1043 nxpu_opt::CalibrationMethod::KlDivergence
1044 );
1045 }
1046
1047 #[test]
1048 fn calibration_method_invalid_value() {
1049 let err = parse_calibration_method("bogus").unwrap_err();
1050 assert!(err.contains("invalid calibration method"));
1051 assert!(err.contains("bogus"));
1052 }
1053}