apr_cli/extended_commands.rs
1/// Attention implementation under test for `apr kernel parity --impl`.
2///
3/// `Flash2` names the pinned `hf-kernels-community:flash-attn2@<sha>` CUDA
4/// kernel. This binary embeds no such kernel, so selecting it is REFUSED —
5/// never quietly answered by `Tiled` under flash2's name, which is the
6/// fabricated-provenance failure CRUX-L-02 exists to prevent.
7#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
8pub enum KernelImpl {
9 /// In-tree tiled online-softmax kernel (`realizar::brick::FlashAttentionBrick`).
10 Tiled,
11 /// Pinned hf-kernels-community flash-attn2 CUDA kernel (not embedded here).
12 Flash2,
13}
14
15/// Reference implementation for `apr kernel parity --ref`.
16#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
17pub enum KernelRef {
18 /// Materialised-score softmax attention, computed in f32 on the CPU.
19 Naive,
20}
21
22/// 2-D projection for `apr debug embed-viz --projection`.
23#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
24pub enum EmbedProjection {
25 /// Exact PCA onto the top 2 principal components (deterministic).
26 Pca,
27 /// Seeded Johnson–Lindenstrauss random projection (deterministic in --seed).
28 Random,
29 /// Not implemented in this binary — selecting it is refused, not substituted.
30 Umap,
31}
32
33/// Extended CLI commands (analysis, profiling, QA, benchmarks, and advanced tools).
34///
35/// Flattened into `Commands` via `#[command(flatten)]` so all subcommands remain
36/// top-level from the user's perspective (e.g., `apr chat`, `apr profile`).
37#[derive(Subcommand, Debug)]
38pub enum ExtendedCommands {
39 /// Interactive chat with language model
40 Chat {
41 /// Path to .apr model file
42 #[arg(value_name = "FILE")]
43 file: PathBuf,
44 /// Sampling temperature (0 = greedy, higher = more random)
45 #[arg(long, default_value = "0.7")]
46 temperature: f32,
47 /// Nucleus sampling threshold
48 #[arg(long, default_value = "0.9")]
49 top_p: f32,
50 /// Maximum tokens to generate per response
51 #[arg(long, default_value = "512")]
52 max_tokens: usize,
53 /// System prompt to set model behavior
54 #[arg(long)]
55 system: Option<String>,
56 /// Show inspection info (top-k probs, tokens/sec)
57 #[arg(long)]
58 inspect: bool,
59 /// Disable GPU acceleration (use CPU)
60 #[arg(long)]
61 no_gpu: bool,
62 /// Force GPU acceleration (requires CUDA)
63 #[arg(long)]
64 gpu: bool,
65 /// Enable inference tracing (APR-TRACE-001)
66 #[arg(long)]
67 trace: bool,
68 /// Trace specific steps only (comma-separated)
69 #[arg(long, value_delimiter = ',')]
70 trace_steps: Option<Vec<String>>,
71 /// Verbose tracing
72 #[arg(long)]
73 trace_verbose: bool,
74 /// Save trace output to JSON file
75 #[arg(long, value_name = "FILE")]
76 trace_output: Option<PathBuf>,
77 /// Trace detail level (none, basic, layer, payload)
78 #[arg(long, value_name = "LEVEL", default_value = "basic", value_parser = TRACE_LEVEL_VALUES)]
79 trace_level: String,
80 /// Enable inline Roofline profiling (PMAT-SHOWCASE-METHODOLOGY-001)
81 #[arg(long)]
82 profile: bool,
83 // PMAT-488 / #2583: shared `--backend` declaration (see `BackendArg`).
84 #[command(flatten)]
85 backend: BackendArg,
86 },
87 /// Benchmark throughput (spec H12: >= 10 tok/s)
88 Bench {
89 /// Path to model file
90 #[arg(value_name = "FILE")]
91 file: PathBuf,
92 /// Number of warmup iterations
93 #[arg(long, default_value = "3")]
94 warmup: usize,
95 /// Number of measurement iterations
96 #[arg(long, default_value = "5")]
97 iterations: usize,
98 /// Max tokens to generate per iteration
99 #[arg(long, default_value = "32")]
100 max_tokens: usize,
101 /// Test prompt
102 #[arg(long)]
103 prompt: Option<String>,
104 /// Use realizar for fast inference (vs aprender baseline)
105 #[arg(long)]
106 fast: bool,
107 /// Benchmark specific brick
108 #[arg(long)]
109 brick: Option<String>,
110 /// Comma-separated latency percentile points for JSON output
111 /// (CRUX-E-07). Default: `50,95,99`. Values must be in (0, 100].
112 #[arg(
113 long,
114 value_delimiter = ',',
115 default_value = "50,95,99",
116 value_parser = crate::commands::bench::parse_percentile
117 )]
118 percentiles: Vec<f64>,
119 },
120 /// Evaluate model perplexity (spec H13: PPL <= 20) or classification metrics
121 Eval {
122 /// Path to model file or checkpoint directory
123 #[arg(value_name = "FILE")]
124 file: PathBuf,
125 /// Dataset: wikitext-2, lambada, or custom
126 #[arg(long, default_value = "wikitext-2")]
127 dataset: String,
128 /// Custom text (when dataset=custom)
129 #[arg(long)]
130 text: Option<String>,
131 /// Maximum tokens to evaluate
132 #[arg(long, default_value = "512")]
133 max_tokens: usize,
134 /// Perplexity threshold for pass/fail
135 #[arg(long, default_value = "20.0",
136 value_parser = commands::threshold_arg::parse_tolerance_f32)]
137 threshold: f32,
138 /// Task type: omit for perplexity, "classify" for classification eval
139 #[arg(long)]
140 task: Option<String>,
141 /// Test data file (JSONL) for classification evaluation
142 #[arg(long, value_name = "FILE")]
143 data: Option<PathBuf>,
144 /// Model size hint: "0.5B", "tiny" (for classification eval)
145 #[arg(long)]
146 model_size: Option<String>,
147 /// Number of output classes (default: 5)
148 #[arg(long, default_value = "5")]
149 num_classes: usize,
150 /// Generate HuggingFace model card (README.md) in checkpoint dir
151 #[arg(long)]
152 generate_card: bool,
153 /// Device for inference: "cpu" (default) or "cuda" (GPU-accelerated, ALB-089).
154 /// Applies to --task humaneval/mbpp; perplexity evaluation is CPU-only.
155 #[arg(long, default_value = "cpu", value_parser = ["cpu", "cuda"])]
156 device: String,
157 /// Number of samples per problem for pass@k (ALB-088, default: 1)
158 #[arg(long, default_value = "1")]
159 samples: usize,
160 /// Sampling temperature (0.0 = greedy, 0.8 = standard for pass@k>1)
161 #[arg(long, default_value = "0.0")]
162 temperature: f32,
163 },
164 /// Deep profiling with Roofline analysis
165 Profile {
166 /// Path to model file
167 #[arg(value_name = "FILE")]
168 file: PathBuf,
169 /// Layer-by-layer granular analysis
170 #[arg(long)]
171 granular: bool,
172 /// Output format (human, json, flamegraph)
173 #[arg(long, default_value = "human")]
174 format: String,
175 /// Focus on specific operation
176 #[arg(long)]
177 focus: Option<String>,
178 /// Detect naive implementations
179 #[arg(long)]
180 detect_naive: bool,
181 /// Achieved-GFLOPS floor below which the run is reported as naive
182 #[arg(long, default_value = "10.0",
183 value_parser = commands::threshold_arg::parse_tolerance)]
184 threshold: f64,
185 /// [NOT IMPLEMENTED — accepted and ignored] Compare against HuggingFace baseline
186 #[arg(long)]
187 compare_hf: Option<String>,
188 /// [NOT IMPLEMENTED — accepted and ignored] Measure energy consumption (requires RAPL)
189 #[arg(long)]
190 energy: bool,
191 /// Compute performance grade (vs Ollama baseline)
192 #[arg(long)]
193 perf_grade: bool,
194 /// [NOT IMPLEMENTED — accepted and ignored] Show call graph
195 #[arg(long)]
196 callgraph: bool,
197 /// Exit non-zero if naive implementation detected (implies --detect-naive)
198 #[arg(long)]
199 fail_on_naive: bool,
200 /// Output file path for flamegraph SVG (GH-174, PMAT-182)
201 #[arg(long, short = 'o')]
202 output: Option<PathBuf>,
203
204 // PMAT-192: CI Assertion Mode (GH-180)
205 /// Enable CI mode with assertion checks (exits 1 on failure)
206 #[arg(long)]
207 ci: bool,
208 /// Minimum throughput in tok/s (CI assertion, exits 1 if below)
209 #[arg(long, value_parser = commands::threshold_arg::parse_tolerance)]
210 assert_throughput: Option<f64>,
211 /// Maximum p99 latency in ms (CI assertion, exits 1 if above)
212 #[arg(long, value_parser = commands::threshold_arg::parse_tolerance)]
213 assert_p99: Option<f64>,
214 /// Maximum p50 latency in ms (CI assertion, exits 1 if above)
215 #[arg(long, value_parser = commands::threshold_arg::parse_tolerance)]
216 assert_p50: Option<f64>,
217 /// Warmup passes before measurement (default: 3)
218 #[arg(long, default_value = "3")]
219 warmup: usize,
220 /// Measurement passes (default: 10)
221 #[arg(long, default_value = "10")]
222 measure: usize,
223 /// Tokens generated per measurement pass — GPU and --ollama paths only;
224 /// the CPU per-operation profiler measures one forward pass per pass
225 #[arg(long, default_value = "32")]
226 tokens: usize,
227 /// Compare against Ollama baseline (runs ollama for comparison)
228 #[arg(long)]
229 ollama: bool,
230 /// Disable GPU (force CPU-only profiling)
231 #[arg(long)]
232 no_gpu: bool,
233 /// Compare against another model format (F-PROFILE-011)
234 #[arg(long, value_name = "FILE")]
235 compare: Option<PathBuf>,
236 },
237 /// Falsifiable QA checklist for model releases
238 Qa {
239 /// Path to model file
240 #[arg(value_name = "FILE")]
241 file: PathBuf,
242 /// Minimum throughput threshold in tok/s
243 #[arg(long, value_name = "TPS",
244 value_parser = commands::threshold_arg::parse_tolerance)]
245 assert_tps: Option<f64>,
246 /// Minimum speedup vs Ollama
247 #[arg(long, value_name = "SPEEDUP",
248 value_parser = commands::threshold_arg::parse_tolerance)]
249 assert_speedup: Option<f64>,
250 /// Minimum GPU vs CPU speedup (F-PERF-042)
251 #[arg(long, value_name = "SPEEDUP",
252 value_parser = commands::threshold_arg::parse_tolerance)]
253 assert_gpu_speedup: Option<f64>,
254 /// Skip golden output test
255 #[arg(long)]
256 skip_golden: bool,
257 /// Skip throughput benchmark
258 #[arg(long)]
259 skip_throughput: bool,
260 /// Skip Ollama parity comparison
261 #[arg(long)]
262 skip_ollama: bool,
263 /// Skip GPU vs CPU speedup test (F-PERF-042)
264 #[arg(long)]
265 skip_gpu_speedup: bool,
266 /// Skip tensor contract validation (PMAT-235)
267 #[arg(long)]
268 skip_contract: bool,
269 /// Skip cross-format parity test (F-QUAL-032)
270 #[arg(long)]
271 skip_format_parity: bool,
272 /// Skip PTX parity validation (GH-219)
273 #[arg(long)]
274 skip_ptx_parity: bool,
275 /// SafeTensors model path for cross-format parity test (F-QUAL-032)
276 #[arg(long, value_name = "PATH")]
277 safetensors_path: Option<PathBuf>,
278 /// Number of benchmark iterations
279 #[arg(long, default_value = "10")]
280 iterations: usize,
281 /// Number of warmup iterations
282 #[arg(long, default_value = "3")]
283 warmup: usize,
284 /// Maximum tokens to generate
285 #[arg(long, default_value = "32")]
286 max_tokens: usize,
287 /// Output as JSON (for CI integration)
288 #[arg(long)]
289 json: bool,
290 /// Verbose output
291 #[arg(short, long)]
292 verbose: bool,
293 /// Minimum number of gates that must execute (fail if fewer)
294 #[arg(long, value_name = "N")]
295 min_executed: Option<usize>,
296 /// Previous QA report for regression detection
297 #[arg(long, value_name = "FILE")]
298 previous_report: Option<PathBuf>,
299 /// Maximum allowed performance regression ratio (default: 0.10 = 10%)
300 #[arg(long, value_name = "RATIO",
301 value_parser = commands::threshold_arg::parse_fraction)]
302 regression_threshold: Option<f64>,
303 /// Skip GPU state isolation test
304 #[arg(long)]
305 skip_gpu_state: bool,
306 /// Skip metadata plausibility validation (Bug 210, GH-222)
307 #[arg(long)]
308 skip_metadata: bool,
309 /// Skip GPU capability match gate (GH-280)
310 #[arg(long)]
311 skip_capability: bool,
312 /// Assert classifier head presence and shape (F-CLASS-004)
313 #[arg(long)]
314 assert_classifier_head: bool,
315 },
316 /// GPU/CPU parity check (PMAT-232: genchi genbutsu — see where GPU diverges)
317 Parity {
318 /// Path to GGUF model file
319 #[arg(value_name = "FILE")]
320 file: PathBuf,
321 /// Prompt text (default: "What is 2+2?")
322 #[arg(short, long, default_value = "What is 2+2?")]
323 prompt: String,
324 /// Assert parity (exit non-zero on divergence)
325 #[arg(long)]
326 assert: bool,
327 },
328 /// Model-to-PTX source mapping (Mieruka: make GPU kernel dispatch visible)
329 #[command(name = "ptx-map")]
330 PtxMap {
331 /// Path to GGUF model file
332 #[arg(value_name = "FILE")]
333 file: PathBuf,
334 /// Filter to specific kernel (e.g., --kernel Q4KGemv)
335 #[arg(long)]
336 kernel: Option<String>,
337 /// Reverse lookup: kernel name -> which layers/steps use it
338 #[arg(long)]
339 reverse: Option<String>,
340 /// Output as JSON
341 #[arg(long)]
342 json: bool,
343 /// Full PTX snippets and detailed analysis
344 #[arg(short, long)]
345 verbose: bool,
346 /// Show batched prefill kernel variants instead of decode
347 #[arg(long)]
348 prefill: bool,
349 },
350 /// PTX analysis and bug detection (register pressure, roofline)
351 ///
352 /// #2399 finding 1: on a build without the analyzer this line is the only
353 /// thing a user sees before running the command, so it has to say so.
354 #[cfg_attr(feature = "trueno-explain", command(name = "ptx"))]
355 #[cfg_attr(
356 not(feature = "trueno-explain"),
357 command(
358 name = "ptx",
359 about = "PTX analysis and bug detection [unavailable in this build: cargo install aprender --features ptx]"
360 )
361 )]
362 Ptx {
363 /// Path to a PTX source file
364 #[arg(value_name = "FILE")]
365 file: Option<PathBuf>,
366 /// Analyze a named kernel from trueno-gpu
367 #[arg(long, short)]
368 kernel: Option<String>,
369 /// Strict mode (no performance whitelist)
370 #[arg(long)]
371 strict: bool,
372 /// Show only bug analysis (skip register/memory/roofline)
373 #[arg(long)]
374 bugs: bool,
375 /// Output as JSON
376 #[arg(long)]
377 json: bool,
378 /// Verbose output (include PTX source listing)
379 #[arg(short, long)]
380 verbose: bool,
381 },
382 /// ML tuning: LoRA/QLoRA configuration, memory planning, and HPO (GH-176, SPEC-TUNE-2026-001)
383 #[cfg(feature = "training")]
384 Tune {
385 /// Path to model file (optional if using --model)
386 #[arg(value_name = "FILE")]
387 file: Option<PathBuf>,
388 /// Tuning method: auto, full, lora, qlora
389 #[arg(long, short = 'm', default_value = "auto")]
390 method: String,
391 /// LoRA rank (default: auto-selected)
392 #[arg(long, short = 'r')]
393 rank: Option<u32>,
394 /// Available VRAM in GB
395 #[arg(long, default_value = "16.0")]
396 vram: f64,
397 /// Only plan configuration, don't train
398 #[arg(long)]
399 plan: bool,
400 /// Model size for planning (e.g., "7B", "1.5B")
401 #[arg(long, value_name = "SIZE")]
402 model: Option<String>,
403 /// Freeze base model weights
404 #[arg(long)]
405 freeze_base: bool,
406 /// Training data file (JSONL format)
407 #[arg(long, value_name = "FILE")]
408 train_data: Option<PathBuf>,
409 /// Output as JSON (for CI integration)
410 #[arg(long)]
411 json: bool,
412 /// Task type for HPO: classify (SPEC-TUNE-2026-001)
413 #[arg(long)]
414 task: Option<String>,
415 /// Number of HPO trials (default: 10)
416 #[arg(long, default_value = "10")]
417 budget: usize,
418 /// HPO search strategy: tpe, grid, random
419 #[arg(long, default_value = "tpe")]
420 strategy: String,
421 /// HPO scheduler: asha, median, none
422 #[arg(long, default_value = "asha")]
423 scheduler: String,
424 /// Scout mode: 1 epoch per trial for fast exploration
425 #[arg(long)]
426 scout: bool,
427 /// Training data file for HPO (JSONL format)
428 #[arg(long, value_name = "FILE")]
429 data: Option<PathBuf>,
430 /// Number of output classes for classification
431 #[arg(long, default_value = "5")]
432 num_classes: usize,
433 /// Model size hint for HPO (e.g., "0.5B", "1.5B")
434 #[arg(long)]
435 model_size: Option<String>,
436 /// Warm-start from scout phase results directory
437 #[arg(long, value_name = "DIR")]
438 from_scout: Option<PathBuf>,
439 /// Maximum epochs per trial (full mode, default: 20)
440 #[arg(long, default_value = "20")]
441 max_epochs: usize,
442 /// Maximum wall-clock time (e.g., "8h", "30m")
443 #[arg(long)]
444 time_limit: Option<String>,
445 },
446 /// Attach live TUI to a running training session
447 #[cfg(feature = "training")]
448 Monitor {
449 /// Experiment output directory (same as finetune -o)
450 #[arg(value_name = "DIR")]
451 dir: Option<PathBuf>,
452 /// Refresh interval in milliseconds
453 #[arg(long, default_value = "500")]
454 refresh_ms: u64,
455 /// Compact display mode
456 #[arg(long)]
457 compact: bool,
458 /// Output JSON lines instead of TUI (for LLM agents and CI)
459 #[arg(long)]
460 json: bool,
461 /// Output format: tui (default), json, text
462 #[arg(long, default_value = "tui")]
463 format: String,
464 },
465 /// List, show, and compare training experiment runs
466 #[cfg(feature = "training")]
467 Runs {
468 #[command(subcommand)]
469 command: RunsCommands,
470 },
471 /// Interactive experiment browser (TUI with loss curves)
472 #[cfg(feature = "training")]
473 Experiment {
474 #[command(subcommand)]
475 command: ExperimentCommands,
476 },
477 /// ComputeBrick pipeline monitor (cbtop)
478 Cbtop {
479 /// Model name (e.g., qwen2.5-coder-1.5b)
480 #[arg(long)]
481 model: Option<String>,
482 /// Attach to running realizar process
483 #[arg(long)]
484 attach: Option<String>,
485 /// Path to GGUF model file for real profiling
486 #[arg(long, value_name = "MODEL")]
487 model_path: Option<PathBuf>,
488 /// Run in headless mode (no TUI, for CI/automation)
489 #[arg(long)]
490 headless: bool,
491 /// Output JSON format (requires --headless)
492 #[arg(long, requires = "headless")]
493 json: bool,
494 /// Output file path (requires --headless)
495 #[arg(long, value_name = "FILE", requires = "headless")]
496 output: Option<PathBuf>,
497 /// CI mode: exit non-zero if thresholds are not met or the report status is FAIL
498 #[arg(long)]
499 ci: bool,
500 /// Minimum throughput threshold in tok/s (for --ci)
501 #[arg(long, value_name = "TOK_S",
502 value_parser = commands::threshold_arg::parse_tolerance)]
503 throughput: Option<f64>,
504 /// Minimum brick score threshold 0-100 (for --ci)
505 #[arg(long, value_name = "SCORE")]
506 brick_score: Option<u32>,
507 /// Number of warmup iterations before measurement
508 #[arg(long, default_value = "10")]
509 warmup: usize,
510 /// Number of measurement iterations (must be >= 1)
511 #[arg(long, default_value = "100", value_parser = parse_cbtop_iterations)]
512 iterations: usize,
513 /// PAR-100: Enable speculative decoding benchmark
514 #[arg(long)]
515 speculative: bool,
516 /// PAR-100: Number of tokens to draft speculatively (default: 4)
517 #[arg(long, default_value = "4")]
518 speculation_k: usize,
519 /// PAR-099: Path to draft model for speculative decoding
520 #[arg(long, value_name = "DRAFT_MODEL")]
521 draft_model: Option<PathBuf>,
522 /// PAR-102: Number of concurrent requests
523 #[arg(long, default_value = "1")]
524 concurrent: usize,
525 /// Use simulated data (for CI testing only)
526 #[arg(long)]
527 simulated: bool,
528 },
529 /// Test harness for web, LLM, media and replay — powered by probador.
530 ///
531 /// Named for what it tests, not for the act of testing. `probar` is Spanish
532 /// for "to try"; it named the VERB, so `apr probar --help` told a reader
533 /// nothing about the subject. This follows the precedent already set by
534 /// `apr data` ("Data quality pipeline ... powered by alimentar"): a plain
535 /// noun for the user-facing command, the Spanish name kept for the engine
536 /// and credited in the description.
537 ///
538 /// The harness covers four distinct things, and the subcommands group by
539 /// what is UNDER TEST rather than by verb:
540 ///
541 /// web the WASM/browser build and its runtime behaviour
542 /// (serve, build, watch, comply, stress)
543 /// llm inference correctness, throughput and cost against an endpoint
544 /// (test, load, bench, sweep, score, experiment, data-audit)
545 /// media rendered output against ground truth
546 /// (av-sync, audio, video, animation)
547 /// replay the runner itself — recording, state machines, reporting
548 /// (record, playbook, coverage, report)
549 ///
550 /// Only `tensor` is routed today (PMAT-481 visual regression); the rest
551 /// land as they are delegated to the probador library. Renaming now costs
552 /// one path — after those land it is a breaking change across the whole
553 /// testing surface.
554 ///
555 /// `apr probar` stays as a hidden alias so existing scripts keep working.
556 #[command(alias = "probar")]
557 Test {
558 #[command(subcommand)]
559 command: TestSubcommand,
560 },
561 /// Compare APR model against HuggingFace source
562 #[command(name = "compare-hf")]
563 CompareHf {
564 /// Path to .apr model file
565 #[arg(value_name = "FILE")]
566 file: PathBuf,
567 /// HuggingFace repo ID (e.g., openai/whisper-tiny)
568 #[arg(long)]
569 hf: String,
570 /// Filter tensors by name pattern
571 #[arg(long)]
572 tensor: Option<String>,
573 /// Comparison threshold (default: 1e-5)
574 #[arg(long, default_value = "1e-5",
575 value_parser = commands::threshold_arg::parse_tolerance)]
576 threshold: f64,
577 /// Output as JSON
578 #[arg(long)]
579 json: bool,
580 },
581 /// CRUX-K-11: parse Ollama-style Modelfile DSL into apr config.
582 Modelfile {
583 #[command(subcommand)]
584 command: ModelfileSubcommand,
585 },
586 /// Format-aware binary forensics (10X better than xxd)
587 Hex {
588 /// Path to model file (APR, GGUF, or SafeTensors)
589 #[arg(value_name = "FILE")]
590 file: PathBuf,
591 /// Filter tensors by name pattern
592 #[arg(long)]
593 tensor: Option<String>,
594 /// Limit bytes/values to display
595 #[arg(long, default_value = "64")]
596 limit: usize,
597 /// Show tensor statistics
598 #[arg(long)]
599 stats: bool,
600 /// List tensor names only
601 #[arg(long)]
602 list: bool,
603 /// Output as JSON
604 #[arg(long)]
605 json: bool,
606 /// Annotated file header (magic, version, tensor count, metadata)
607 #[arg(long)]
608 header: bool,
609 /// Q4K/Q6K/Q8_0 super-block structure with field annotations
610 #[arg(long)]
611 blocks: bool,
612 /// Value histogram + entropy + kurtosis analysis
613 #[arg(long)]
614 distribution: bool,
615 /// Layout contract verification overlay per tensor
616 #[arg(long)]
617 contract: bool,
618 /// Per-region byte entropy analysis
619 #[arg(long)]
620 entropy: bool,
621 /// Raw bytes (like xxd but format-aware, with ASCII column)
622 #[arg(long)]
623 raw: bool,
624 /// Start at byte offset (supports 0x prefix for hex)
625 #[arg(long, default_value = "0")]
626 offset: String,
627 /// Bytes per row for raw output (default: 16)
628 #[arg(long, default_value = "16")]
629 width: usize,
630 /// Slice range for partial tensor reads (e.g., 0:3 for first 3 elements)
631 #[arg(long)]
632 slice: Option<String>,
633 },
634 /// Model architecture tree view
635 Tree {
636 /// Path to .apr model file
637 #[arg(value_name = "FILE")]
638 file: PathBuf,
639 /// Filter by component pattern
640 #[arg(long)]
641 filter: Option<String>,
642 /// Output format: ascii, dot, mermaid, json
643 ///
644 /// #2394 finding 15: this was a `String` that the dispatcher parsed
645 /// with `.unwrap_or(TreeFormat::Ascii)`, so `--format bogusvalue`
646 /// silently rendered ascii and exited 0 — a typo'd `--format josn` in
647 /// a pipeline produced a tree instead of JSON, with no warning. Parsing
648 /// at the CLI boundary makes the unparseable value unrepresentable
649 /// downstream: clap rejects it before any command runs.
650 #[arg(long, default_value = "ascii")]
651 format: crate::commands::tree::TreeFormat,
652 /// Show tensor sizes
653 #[arg(long)]
654 sizes: bool,
655 /// Maximum tree depth
656 #[arg(long)]
657 depth: Option<usize>,
658 },
659 /// Data flow visualization
660 Flow {
661 /// Path to .apr model file
662 #[arg(value_name = "FILE")]
663 file: PathBuf,
664 /// Filter by layer pattern
665 #[arg(long)]
666 layer: Option<String>,
667 /// Component to visualize: full, encoder, decoder, etc.
668 #[arg(long, default_value = "full")]
669 component: String,
670 /// Verbose output with statistics
671 #[arg(short, long)]
672 verbose: bool,
673 /// Output as JSON
674 #[arg(long)]
675 json: bool,
676 },
677 /// Cross-subcommand smoke test (does every tool handle this model?)
678 Qualify {
679 /// Path to model file (APR, GGUF, or SafeTensors)
680 #[arg(value_name = "FILE")]
681 file: PathBuf,
682 /// Testing tier: smoke (Phase 1), standard (+contracts), full (+playbook)
683 #[arg(long, default_value = "smoke")]
684 tier: String,
685 /// Timeout per gate in seconds
686 #[arg(long, default_value = "120")]
687 timeout: u64,
688 /// Output as JSON
689 #[arg(long)]
690 json: bool,
691 /// Show subcommand output (disable stdout suppression)
692 #[arg(short, long)]
693 verbose: bool,
694 /// Skip specific gates (comma-separated)
695 #[arg(long, value_delimiter = ',')]
696 skip: Option<Vec<String>>,
697 },
698 /// Training pipeline (plan/apply) — forjar-style pre-flight validation
699 #[cfg(feature = "training")]
700 Train {
701 #[command(subcommand)]
702 command: TrainCommands,
703 },
704 /// Pretraining loop driver (SHIP-TWO-001 MODEL-2).
705 ///
706 /// Wires the pretraining loop shape defined by
707 /// `contracts/training-loop-pretrain-v1.yaml`. Executes a synthetic
708 /// decreasing-loss drive by default so GATE-TRAIN-005 / -007 / -008
709 /// divergence-and-NaN guards can be exercised without an actual
710 /// 370M compute run. Real corpus wiring is a follow-up ticket.
711 #[cfg(feature = "training")]
712 Pretrain {
713 /// Dataset path (tokenized shard index or raw corpus).
714 #[arg(long, value_name = "PATH")]
715 dataset: PathBuf,
716 /// Tokenizer directory (vocab.json + merges.txt).
717 #[arg(long, value_name = "DIR")]
718 tokenizer: PathBuf,
719 /// Run output directory — checkpoints + metadata go to `{run_dir}/ckpt/`.
720 #[arg(long, value_name = "DIR")]
721 run_dir: PathBuf,
722 /// Training regime — finetune (MODEL-1) or from-scratch (MODEL-2 cold start).
723 /// Per contract training-loop-pretrain-v1 §hyperparameter_defaults,
724 /// this atomically flips (regime, lr_max, warmup_steps, target_val_loss)
725 /// unless explicit --lr / --warmup-steps / --target-val-loss override.
726 #[arg(long, value_enum, default_value = "finetune")]
727 mode: PretrainMode,
728 /// Peak learning rate after warmup. Omit to inherit mode default
729 /// (finetune: 5e-5, from-scratch: 3e-4).
730 #[arg(long)]
731 lr: Option<f32>,
732 /// Warmup + cosine decay total steps.
733 #[arg(long, default_value = "1000")]
734 num_steps: usize,
735 /// Number of warmup steps. Omit to inherit mode default
736 /// (finetune: 100, from-scratch: 1000).
737 #[arg(long)]
738 warmup_steps: Option<usize>,
739 /// Micro-batch size.
740 #[arg(long, default_value = "16")]
741 batch_size: usize,
742 /// Sequence length per example.
743 #[arg(long, default_value = "1024")]
744 seq_length: usize,
745 /// Steps per epoch — controls per-epoch artifact cadence.
746 #[arg(long, default_value = "100")]
747 steps_per_epoch: usize,
748 /// GATE-TRAIN-006 fixed RNG seed.
749 #[arg(long, default_value = "42")]
750 seed: u64,
751 /// Target val_loss. Omit to inherit mode default
752 /// (finetune: 2.2, from-scratch: 3.0).
753 #[arg(long, value_parser = commands::threshold_arg::parse_tolerance_f32)]
754 target_val_loss: Option<f32>,
755 /// Vocabulary size (required for `--mode from-scratch` INV-TRAIN-005
756 /// regime-dependent cap: 2·ln(vocab_size)). MODEL-2 uses 50257.
757 #[arg(long, default_value = "50257")]
758 vocab_size: u32,
759 /// Synthetic-drive only — do not attempt real compute, exercise loop gates only.
760 /// INV-TRAIN-010: absent = real compute (drive_real), present = synthetic (drive_synthetic).
761 #[arg(long, action = clap::ArgAction::SetTrue)]
762 synthetic: bool,
763 /// Training backend. Grammar (contract gpu-training-backend-v1
764 /// INV-GPUTRAIN-001): `^(cpu|cuda(:[0-9]|:1[0-5])?|auto)$`.
765 /// Default `auto` uses CUDA if available, else CPU (the only
766 /// spelling that may fall back silently — all other values
767 /// hard-fail on missing runtime per GATE-GPUTRAIN-002).
768 #[arg(long, default_value = "auto")]
769 device: String,
770 /// Initial weights from a pretrained APR file
771 /// (contract `apr-pretrain-from-init-v1`). Per spec §49's
772 /// MODEL-2 pretrained-init pivot: when present, load weights
773 /// from `<PATH>` instead of random-init. Composes with
774 /// `--mode finetune` (canonical) or `--mode from-scratch`
775 /// (allowed but non-canonical — emits a warning). Missing,
776 /// corrupted, or arch-mismatched APR files exit non-zero
777 /// before step 1 (no silent random-init fallback).
778 #[arg(long, value_name = "PATH")]
779 init: Option<PathBuf>,
780 /// SPEC §83 P0-J: bypass the Chinchilla compute-optimal hard
781 /// gate (`chinchilla-gate-v1`). Default is fail-fast when
782 /// D/N < 10× (severely under-provisioned per Hoffmann et al.
783 /// 2022). Pass this flag to acknowledge the under-provisioning
784 /// and proceed anyway (e.g. for ablation studies, resumed
785 /// runs, or smoke tests).
786 #[arg(long, action = clap::ArgAction::SetTrue)]
787 force_under_provisioned: bool,
788 /// SPEC §84 P2-F: shared held-out validation shard.
789 ///
790 /// When provided, the val-loss eval reads `HELD_OUT_BATCHES`
791 /// batches from this separate `.bin`-shards directory instead
792 /// of stealing the first 16 batches of `--dataset`. This makes
793 /// `val_loss` comparable across runs whose `--dataset`
794 /// composition changes (P2-C's audit-falsified result was
795 /// confounded by val sets being drawn from different corpus
796 /// distributions — qwen-v2 = codeparrot only, qwen-v3 =
797 /// codeparrot + the-stack-dedup).
798 ///
799 /// Path semantics: directory of `.bin` shards (same format as
800 /// `--dataset`). Operator tokenizes the held-out corpus
801 /// independently via `apr tokenize encode-corpus --max-docs N`
802 /// to a separate output dir, then passes that dir here. The
803 /// shard contract is `contracts/dataset-thestack-python-v1.yaml`.
804 ///
805 /// When omitted, falls back to the historical "first 16
806 /// batches of --dataset" behaviour for backwards compatibility.
807 #[arg(long, value_name = "DIR")]
808 val_shard: Option<PathBuf>,
809 },
810 /// Tokenizer training pipeline (plan/apply) — BPE vocabulary learning
811 Tokenize {
812 #[command(subcommand)]
813 command: TokenizeCommands,
814 },
815 /// Data quality pipeline (audit, split, balance) — powered by alimentar
816 Data {
817 #[command(subcommand)]
818 command: DataCommands,
819 },
820 /// Pipeline orchestration (plan/apply/status) — wraps forjar DAG engine
821 Pipeline {
822 #[command(subcommand)]
823 command: PipelineCommands,
824 },
825 /// Automated Five Whys diagnosis on a training checkpoint
826 Diagnose {
827 /// Path to checkpoint directory
828 #[arg(value_name = "CHECKPOINT_DIR")]
829 checkpoint_dir: PathBuf,
830 /// Test data file (JSONL) for evaluation
831 #[arg(long, value_name = "FILE")]
832 data: Option<PathBuf>,
833 /// Model size hint: "0.5B", "tiny"
834 #[arg(long)]
835 model_size: Option<String>,
836 /// Number of output classes (default: 5)
837 #[arg(long, default_value = "5")]
838 num_classes: usize,
839 },
840 /// Lint an Ollama /api/chat response for schema + NDJSON invariants (CRUX-C-04)
841 OllamaChatLint {
842 /// Path to captured /api/chat response (JSON object, or NDJSON if --stream)
843 #[arg(long, value_name = "FILE")]
844 response_file: PathBuf,
845 /// Treat input as NDJSON stream (one frame per line)
846 #[arg(long)]
847 stream: bool,
848 },
849 /// Lint an Ollama /api/chat function-calling response (CRUX-I-04)
850 OllamaToolsLint {
851 /// Path to captured /api/chat response (JSON object, or NDJSON if --stream)
852 #[arg(long, value_name = "FILE")]
853 response_file: PathBuf,
854 /// Captured request JSON, required unless --stream — supplies the
855 /// tool-name allowlist (every called tool name must appear in
856 /// request.tools[*].function.name)
857 #[arg(long, value_name = "FILE")]
858 request_file: Option<PathBuf>,
859 /// Treat input as NDJSON stream (one frame per line)
860 #[arg(long)]
861 stream: bool,
862 },
863 /// Lint a captured DRY-sampling observation (CRUX-C-23)
864 DrySamplingLint {
865 /// Path to observation JSON
866 #[arg(long, value_name = "FILE")]
867 observation_file: PathBuf,
868 },
869 /// Lint a captured AWQ quality/compression/flags observation (CRUX-B-08)
870 AwqLint {
871 /// Path to captured AWQ observation JSON
872 #[arg(long, value_name = "FILE")]
873 observation_file: PathBuf,
874 },
875 /// Lint a captured FP8 (E4M3) round-trip + SM-capability observation (CRUX-B-11)
876 Fp8Lint {
877 /// Path to captured observation JSON (frobenius, capability blocks)
878 #[arg(long, value_name = "FILE")]
879 observation_file: PathBuf,
880 },
881 /// Lint a captured NF4 codebook/roundtrip/storage/parity observation (CRUX-B-10)
882 Nf4Lint {
883 /// Path to captured NF4 observation JSON
884 #[arg(long, value_name = "FILE")]
885 observation_file: PathBuf,
886 },
887 /// Lint a captured GPTQ compression/cosine/flags observation (CRUX-B-09)
888 GptqLint {
889 /// Path to captured GPTQ observation JSON
890 #[arg(long, value_name = "FILE")]
891 observation_file: PathBuf,
892 },
893 /// Lint a captured CUDA OOM postmortem report (CRUX-F-13)
894 OomLint {
895 /// Path to captured OOM postmortem JSON (e.g. /tmp/apr-oom-<ts>.json)
896 #[arg(long, value_name = "FILE")]
897 report_file: PathBuf,
898 /// Optional captured stderr log to verify the OOM_REPORT breadcrumb
899 #[arg(long, value_name = "FILE")]
900 stderr_file: Option<PathBuf>,
901 },
902 /// Lint a captured NCCL failure-diagnostics JSON from stderr (CRUX-F-15)
903 NcclDiagLint {
904 /// Path to captured stderr JSON diagnostic
905 #[arg(long, value_name = "FILE")]
906 diag_file: PathBuf,
907 /// Optional observed exit code (gate: >= 128 = NCCL class)
908 #[arg(long, value_name = "I32")]
909 exit_code: Option<i32>,
910 /// Require the `suggest` field to cite an nvidia.com / NVIDIA/nccl URL
911 #[arg(long)]
912 require_doc_link: bool,
913 },
914 /// Lint an externally captured ReAct loop trace JSON (CRUX-I-06 — no apr producer yet)
915 ReactTraceLint {
916 /// Path to captured trace JSON
917 #[arg(long, value_name = "FILE")]
918 trace_file: PathBuf,
919 /// Optional max_iterations budget the trace was produced under
920 #[arg(long, value_name = "N")]
921 max_iterations: Option<i64>,
922 /// Require the scratchpad to parse cleanly as Thought/Action/Observation blocks
923 #[arg(long)]
924 require_grammar: bool,
925 },
926 /// Lint a captured `$APR_TRACE_DIR` hang stack-dump directory (CRUX-F-14)
927 HangTraceLint {
928 /// Path to the captured trace directory
929 #[arg(long, value_name = "DIR")]
930 trace_dir: PathBuf,
931 /// Inspection mode: `timeout` (expects per-rank dumps) or `success` (expects empty dir)
932 #[arg(long, value_name = "MODE", default_value = "timeout")]
933 mode: String,
934 /// Expected world_size when mode=timeout (number of rank{N}.py.txt files)
935 #[arg(long, value_name = "N", default_value_t = 2)]
936 world_size: usize,
937 /// Actual exit code from the run under inspection (for exit-code gate)
938 #[arg(long, value_name = "I32")]
939 exit_code: Option<i32>,
940 /// Expected exit code (typically 124 for timeout, 1 for other error, 0 for success)
941 #[arg(long, value_name = "I32")]
942 expected_exit_code: Option<i32>,
943 },
944 /// Lint two externally captured DDP metrics JSONs, N=1 and N=k (CRUX-D-11 — no apr producer yet)
945 DdpMetricsLint {
946 /// Path to N=1 metrics JSON
947 #[arg(long, value_name = "FILE")]
948 metrics_1gpu_file: PathBuf,
949 /// Path to N=world_size metrics JSON
950 #[arg(long, value_name = "FILE")]
951 metrics_ngpu_file: PathBuf,
952 /// World size used for --metrics-ngpu-file run (>= 2)
953 #[arg(long, value_name = "N")]
954 world_size: i64,
955 /// Scaling-efficiency floor (default 0.85, PyTorch DDP convention)
956 #[arg(long, value_name = "F", default_value_t = 0.85,
957 value_parser = commands::threshold_arg::parse_fraction)]
958 scaling_floor: f64,
959 /// Loss-parity relative tolerance (default 0.01)
960 #[arg(long, value_name = "F", default_value_t = 0.01,
961 value_parser = commands::threshold_arg::parse_tolerance)]
962 loss_tolerance: f64,
963 },
964 /// Dataset inspection tools (CRUX-H-13)
965 Dataset {
966 #[command(subcommand)]
967 command: DatasetCommands,
968 },
969 /// Kernel-level parity measurements (CRUX-L-02)
970 Kernel {
971 #[command(subcommand)]
972 command: KernelCommands,
973 },
974 /// Lint an audio-inspect JSON body, e.g. from
975 /// `apr dataset audio-inspect clip.wav --format json -o audio.json` (CRUX-H-13)
976 AudioInspectLint {
977 /// Path to the JSON body written by `apr dataset audio-inspect --format json`
978 #[arg(long, value_name = "FILE")]
979 json_file: PathBuf,
980 /// Optional expected sample_rate (typically the `--resample-to` arg)
981 #[arg(long, value_name = "U32")]
982 expected_sample_rate: Option<u32>,
983 /// Optional expected channel count (1 = mono after --mono)
984 #[arg(long, value_name = "U32")]
985 expected_channels: Option<u32>,
986 },
987 /// Lint attention parity + provenance JSON, e.g. from
988 /// `apr kernel parity --impl tiled --ref naive --json -o parity.json` (CRUX-L-02)
989 AttnParityLint {
990 /// Parity JSON body (`max_abs_diff`, `cosine_sim`), as written by
991 /// `apr kernel parity --json`
992 #[arg(long, value_name = "FILE")]
993 parity_file: Option<PathBuf>,
994 /// Provenance JSON body (`attn_impl`, `kernel_source`, `fallback`).
995 /// `apr kernel parity --json` writes both gates' fields into one body,
996 /// so the same file may be passed here and to --parity-file
997 #[arg(long, value_name = "FILE")]
998 provenance_file: Option<PathBuf>,
999 /// head_dim refusal JSON, as written by
1000 /// `apr kernel parity --impl flash2 --head-dim 96 --json` (which exits non-zero)
1001 #[arg(long, value_name = "FILE")]
1002 head_dim_error_file: Option<PathBuf>,
1003 /// Max absolute diff tolerance (default 5e-3, FlashAttention-2 bound)
1004 #[arg(long, value_name = "F", default_value_t = 5e-3,
1005 value_parser = commands::threshold_arg::parse_tolerance)]
1006 tol_abs: f64,
1007 /// Min cosine similarity floor (default 0.9999)
1008 #[arg(long, value_name = "F", default_value_t = 0.9999,
1009 value_parser = commands::threshold_arg::parse_cosine)]
1010 tol_cos: f64,
1011 },
1012 /// Lint an externally captured attention dump (CRUX-F-17 — no apr producer yet)
1013 AttnVizLint {
1014 /// Path to attention dump in JSON form (4-D [layers][heads][rows][cols] floats)
1015 #[arg(long, value_name = "FILE")]
1016 attn_file: Option<PathBuf>,
1017 /// Path to HTML heatmap output
1018 #[arg(long, value_name = "FILE")]
1019 html_file: Option<PathBuf>,
1020 /// Minimum <svg|<canvas open-tag count expected in HTML (|layers|*|heads|)
1021 #[arg(long, value_name = "N", default_value_t = 1)]
1022 expected_heatmaps: usize,
1023 /// Row-softmax normalization tolerance (default 1e-5)
1024 #[arg(long, value_name = "F64", default_value_t = 1e-5,
1025 value_parser = commands::threshold_arg::parse_tolerance)]
1026 tolerance: f64,
1027 /// Causal-mask zero epsilon (default 1e-9)
1028 #[arg(long, value_name = "F64", default_value_t = 1e-9,
1029 value_parser = commands::threshold_arg::parse_tolerance)]
1030 epsilon: f64,
1031 },
1032 /// Lint an externally captured check-finite error and/or coverage JSON (CRUX-F-11 — no apr producer yet)
1033 CheckFiniteLint {
1034 /// Externally captured check-finite stderr JSON from a poisoned model
1035 #[arg(long, value_name = "FILE")]
1036 error_file: Option<PathBuf>,
1037 /// Externally captured check-finite layer-coverage JSON
1038 #[arg(long, value_name = "FILE")]
1039 list_file: Option<PathBuf>,
1040 /// Minimum layer-coverage count when `--list-file` is supplied (default 100)
1041 #[arg(long, value_name = "N", default_value_t = 100)]
1042 min_layers: usize,
1043 },
1044 /// Lint an embedding-projection CSV, e.g. from
1045 /// `apr debug embed-viz --model model.apr --seed 42 -o emb.csv` (CRUX-F-18)
1046 EmbedVizLint {
1047 /// Path to the `token_id,token_str,x,y` CSV written by `apr debug embed-viz`
1048 #[arg(long, value_name = "FILE")]
1049 csv_file: PathBuf,
1050 /// Expected row count == vocab_size (optional)
1051 #[arg(long, value_name = "N")]
1052 expected_vocab_size: Option<usize>,
1053 /// Second CSV from a rerun at the same --seed, for the determinism gate (optional)
1054 #[arg(long, value_name = "FILE")]
1055 csv_file_b: Option<PathBuf>,
1056 },
1057 /// Lint an externally captured token-selection JSONL trace (CRUX-F-19 — no apr producer yet)
1058 ExplainTokenLint {
1059 /// Path to captured JSONL body (one sampled-token record per line)
1060 #[arg(long, value_name = "FILE")]
1061 jsonl_file: PathBuf,
1062 /// Tolerance for `Σ post_prob ≈ 1.0` (default 1e-5)
1063 #[arg(long, value_name = "F64", default_value_t = 1e-5,
1064 value_parser = commands::threshold_arg::parse_tolerance)]
1065 tolerance: f64,
1066 /// Assert greedy decoding: sampled_id must equal argmax(pre_prob)
1067 #[arg(long)]
1068 require_greedy: bool,
1069 },
1070 /// Lint a captured GPU memory Chrome Trace Event Format JSON (CRUX-F-07)
1071 GpuMemtraceLint {
1072 /// Path to an externally captured GPU-memory Chrome Trace JSON (no apr producer yet)
1073 #[arg(long, value_name = "FILE")]
1074 trace_file: PathBuf,
1075 },
1076 /// Lint a captured KV-cache utilization timeline (CRUX-F-06)
1077 KvTimelineLint {
1078 /// Path to an externally captured KV-cache timeline JSON body (no apr producer yet)
1079 #[arg(long, value_name = "FILE")]
1080 timeline_file: PathBuf,
1081 /// Preemption threshold (default 0.95, vLLM canonical)
1082 #[arg(long, value_name = "FRACTION", default_value_t = 0.95,
1083 value_parser = commands::threshold_arg::parse_fraction)]
1084 preempt_threshold: f64,
1085 },
1086 /// Lint a captured OTLP/JSON ExportTraceServiceRequest body (CRUX-K-08).
1087 ///
1088 /// At least one gate flag is required: every check is opt-in, so a bare
1089 /// invocation would check nothing and exit 0 for any parseable JSON.
1090 OtlpLint {
1091 /// Path to captured OTLP/JSON export body
1092 #[arg(long, value_name = "FILE")]
1093 otlp_file: PathBuf,
1094 /// Require at least one `apr.inference` span to be present
1095 #[arg(long)]
1096 require_apr_span: bool,
1097 /// Require gen_ai.* and apr.tokens.* attribute keys on some span
1098 #[arg(long)]
1099 require_genai_attrs: bool,
1100 /// Verify W3C trace-context propagation: expect this 32-hex traceId
1101 #[arg(long, value_name = "HEX32")]
1102 expect_trace_id: Option<String>,
1103 },
1104 /// Lint a captured Prometheus /metrics response (CRUX-K-07)
1105 PrometheusLint {
1106 /// Path to captured /metrics response body (text/plain; version=0.0.4)
1107 #[arg(long, value_name = "FILE")]
1108 metrics_file: PathBuf,
1109 /// Optional captured Content-Type header to verify against version=0.0.4
1110 #[arg(long, value_name = "HEADER")]
1111 content_type: Option<String>,
1112 /// Require the K-07 metric set (apr_num_requests_running, ...) to be present
1113 #[arg(long)]
1114 require_k07_metrics: bool,
1115 },
1116 /// Lint a captured OpenAI tool-use response (CRUX-C-11)
1117 ToolUseLint {
1118 /// Path to captured OpenAI tool-use response JSON
1119 #[arg(long, value_name = "FILE")]
1120 observation_file: PathBuf,
1121 },
1122 /// Lint a GBNF grammar-constrained observation (CRUX-C-10)
1123 GbnfLint {
1124 /// Path to captured GBNF observation JSON
1125 #[arg(long, value_name = "FILE")]
1126 observation_file: PathBuf,
1127 },
1128 /// Lint a typical-p sampling observation (CRUX-C-22)
1129 TypicalPLint {
1130 /// Path to captured typical-p observation JSON, with any of the
1131 /// sections range/identity/mass/sort/renorm
1132 #[arg(long, value_name = "FILE")]
1133 observation_file: PathBuf,
1134 },
1135 /// Gradient-norm telemetry analysis (CRUX-F-09)
1136 GradNorm {
1137 /// Path to JSON file of per-step grad-norm records
1138 #[arg(long, value_name = "FILE")]
1139 history_file: PathBuf,
1140 /// Maximum allowed clipped grad-norm (for cap-violation check)
1141 #[arg(long, value_name = "M",
1142 value_parser = commands::threshold_arg::parse_tolerance)]
1143 max_grad_norm: Option<f64>,
1144 /// Rolling-median window size for spike detection (in steps)
1145 #[arg(long, default_value = "16")]
1146 spike_window: usize,
1147 /// Multiplier threshold for spike detection
1148 #[arg(long, default_value = "10.0",
1149 value_parser = commands::threshold_arg::parse_tolerance)]
1150 spike_multiplier: f64,
1151 },
1152 /// Lint a captured registry byte-quota observation (CRUX-A-22)
1153 RegistryQuotaLint {
1154 /// Path to captured quota/atomic/ceiling observation JSON
1155 #[arg(long, value_name = "FILE")]
1156 observation_file: PathBuf,
1157 },
1158 /// Lint a captured imatrix calibration observation (CRUX-B-07)
1159 ImatrixLint {
1160 /// Path to captured imatrix observation JSON
1161 #[arg(long, value_name = "FILE")]
1162 observation_file: PathBuf,
1163 },
1164 /// Lint a captured /v1/embeddings observation (CRUX-C-13)
1165 EmbeddingsLint {
1166 /// Path to captured /v1/embeddings observation JSON, with any of the
1167 /// sections shape/determinism/usage/flag
1168 #[arg(long, value_name = "FILE")]
1169 observation_file: PathBuf,
1170 },
1171 /// Lint a captured Hub+local unified-search merge observation (CRUX-A-23)
1172 UnifiedSearchLint {
1173 /// Path to captured unified-search observation JSON
1174 #[arg(long, value_name = "FILE")]
1175 observation_file: PathBuf,
1176 },
1177 /// Lint a captured `apr rm` / externally captured gc blob-GC observation (CRUX-A-25)
1178 RmGcLint {
1179 /// Path to captured rm/gc observation JSON
1180 #[arg(long, value_name = "FILE")]
1181 observation_file: PathBuf,
1182 },
1183 /// Lint a captured APR_MODELS shared-cache observation (CRUX-A-21)
1184 SharedCacheLint {
1185 /// Path to captured dedup/permission observation JSON
1186 #[arg(long, value_name = "FILE")]
1187 observation_file: PathBuf,
1188 },
1189 /// Perplexity classifier (CRUX-E-02)
1190 Ppl {
1191 /// JSON file containing an array of per-token natural-log
1192 /// probabilities (e.g. `[-1.2, -0.5, -2.1, ...]`). Required.
1193 #[arg(long, value_name = "FILE")]
1194 log_probs_file: PathBuf,
1195 },
1196 /// Validate dequant→requant metadata preservation (CRUX-B-19)
1197 QuantPreservationLint {
1198 /// Reference GGUF (pre-roundtrip)
1199 #[arg(long, value_name = "REF.gguf")]
1200 reference: PathBuf,
1201 /// Requantized GGUF (post-roundtrip)
1202 #[arg(long, value_name = "REQ.gguf")]
1203 requant: PathBuf,
1204 },
1205 /// Split a safetensors file into shards + weight-map index (CRUX-B-05)
1206 Shard {
1207 /// Single-file safetensors model to split
1208 #[arg(value_name = "FILE")]
1209 file: PathBuf,
1210 /// Maximum size of each shard (e.g. 5GB, 500MB, 1.5GiB)
1211 #[arg(long, value_name = "SIZE", default_value = "5GB")]
1212 max_shard_size: String,
1213 /// Output directory for shards + model.safetensors.index.json
1214 #[arg(short, long, value_name = "DIR")]
1215 output: PathBuf,
1216 /// #2392: Overwrite an existing shard set in the output directory
1217 #[arg(short, long)]
1218 force: bool,
1219 },
1220 /// Reconstruct a single safetensors file from a sharded directory (CRUX-B-05)
1221 Unshard {
1222 /// Sharded directory containing model.safetensors.index.json
1223 #[arg(value_name = "DIR")]
1224 input: PathBuf,
1225 /// Output single-file safetensors path
1226 #[arg(short, long, value_name = "FILE")]
1227 output: PathBuf,
1228 /// #2392: Overwrite an existing output file (refused without it)
1229 #[arg(short, long)]
1230 force: bool,
1231 },
1232 /// Publishing, conversion, and analysis tools
1233 #[command(flatten)]
1234 Tools(ToolCommands),
1235 /// Score a query/passage pair (or rank multiple passages) with a BERT
1236 /// cross-encoder loaded from an APR v2 file (GH-326 Phase 3).
1237 ///
1238 /// Wraps `aprender_core::models::bert::CrossEncoder::load_from_reader`
1239 /// + `score()`. The APR must contain the canonical HF BERT tensor
1240 /// names (see `models::bert::expected_bert_tensor_names`).
1241 ///
1242 /// Tokenisation is NOT applied here — caller passes pre-tokenised
1243 /// `input_ids` + `token_type_ids` as comma-delimited u32 lists. A
1244 /// dedicated tokeniser-aware mode is Phase 3b follow-up scope.
1245 Rerank {
1246 /// Path to the APR file containing the cross-encoder weights.
1247 #[arg(value_name = "MODEL")]
1248 model: PathBuf,
1249 /// Pre-tokenised input ids (comma-separated `u32`s). Mutually
1250 /// exclusive with `--query`+`--passage`+`--vocab` (Phase 3b).
1251 /// Example: `--input-ids 101,2024,102,3456,102` for `[CLS] q [SEP] p [SEP]`.
1252 #[arg(long, value_name = "IDS")]
1253 input_ids: Option<String>,
1254 /// Pre-tokenised token-type ids (comma-separated `u32`s).
1255 /// Same length as `--input-ids`. 0 for query side, 1 for passage.
1256 #[arg(long, value_name = "IDS")]
1257 token_type_ids: Option<String>,
1258 /// Phase 3b — query text. Pair with `--passage` + `--vocab` to enable
1259 /// in-process WordPiece tokenisation. The tokeniser builds
1260 /// `[CLS] query [SEP] passage [SEP]` with `token_type_ids = 0` for
1261 /// the query side and `1` for the passage side.
1262 #[arg(long, value_name = "TEXT")]
1263 query: Option<String>,
1264 /// Phase 3b — passage text. Required when `--query` is supplied
1265 /// in single-pair mode (use `--passages` for batch ranking).
1266 #[arg(long, value_name = "TEXT")]
1267 passage: Option<String>,
1268 /// Phase 5 — batch ranking mode (#326). Passage candidates to
1269 /// score against `--query`. May be supplied multiple times:
1270 /// `apr rerank model.apr --query "..." --passages "p1" --passages "p2"`.
1271 /// Mutually exclusive with `--passage`. Output is one
1272 /// `score[i]` line per passage in input order, OR a JSON array
1273 /// of `{passage, logit, score}` objects sorted by descending
1274 /// score when `--sort` is set.
1275 #[arg(long, value_name = "TEXT")]
1276 passages: Vec<String>,
1277 /// Phase 5 — sort batch output by descending score (highest
1278 /// relevance first). Only meaningful with `--passages` and
1279 /// `--json`. Default: preserve input order.
1280 #[arg(long)]
1281 sort: bool,
1282 /// Phase 5 — limit to top-K passages after sorting. Implies
1283 /// `--sort`. Default 0 (no limit).
1284 #[arg(long, default_value_t = 0)]
1285 top_k: usize,
1286 /// Phase 3b — path to a WordPiece `vocab.txt` (one token per line,
1287 /// line index = token id). Required when `--query` is supplied.
1288 /// Must contain entries for `[CLS]`, `[SEP]`, and `[UNK]`.
1289 /// Phase 4 accepts HuggingFace `tokenizer.json` (extension-detected).
1290 #[arg(long, value_name = "FILE")]
1291 vocab: Option<PathBuf>,
1292 /// Override hidden_dim (default: 384 / MiniLM-L-6).
1293 #[arg(long, default_value_t = 384)]
1294 hidden_dim: usize,
1295 /// Override num_layers (default: 6 / MiniLM-L-6).
1296 #[arg(long, default_value_t = 6)]
1297 num_layers: usize,
1298 /// Override num_heads (default: 12 / MiniLM-L-6).
1299 #[arg(long, default_value_t = 12)]
1300 num_heads: usize,
1301 /// Override intermediate_dim (default: 1536 / MiniLM-L-6).
1302 #[arg(long, default_value_t = 1536)]
1303 intermediate_dim: usize,
1304 /// Override vocab_size (default: 30522 / bert-base-uncased).
1305 #[arg(long, default_value_t = 30522)]
1306 vocab_size: usize,
1307 /// Override max_position_embeddings (default: 512).
1308 #[arg(long, default_value_t = 512)]
1309 max_position_embeddings: usize,
1310 /// Override type_vocab_size (default: 2).
1311 #[arg(long, default_value_t = 2)]
1312 type_vocab_size: usize,
1313 /// Number of labels in the classifier head (default: 1 for
1314 /// regression-style relevance scoring).
1315 #[arg(long, default_value_t = 1)]
1316 num_labels: usize,
1317 /// Load the optional BERT pooler dense layer (default: true).
1318 /// Cross-encoders that skip the pooler should pass `--with-pooler false`.
1319 ///
1320 /// Takes an optional value: `--with-pooler` (bare) and an omitted flag
1321 /// both mean true; `--with-pooler false` / `--with-pooler=false` turn
1322 /// the pooler off. A bare `bool` here would compile to a SetTrue switch
1323 /// and make the documented `false` unreachable.
1324 #[arg(
1325 long,
1326 num_args = 0..=1,
1327 default_value_t = true,
1328 default_missing_value = "true",
1329 action = clap::ArgAction::Set,
1330 )]
1331 with_pooler: bool,
1332 /// Emit the raw logit instead of the sigmoid-mapped relevance score.
1333 #[arg(long)]
1334 raw_logit: bool,
1335 /// Output as JSON.
1336 #[arg(long)]
1337 json: bool,
1338 },
1339 /// Produce sentence embeddings from a BERT bi-encoder (GH-326 Phase 6).
1340 ///
1341 /// First-stage dense retrieval companion to `apr rerank`. Loads an
1342 /// encoder-only BertModel (e.g. `sentence-transformers/all-MiniLM-L6-v2`),
1343 /// tokenises the input text with WordPiece, runs the full encoder
1344 /// forward, then pools the hidden states with one of:
1345 /// `--pool cls` — take the [CLS] hidden state
1346 /// `--pool mean` — mean over non-padding token positions (default;
1347 /// sentence-transformers convention)
1348 /// Optionally L2-normalises the result (`--normalize`, default true,
1349 /// matches sentence-transformers).
1350 Embed {
1351 /// Path to the APR file containing the encoder weights (BertModel).
1352 #[arg(value_name = "MODEL")]
1353 model: PathBuf,
1354 /// Text to encode. Repeatable: `apr embed model.apr --text "a" --text "b" --vocab tok.json`.
1355 #[arg(long, value_name = "TEXT")]
1356 text: Vec<String>,
1357 /// Phase 7 (GH-326) — read texts from a file, one per line.
1358 /// Concatenated with `--text` inputs in order: `--text` first,
1359 /// then `--text-file` rows. Blank lines and lines starting
1360 /// with `#` are skipped. Useful for RAG-style first-stage
1361 /// retrieval where the second-stage rerank candidate set
1362 /// (50-100 documents) is the embed input.
1363 #[arg(long, value_name = "FILE")]
1364 text_file: Option<PathBuf>,
1365 /// Path to a WordPiece `vocab.txt` or HF `tokenizer.json`.
1366 #[arg(long, value_name = "FILE")]
1367 vocab: PathBuf,
1368 /// Pooling strategy (`cls` or `mean`). Default: `mean`
1369 /// (matches sentence-transformers convention).
1370 #[arg(long, default_value = "mean")]
1371 pool: String,
1372 /// L2-normalise the output embedding. Default: true (matches
1373 /// sentence-transformers convention). Pass `--normalize false`
1374 /// to keep raw magnitudes.
1375 ///
1376 /// Takes an optional value: `--normalize` (bare) and an omitted flag
1377 /// both mean true; `--normalize false` / `--normalize=false` keep the
1378 /// raw magnitudes. A bare `bool` here would compile to a SetTrue switch
1379 /// and make the documented `false` unreachable.
1380 ///
1381 /// Because the value is optional, do not place a bare `--normalize`
1382 /// immediately before the MODEL positional — write
1383 /// `apr embed MODEL --normalize` or `--normalize=true MODEL`.
1384 #[arg(
1385 long,
1386 num_args = 0..=1,
1387 default_value_t = true,
1388 default_missing_value = "true",
1389 action = clap::ArgAction::Set,
1390 )]
1391 normalize: bool,
1392 /// Override hidden_dim (default: 384 / MiniLM).
1393 #[arg(long, default_value_t = 384)]
1394 hidden_dim: usize,
1395 /// Override num_layers (default: 6 / MiniLM-L-6).
1396 #[arg(long, default_value_t = 6)]
1397 num_layers: usize,
1398 /// Override num_heads.
1399 #[arg(long, default_value_t = 12)]
1400 num_heads: usize,
1401 /// Override intermediate_dim.
1402 #[arg(long, default_value_t = 1536)]
1403 intermediate_dim: usize,
1404 /// Override vocab_size.
1405 #[arg(long, default_value_t = 30522)]
1406 vocab_size: usize,
1407 /// Override max_position_embeddings.
1408 #[arg(long, default_value_t = 512)]
1409 max_position_embeddings: usize,
1410 /// Override type_vocab_size.
1411 #[arg(long, default_value_t = 2)]
1412 type_vocab_size: usize,
1413 /// Output as JSON.
1414 #[arg(long)]
1415 json: bool,
1416 },
1417}
1418
1419/// Subcommands for `apr dataset` — dataset inspection (aprender#2377 finding 3).
1420///
1421/// `audio-inspect` is the PRODUCER for `apr audio-inspect-lint`: CRUX-H-13
1422/// shipped the lint with help pointing at a command the binary did not have,
1423/// so its gates had never run on real data.
1424#[derive(Subcommand, Debug)]
1425pub enum DatasetCommands {
1426 /// Decode an uncompressed RIFF/WAVE file and report its measured shape and
1427 /// amplitude extrema — the observation `apr audio-inspect-lint` reads.
1428 ///
1429 /// Supports PCM u8/i16/i24/i32 and IEEE float32. Compressed containers
1430 /// (FLAC, MP3, Ogg) and codecs it cannot decode are REFUSED with a non-zero
1431 /// exit; no resampling and no channel mixdown are performed, so the reported
1432 /// `sample_rate` and `channels` are always the file's own.
1433 AudioInspect {
1434 /// Path to the .wav file to decode
1435 #[arg(value_name = "FILE")]
1436 file: PathBuf,
1437 /// Output format: `json` for the lint-readable body, `text` for humans
1438 #[arg(long, value_name = "FORMAT", default_value = "text",
1439 value_parser = ["json", "text"])]
1440 format: String,
1441 /// Write the observation here instead of stdout
1442 #[arg(short, long, value_name = "FILE")]
1443 output: Option<PathBuf>,
1444 /// Overwrite an existing --output file (refused without it)
1445 #[arg(short, long)]
1446 force: bool,
1447 },
1448}
1449
1450/// Subcommands for `apr kernel` — kernel-level measurements (aprender#2377 finding 3).
1451///
1452/// `parity` is the PRODUCER for `apr attn-parity-lint`: CRUX-L-02 shipped the
1453/// lint with help pointing at `apr kernel parity`, which did not exist.
1454#[derive(Subcommand, Debug)]
1455pub enum KernelCommands {
1456 /// Measure a tiled attention kernel against a naive reference on seeded
1457 /// Q/K/V, emitting the parity + provenance body `apr attn-parity-lint` reads.
1458 ///
1459 /// `--impl tiled` runs the in-tree `realizar::brick::FlashAttentionBrick`
1460 /// online-softmax kernel. `--impl flash2` means the pinned
1461 /// `hf-kernels-community:flash-attn2@<sha>` CUDA kernel, which this binary
1462 /// does not embed: asking for it is REFUSED with a non-zero exit rather
1463 /// than answered by a different kernel under a borrowed name.
1464 Parity {
1465 /// Attention implementation under test
1466 #[arg(long = "impl", value_name = "IMPL", value_enum, default_value_t = KernelImpl::Tiled)]
1467 kernel: KernelImpl,
1468 /// Reference implementation to compare against
1469 #[arg(long = "ref", value_name = "REF", value_enum, default_value_t = KernelRef::Naive)]
1470 reference: KernelRef,
1471 /// KV cache length to attend over
1472 #[arg(long, value_name = "N", default_value_t = 128)]
1473 seq_len: usize,
1474 /// Number of query heads
1475 #[arg(long, value_name = "N", default_value_t = 8)]
1476 num_heads: usize,
1477 /// Number of key/value heads (GQA groups when smaller than --num-heads)
1478 #[arg(long, value_name = "N", default_value_t = 8)]
1479 num_kv_heads: usize,
1480 /// Per-head dimension. flash2 dispatches only 64 or 128
1481 #[arg(long, value_name = "N", default_value_t = 64)]
1482 head_dim: usize,
1483 /// Seed pinning the Q/K/V draw, so a run is reproducible
1484 #[arg(long, value_name = "N", default_value_t = 0)]
1485 seed: u64,
1486 /// Emit the observation as JSON (required to capture it for the lint)
1487 #[arg(long)]
1488 json: bool,
1489 /// Write the observation here instead of stdout
1490 #[arg(short, long, value_name = "FILE")]
1491 output: Option<PathBuf>,
1492 /// Overwrite an existing --output file (refused without it)
1493 #[arg(short, long)]
1494 force: bool,
1495 },
1496}
1497
1498#[cfg(feature = "training")]
1499/// Subcommands for `apr runs` — experiment run management (ALB-050/051)
1500#[derive(Subcommand, Debug)]
1501pub enum RunsCommands {
1502 /// List all training experiment runs (with inline loss sparklines)
1503 Ls {
1504 /// Directory to scan for experiments (default: current dir)
1505 #[arg(long, value_name = "DIR")]
1506 dir: Option<PathBuf>,
1507 /// Read from global experiment registry (~/.entrenar/experiments.db)
1508 #[arg(long)]
1509 global: bool,
1510 /// Filter by status: all, pending, running, completed, failed, cancelled
1511 #[arg(long, default_value = "all")]
1512 status: String,
1513 /// Output as JSON
1514 #[arg(long)]
1515 json: bool,
1516 /// Maximum number of runs to show
1517 #[arg(long, default_value = "50")]
1518 limit: usize,
1519 },
1520 /// Show detailed metrics for a specific run (with braille loss curve)
1521 Show {
1522 /// Run ID
1523 #[arg(value_name = "RUN_ID")]
1524 run_id: String,
1525 /// Directory containing experiment DB
1526 #[arg(long, value_name = "DIR")]
1527 dir: Option<PathBuf>,
1528 /// Read from global registry
1529 #[arg(long)]
1530 global: bool,
1531 /// Output as JSON
1532 #[arg(long)]
1533 json: bool,
1534 },
1535 /// Compare two runs side-by-side (loss curves, config diff, metrics)
1536 Diff {
1537 /// First run ID
1538 #[arg(value_name = "RUN_A")]
1539 run_a: String,
1540 /// Second run ID
1541 #[arg(value_name = "RUN_B")]
1542 run_b: String,
1543 /// Directory containing experiment DB
1544 #[arg(long, value_name = "DIR")]
1545 dir: Option<PathBuf>,
1546 /// Read from global registry
1547 #[arg(long)]
1548 global: bool,
1549 /// Output as JSON
1550 #[arg(long)]
1551 json: bool,
1552 },
1553}
1554
1555#[cfg(feature = "training")]
1556/// Subcommands for `apr experiment` — interactive experiment browser (ALB-024)
1557#[derive(Subcommand, Debug)]
1558pub enum ExperimentCommands {
1559 /// Browse experiment history with interactive TUI (loss curves, params)
1560 View {
1561 /// Path to experiment database file
1562 #[arg(long, value_name = "FILE")]
1563 db: Option<PathBuf>,
1564 /// Read from global experiment registry (~/.entrenar/experiments.db)
1565 #[arg(long)]
1566 global: bool,
1567 /// Output as JSON (non-interactive)
1568 #[arg(long)]
1569 json: bool,
1570 },
1571}
1572
1573/// CRUX-K-11: Subcommands for `apr modelfile`.
1574#[derive(Subcommand, Debug)]
1575pub enum ModelfileSubcommand {
1576 /// Parse an Ollama-style Modelfile and emit the parsed config.
1577 ///
1578 /// Grammar: `FROM`, `PARAMETER`, `TEMPLATE`, `SYSTEM`, `LICENSE`,
1579 /// `MESSAGE`, `ADAPTER` directives. Triple-quoted blocks supported.
1580 /// Directive names are case-insensitive. Unknown directives raise
1581 /// `file:line:col` errors.
1582 Parse {
1583 /// Path to the Modelfile
1584 #[arg(value_name = "FILE")]
1585 file: PathBuf,
1586 /// Output format: `json` or `human`
1587 #[arg(long, default_value = "json")]
1588 format: String,
1589 },
1590}
1591
1592/// GH-876: Subcommands for `apr probar` — consolidates the probador testing
1593/// framework under `apr`. Milestone 1 ships only `tensor` (the migrated
1594/// existing behavior). Subsequent milestones add the remaining 14 probador
1595/// subcommands as separate PRs that delegate to the probador library.
1596#[derive(Subcommand, Debug)]
1597pub enum TestSubcommand {
1598 /// Export tensor activations for visual regression testing (PMAT-481).
1599 ///
1600 /// Generates JSON/PNG per-layer test artifacts that can be compared
1601 /// against a golden reference directory to detect regressions in
1602 /// model behavior after weight updates, quantization, or refactors.
1603 Tensor {
1604 /// Path to .apr model file
1605 #[arg(value_name = "FILE")]
1606 file: PathBuf,
1607 /// Output directory for test artifacts
1608 #[arg(short, long, default_value = "./probar-export")]
1609 output: PathBuf,
1610 /// Export format: json, png, or both
1611 #[arg(long, default_value = "both")]
1612 format: String,
1613 /// Golden reference directory for comparison
1614 #[arg(long)]
1615 golden: Option<PathBuf>,
1616 /// Filter layers by name pattern
1617 #[arg(long)]
1618 layer: Option<String>,
1619 /// Exit non-zero on golden divergence (CI mode, PMAT-481)
1620 #[arg(long)]
1621 assert: bool,
1622 /// Cosine similarity threshold for golden comparison (default: 0.98)
1623 #[arg(long, default_value = "0.98",
1624 value_parser = commands::threshold_arg::parse_cosine_f32)]
1625 tolerance: f32,
1626 },
1627}
1628
1629/// Parse `apr cbtop --iterations`, rejecting 0.
1630///
1631/// With zero measurement iterations every brick keeps zero samples, so its
1632/// measured time is 0.0µs, its gap factor is 0.00x and it scores a perfect
1633/// 100/A — a green report attesting to measurements that never ran. Reject the
1634/// value where the user typed it rather than emitting the fabricated report.
1635fn parse_cbtop_iterations(s: &str) -> std::result::Result<usize, String> {
1636 let n: usize = s
1637 .parse()
1638 .map_err(|_| format!("`{s}` is not a valid iteration count"))?;
1639 if n == 0 {
1640 return Err(
1641 "must be at least 1 — a zero-iteration run measures nothing and would report every \
1642 brick as a perfect 100/A from zero samples"
1643 .to_string(),
1644 );
1645 }
1646 Ok(n)
1647}