1use std::collections::BTreeSet;
4use std::fs;
5use std::path::{Component, Path, PathBuf};
6
7use anyhow::{bail, ensure, Context, Result};
8use serde::Serialize;
9
10use crate::artifact::{
11 publish_bundle, verify_bundle, verify_consumed_bundle_files, BundleVerificationReceipt,
12};
13use crate::comparison::{compare_unverified_traces, compare_verified_bundles};
14use crate::evidence::{build_evidence, EvidencePacket};
15use crate::graph::{ExecutionGraph, GraphNode, GraphNodeKind};
16use crate::nsight::{GpuEvidenceStatus, ProvenanceBindingState};
17use crate::trace::parse_trace;
18
19const QUERY_ROW_LIMIT: usize = 50;
20const QUERY_LABEL_LIMIT: usize = 100;
21const QUERY_DIAGNOSTIC_LIMIT: usize = 50;
22const SUMMARY_SCHEMA: &str = "candle-graph/summary/4";
23const QUERY_SCHEMA: &str = "candle-graph/trace-query/4";
24
25#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
26struct QueryAvailability {
27 status: GpuEvidenceStatus,
28 #[serde(skip_serializing_if = "Option::is_none")]
29 reason: Option<String>,
30}
31
32impl QueryAvailability {
33 fn is_available(&self) -> bool {
34 self.status == GpuEvidenceStatus::Available
35 }
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum TraceQueryKind {
40 SlowestHost,
41 SlowestDevice,
42 Heaviest,
43 Memory,
44 Spans,
45 Tensors,
46 TensorStats,
47 Gradients,
48 Capabilities,
49 GpuStatus,
50 GpuCorrelation,
51 GpuPhases,
52 GpuKernels,
53 GpuAttributionGaps,
54}
55
56impl TraceQueryKind {
57 pub fn as_str(self) -> &'static str {
58 match self {
59 Self::SlowestHost => "slowest-host",
60 Self::SlowestDevice => "slowest-device",
61 Self::Heaviest => "heaviest",
62 Self::Memory => "memory",
63 Self::Spans => "spans",
64 Self::Tensors => "tensors",
65 Self::TensorStats => "tensor-stats",
66 Self::Gradients => "gradients",
67 Self::Capabilities => "capabilities",
68 Self::GpuStatus => "gpu-status",
69 Self::GpuCorrelation => "gpu-correlation",
70 Self::GpuPhases => "gpu-phases",
71 Self::GpuKernels => "gpu-kernels",
72 Self::GpuAttributionGaps => "gpu-attribution-gaps",
73 }
74 }
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
78#[serde(rename_all = "snake_case")]
79enum EvidenceInputKind {
80 RawTrace,
81 VerifiedBundle,
82}
83
84#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
85struct EvidenceInput {
86 kind: EvidenceInputKind,
87 requested_path: PathBuf,
88 trace_path: PathBuf,
89 #[serde(skip_serializing_if = "Option::is_none")]
90 bundle_root: Option<PathBuf>,
91 evidence_source: &'static str,
92 gpu_identity_bound: bool,
93 #[serde(skip_serializing_if = "Option::is_none")]
94 verification: Option<BundleVerificationReceipt>,
95}
96
97struct LoadedEvidence {
98 packet: EvidencePacket,
99 input: EvidenceInput,
100}
101
102pub fn load_evidence(input: &Path) -> Result<EvidencePacket> {
105 Ok(load_evidence_input(input)?.packet)
106}
107
108fn load_evidence_input(input: &Path) -> Result<LoadedEvidence> {
109 if !input.exists() {
110 bail!("evidence input does not exist: {}", input.display());
111 }
112
113 if input.is_dir() {
114 if input.join("bundle.json").exists() {
115 return load_verified_bundle(input, input);
116 }
117 let trace = input.join("trace.jsonl");
118 ensure!(
119 trace.is_file(),
120 "input directory is neither a finalized bundle nor a raw-trace directory: {}",
121 input.display()
122 );
123 reject_unverified_augmented_parent(input)?;
124 return load_raw_trace(input, &trace);
125 }
126
127 ensure!(
128 input.is_file(),
129 "evidence input is not a regular file: {}",
130 input.display()
131 );
132 let parent = input.parent().unwrap_or_else(|| Path::new("."));
133 if parent.join("bundle.json").exists() {
134 return load_verified_bundle(input, parent);
135 }
136 reject_unverified_augmented_parent(parent)?;
137 load_raw_trace(input, input)
138}
139
140fn load_raw_trace(requested_path: &Path, trace_path: &Path) -> Result<LoadedEvidence> {
141 let packet = build_evidence(trace_path, None)?;
142 Ok(LoadedEvidence {
143 input: EvidenceInput {
144 kind: EvidenceInputKind::RawTrace,
145 requested_path: requested_path.to_path_buf(),
146 trace_path: trace_path.to_path_buf(),
147 bundle_root: None,
148 evidence_source: "trace_reconstruction",
149 gpu_identity_bound: false,
150 verification: None,
151 },
152 packet,
153 })
154}
155
156fn load_verified_bundle(requested_path: &Path, root: &Path) -> Result<LoadedEvidence> {
157 let verification = verify_bundle(root)
158 .with_context(|| format!("verify evidence bundle {}", root.display()))?;
159 let trace_path = root.join("trace.jsonl");
160 let document = parse_trace(&trace_path)
161 .with_context(|| format!("parse verified bundle trace {}", trace_path.display()))?;
162 ensure!(
163 verification.run_id == document.run.run_id,
164 "verified bundle manifest run ID {:?} does not match trace run ID {:?}",
165 verification.run_id,
166 document.run.run_id
167 );
168
169 let evidence_path = root.join("evidence.json");
170 let packet: EvidencePacket =
171 serde_json::from_slice(&fs::read(&evidence_path).with_context(|| {
172 format!("read verified evidence packet {}", evidence_path.display())
173 })?)
174 .with_context(|| format!("parse verified evidence packet {}", evidence_path.display()))?;
175 packet.validate_schema()?;
176 ensure!(
177 packet.provenance == document.run,
178 "verified evidence packet provenance does not match its trace metadata"
179 );
180 verify_consumed_bundle_files(root, &verification, &["trace.jsonl", "evidence.json"])
181 .with_context(|| {
182 format!(
183 "post-read verify consumed files in evidence bundle {}",
184 root.display()
185 )
186 })?;
187
188 let gpu_identity_bound = packet.gpu.provenance.binding == ProvenanceBindingState::Bound;
189 Ok(LoadedEvidence {
190 packet,
191 input: EvidenceInput {
192 kind: EvidenceInputKind::VerifiedBundle,
193 requested_path: requested_path.to_path_buf(),
194 trace_path,
195 bundle_root: Some(root.to_path_buf()),
196 evidence_source: "verified_evidence_json",
197 gpu_identity_bound,
198 verification: Some(verification),
199 },
200 })
201}
202
203fn reject_unverified_augmented_parent(root: &Path) -> Result<()> {
204 let evidence = root.join("evidence.json");
205 let nsight = root.join("nsight");
206 if evidence.exists() || nsight.exists() {
207 bail!(
208 "input parent {} contains augmented evidence but no bundle.json; refusing to discard unverified GPU evidence and rebuild as trace-only",
209 root.display()
210 );
211 }
212 Ok(())
213}
214
215fn reject_output_inside_verified_bundle(
216 input: &EvidenceInput,
217 output: Option<&Path>,
218) -> Result<()> {
219 let (Some(root), Some(output)) = (input.bundle_root.as_deref(), output) else {
220 return Ok(());
221 };
222 reject_output_inside_bundle_root(root, output)
223}
224
225fn reject_output_inside_bundle_root(root: &Path, output: &Path) -> Result<()> {
226 let resolved_root = fs::canonicalize(root)
227 .with_context(|| format!("resolve verified bundle root {}", root.display()))?;
228 let (resolved_output, traversed_bundle) = resolve_write_path(output, &resolved_root)?;
229 if traversed_bundle || resolved_output.starts_with(&resolved_root) {
230 bail!(
231 "refusing to write command output {} inside verified bundle {}",
232 output.display(),
233 root.display()
234 );
235 }
236 Ok(())
237}
238
239fn containing_bundle_root(input: &Path) -> Option<&Path> {
242 if input.is_dir() && input.join("bundle.json").is_file() {
243 return Some(input);
244 }
245 input
246 .parent()
247 .filter(|parent| parent.join("bundle.json").is_file())
248}
249
250fn resolve_write_path(path: &Path, forbidden_root: &Path) -> Result<(PathBuf, bool)> {
254 let absolute = if path.is_absolute() {
255 path.to_path_buf()
256 } else {
257 std::env::current_dir()
258 .context("resolve current directory for output path")?
259 .join(path)
260 };
261 let mut resolved = PathBuf::new();
262 let mut traversed_forbidden_root = false;
263 for component in absolute.components() {
264 match component {
265 Component::Prefix(prefix) => resolved.push(prefix.as_os_str()),
266 Component::RootDir => resolved.push(component.as_os_str()),
267 Component::CurDir => {}
268 Component::ParentDir => {
269 resolved.pop();
270 }
271 Component::Normal(name) => {
272 resolved.push(name);
273 match fs::symlink_metadata(&resolved) {
274 Ok(_) => {
275 resolved = fs::canonicalize(&resolved).with_context(|| {
276 format!("resolve output path component {}", resolved.display())
277 })?;
278 }
279 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
280 Err(error) => {
281 return Err(error).with_context(|| {
282 format!("inspect output path component {}", resolved.display())
283 });
284 }
285 }
286 }
287 }
288 traversed_forbidden_root |= resolved.starts_with(forbidden_root);
289 }
290 Ok((resolved, traversed_forbidden_root))
291}
292
293pub fn run_import(trace_path: &Path, output: Option<&Path>) -> Result<()> {
294 let loaded = load_evidence_input(trace_path)?;
295 reject_output_inside_verified_bundle(&loaded.input, output)?;
296 super::write_output(
297 output,
298 (serde_json::to_string_pretty(&loaded.packet)? + "\n").as_bytes(),
299 )
300}
301
302pub fn run_summary(input_path: &Path, output: Option<&Path>) -> Result<()> {
303 let loaded = load_evidence_input(input_path)?;
304 reject_output_inside_verified_bundle(&loaded.input, output)?;
305 let evidence = &loaded.packet;
306 let rendered = serde_json::to_string_pretty(&serde_json::json!({
307 "schema": SUMMARY_SCHEMA,
308 "input": &loaded.input,
309 "provenance": &evidence.provenance,
310 "health": &evidence.health,
311 "capabilities": &evidence.capabilities,
312 "findings": &evidence.findings,
313 "gaps": &evidence.gaps,
314 "summary": evidence.graph.as_ref().map(|graph| &graph.summary),
315 "tensor_stats": {
316 "events": evidence.tensor_stats.len(),
317 "non_finite_events": evidence.tensor_stats.iter().filter(|event| event.non_finite > 0).count(),
318 },
319 "timing": &evidence.timing,
320 "memory": &evidence.memory,
321 "gpu": gpu_summary(evidence),
322 }))? + "\n";
323 super::write_output(output, rendered.as_bytes())
324}
325
326pub fn run_query(input_path: &Path, kind: TraceQueryKind, output: Option<&Path>) -> Result<()> {
327 let loaded = load_evidence_input(input_path)?;
328 reject_output_inside_verified_bundle(&loaded.input, output)?;
329 let evidence = &loaded.packet;
330 let result = match kind {
331 TraceQueryKind::Memory => serde_json::to_value(&evidence.memory)?,
332 TraceQueryKind::TensorStats => serde_json::to_value(&evidence.tensor_stats)?,
333 TraceQueryKind::Capabilities => serde_json::to_value(&evidence.capabilities)?,
334 TraceQueryKind::GpuStatus => query_gpu_status(evidence),
335 TraceQueryKind::GpuCorrelation => query_gpu_correlation(evidence),
336 TraceQueryKind::GpuPhases => query_gpu_phases(evidence),
337 TraceQueryKind::GpuKernels => query_gpu_kernels(evidence),
338 TraceQueryKind::GpuAttributionGaps => query_gpu_attribution_gaps(evidence),
339 other => {
340 let graph = evidence
341 .graph
342 .as_ref()
343 .context("query requires a complete, structurally valid capture")?;
344 query_graph(graph, other)
345 }
346 };
347 let rendered = serde_json::to_string_pretty(&serde_json::json!({
348 "schema": QUERY_SCHEMA,
349 "kind": kind.as_str(),
350 "input": &loaded.input,
351 "capabilities": &evidence.capabilities,
352 "result": result,
353 }))? + "\n";
354 super::write_output(output, rendered.as_bytes())
355}
356
357#[cfg(feature = "visualizer")]
358pub fn run_view(trace_path: &Path, output: &Path, nsight_dir: Option<&Path>) -> Result<()> {
359 if let Some(root) = containing_bundle_root(trace_path) {
360 reject_output_inside_bundle_root(root, output)?;
361 }
362 let evidence = build_evidence(trace_path, nsight_dir)?;
363 super::write_output(
364 Some(output),
365 crate::viewer::render_evidence_html(&evidence).as_bytes(),
366 )
367}
368
369pub fn run_compare(
370 baseline: &[PathBuf],
371 candidate: &[PathBuf],
372 unverified_traces: bool,
373 output: Option<&Path>,
374) -> Result<()> {
375 let comparison = if unverified_traces {
376 let parse_all = |paths: &[PathBuf], cohort: &str| -> Result<Vec<_>> {
377 paths
378 .iter()
379 .map(|path| {
380 parse_trace(path)
381 .with_context(|| format!("parse unverified {cohort} {}", path.display()))
382 })
383 .collect()
384 };
385 compare_unverified_traces(
386 &parse_all(baseline, "baseline")?,
387 &parse_all(candidate, "candidate")?,
388 )
389 } else {
390 if let Some(output) = output {
391 for root in baseline.iter().chain(candidate) {
392 reject_output_inside_bundle_root(root, output)?;
393 }
394 }
395 compare_verified_bundles(baseline, candidate)?
396 };
397 super::write_output(
398 output,
399 (serde_json::to_string_pretty(&comparison)? + "\n").as_bytes(),
400 )
401}
402
403pub fn run_report(trace: &Path, nsight_dir: Option<&Path>, bundle: &Path) -> Result<()> {
404 for ancestor in bundle.ancestors().skip(1) {
405 ensure!(
406 !ancestor.join("bundle.json").is_file(),
407 "refusing to publish bundle {} inside existing bundle {}",
408 bundle.display(),
409 ancestor.display()
410 );
411 }
412 publish_bundle(bundle, trace, nsight_dir)?;
413 Ok(())
414}
415
416pub fn run_verify(bundle: &Path, output: Option<&Path>) -> Result<()> {
417 let receipt = verify_bundle(bundle)?;
418 if let Some(output) = output {
419 reject_output_inside_bundle_root(bundle, output)?;
420 }
421 super::write_output(
422 output,
423 (serde_json::to_string_pretty(&receipt)? + "\n").as_bytes(),
424 )
425}
426
427fn query_graph(graph: &ExecutionGraph, kind: TraceQueryKind) -> serde_json::Value {
428 match kind {
429 TraceQueryKind::SlowestHost => serde_json::json!({
430 "entrypoint": graph.summary.entrypoint,
431 "outer_wall_time_ns": graph.summary.outer_wall_time_ns,
432 "slowest_host_spans": graph.summary.slowest_host_spans,
433 }),
434 TraceQueryKind::SlowestDevice => serde_json::json!({
435 "entrypoint": graph.summary.entrypoint,
436 "slowest_device_spans": graph.summary.slowest_device_spans,
437 }),
438 TraceQueryKind::Heaviest => serde_json::json!({
439 "entrypoint": graph.summary.entrypoint,
440 "heaviest_spans": graph.summary.heaviest_spans,
441 "heaviest_ops": sorted_nodes(graph, |node| matches!(node.kind, GraphNodeKind::Op) && node.allocated_bytes.is_some(), |node| node.allocated_bytes.unwrap_or(0)),
442 }),
443 TraceQueryKind::Spans => serde_json::json!({ "spans": graph.spans, "edges": graph.edges }),
444 TraceQueryKind::Tensors => serde_json::to_value(&graph.tensors).unwrap_or_default(),
445 TraceQueryKind::Gradients => serde_json::to_value(&graph.gradients).unwrap_or_default(),
446 TraceQueryKind::Memory
447 | TraceQueryKind::TensorStats
448 | TraceQueryKind::Capabilities
449 | TraceQueryKind::GpuStatus
450 | TraceQueryKind::GpuCorrelation
451 | TraceQueryKind::GpuPhases
452 | TraceQueryKind::GpuKernels
453 | TraceQueryKind::GpuAttributionGaps => {
454 unreachable!("handled without graph")
455 }
456 }
457}
458
459fn gpu_summary(evidence: &EvidencePacket) -> serde_json::Value {
460 let correlation = report_availability(
461 evidence,
462 "nvtx_gpu_proj_trace",
463 evidence.gpu.coverage.nvtx_projection,
464 );
465 let phase_attribution = combined_report_availability(
466 evidence,
467 &[
468 ("nvtx_gpu_proj_trace", evidence.gpu.coverage.nvtx_projection),
469 ("cuda_gpu_trace", evidence.gpu.coverage.gpu_timeline),
470 ],
471 );
472 serde_json::json!({
473 "status": evidence.gpu.status,
474 "reason": &evidence.gpu.reason,
475 "provenance_binding": evidence.gpu.provenance.binding,
476 "correlation": {
477 "status": correlation.status,
478 "reason": &correlation.reason,
479 "complete": correlation.is_available().then_some(evidence.gpu.correlation.complete),
480 },
481 "coverage": &evidence.gpu.coverage,
482 "normalized_rows": gpu_row_counts(evidence),
483 "attributed_phases": {
484 "status": phase_attribution.status,
485 "reason": &phase_attribution.reason,
486 "total": phase_attribution.is_available().then_some(evidence.gpu.phase_attribution.len()),
487 },
488 "diagnostic_count": evidence.gpu.diagnostics.len().saturating_add(evidence.gpu.provenance.diagnostics.len()),
489 })
490}
491
492fn query_gpu_status(evidence: &EvidencePacket) -> serde_json::Value {
493 let diagnostics = bounded_diagnostics(evidence);
494 let correlation = report_availability(
495 evidence,
496 "nvtx_gpu_proj_trace",
497 evidence.gpu.coverage.nvtx_projection,
498 );
499 serde_json::json!({
500 "status": evidence.gpu.status,
501 "reason": &evidence.gpu.reason,
502 "provenance_binding": evidence.gpu.provenance.binding,
503 "capabilities": {
504 "gpu_correlation": &evidence.capabilities.gpu_correlation,
505 "provenance_binding": &evidence.capabilities.provenance_binding,
506 },
507 "coverage": &evidence.gpu.coverage,
508 "correlation": {
509 "status": correlation.status,
510 "reason": &correlation.reason,
511 "complete": correlation.is_available().then_some(evidence.gpu.correlation.complete),
512 },
513 "normalized_rows": gpu_row_counts(evidence),
514 "source_artifacts": {
515 "raw_report": evidence.gpu.raw_report.is_some(),
516 "csv_files": evidence.gpu.source_csv.len(),
517 },
518 "diagnostics": bounded_values(&diagnostics, QUERY_DIAGNOSTIC_LIMIT),
519 })
520}
521
522fn query_gpu_correlation(evidence: &EvidencePacket) -> serde_json::Value {
523 let availability = report_availability(
524 evidence,
525 "nvtx_gpu_proj_trace",
526 evidence.gpu.coverage.nvtx_projection,
527 );
528 let required_reports = required_report_states(
529 evidence,
530 &[("nvtx_gpu_proj_trace", evidence.gpu.coverage.nvtx_projection)],
531 );
532 if !availability.is_available() {
533 return serde_json::json!({
534 "status": availability.status,
535 "reason": &availability.reason,
536 "provenance_binding": evidence.gpu.provenance.binding,
537 "required_reports": required_reports,
538 "capabilities": {
539 "gpu_correlation": &evidence.capabilities.gpu_correlation,
540 "provenance_binding": &evidence.capabilities.provenance_binding,
541 },
542 "mode": null,
543 "clock_aligned": null,
544 "complete": null,
545 "correlation_reason": null,
546 "ledger": null,
547 });
548 }
549 let ledger = &evidence.gpu.correlation.ledger;
550 let duplicates = ledger
551 .duplicates
552 .iter()
553 .take(QUERY_LABEL_LIMIT)
554 .collect::<Vec<_>>();
555 serde_json::json!({
556 "status": availability.status,
557 "reason": &availability.reason,
558 "provenance_binding": evidence.gpu.provenance.binding,
559 "required_reports": required_reports,
560 "capabilities": {
561 "gpu_correlation": &evidence.capabilities.gpu_correlation,
562 "provenance_binding": &evidence.capabilities.provenance_binding,
563 },
564 "mode": &evidence.gpu.correlation.mode,
565 "clock_aligned": evidence.gpu.correlation.clock_aligned,
566 "complete": evidence.gpu.correlation.complete,
567 "correlation_reason": &evidence.gpu.correlation.reason,
568 "ledger": {
569 "expected": bounded_values(&ledger.expected, QUERY_LABEL_LIMIT),
570 "cpu_only": bounded_values(&ledger.cpu_only, QUERY_LABEL_LIMIT),
571 "observed": bounded_values(&ledger.observed, QUERY_LABEL_LIMIT),
572 "matched": bounded_values(&ledger.matched, QUERY_LABEL_LIMIT),
573 "missing_expected": bounded_values(&ledger.missing_expected, QUERY_LABEL_LIMIT),
574 "unexpected_observed": bounded_values(&ledger.unexpected_observed, QUERY_LABEL_LIMIT),
575 "unexpected_cpu_only": bounded_values(&ledger.unexpected_cpu_only, QUERY_LABEL_LIMIT),
576 "duplicates": {
577 "total": ledger.duplicates.len(),
578 "displayed": duplicates.len(),
579 "truncated": duplicates.len() < ledger.duplicates.len(),
580 "rows": duplicates,
581 },
582 },
583 })
584}
585
586fn query_gpu_phases(evidence: &EvidencePacket) -> serde_json::Value {
587 let projected_availability = report_availability(
588 evidence,
589 "nvtx_gpu_proj_trace",
590 evidence.gpu.coverage.nvtx_projection,
591 );
592 let attributed_availability = combined_report_availability(
593 evidence,
594 &[
595 ("nvtx_gpu_proj_trace", evidence.gpu.coverage.nvtx_projection),
596 ("cuda_gpu_trace", evidence.gpu.coverage.gpu_timeline),
597 ],
598 );
599 let required_reports = required_report_states(
600 evidence,
601 &[
602 ("nvtx_gpu_proj_trace", evidence.gpu.coverage.nvtx_projection),
603 ("cuda_gpu_trace", evidence.gpu.coverage.gpu_timeline),
604 ],
605 );
606
607 let mut projected = if projected_availability.is_available() {
608 evidence.gpu.nvtx_ranges.iter().collect::<Vec<_>>()
609 } else {
610 Vec::new()
611 };
612 projected.sort_by(|left, right| {
613 right
614 .projected_duration_ns
615 .unwrap_or_default()
616 .cmp(&left.projected_duration_ns.unwrap_or_default())
617 .then_with(|| left.name.cmp(&right.name))
618 });
619 let projected_sample_total = projected.len();
620 projected.truncate(QUERY_ROW_LIMIT);
621 let projected_rows = projected
622 .into_iter()
623 .map(|row| {
624 let join_keys = [
625 row.correlation_id.as_ref().map(|_| "correlation_id"),
626 row.device.as_ref().map(|_| "device"),
627 row.context.as_ref().map(|_| "context"),
628 row.stream.as_ref().map(|_| "stream"),
629 ]
630 .into_iter()
631 .flatten()
632 .collect::<Vec<_>>();
633 serde_json::json!({
634 "name": &row.name,
635 "semantic_key": &row.semantic_key,
636 "projected_start_ns": row.projected_start_ns,
637 "projected_duration_ns": row.projected_duration_ns,
638 "declared_gpu_operations": row.gpu_operations,
639 "join_keys": join_keys,
640 })
641 })
642 .collect::<Vec<_>>();
643 let projected_population_total = projected_availability
644 .is_available()
645 .then(|| total_report_rows(evidence, "nvtx_gpu_proj_trace", projected_sample_total));
646
647 let mut attributed = if attributed_availability.is_available() {
648 evidence.gpu.phase_attribution.iter().collect::<Vec<_>>()
649 } else {
650 Vec::new()
651 };
652 attributed.sort_by(|left, right| {
653 right
654 .gpu_busy_ns
655 .cmp(&left.gpu_busy_ns)
656 .then_with(|| left.semantic_key.cmp(&right.semantic_key))
657 });
658 let attributed_total = attributed_availability
659 .is_available()
660 .then_some(attributed.len());
661 attributed.truncate(QUERY_ROW_LIMIT);
662
663 serde_json::json!({
664 "status": attributed_availability.status,
665 "reason": &attributed_availability.reason,
666 "provenance_binding": evidence.gpu.provenance.binding,
667 "required_reports": required_reports,
668 "clock_plane": "nsight_projected_not_host_aligned",
669 "projected_ranges": {
670 "status": projected_availability.status,
671 "reason": &projected_availability.reason,
672 "population_total": projected_population_total,
673 "retained_sample_total": projected_availability.is_available().then_some(projected_sample_total),
674 "displayed": projected_rows.len(),
675 "population_truncated_before_ranking": projected_population_total.map(|total| projected_sample_total < total),
676 "display_truncated": projected_availability.is_available().then_some(projected_rows.len() < projected_sample_total),
677 "sample_selection": "earliest_original_start_rows",
678 "ordering": "projected_duration_ns_desc_within_retained_sample",
679 "global_duration_ranking": false,
680 "rows": projected_rows,
681 },
682 "attributed_phases": {
683 "status": attributed_availability.status,
684 "reason": &attributed_availability.reason,
685 "population_total": attributed_total,
686 "displayed": attributed.len(),
687 "display_truncated": attributed_total.map(|total| attributed.len() < total),
688 "ordering": "gpu_busy_ns_desc_across_normalized_population",
689 "rows": attributed,
690 },
691 })
692}
693
694fn query_gpu_kernels(evidence: &EvidencePacket) -> serde_json::Value {
695 let availability = report_availability(
696 evidence,
697 "cuda_gpu_kern_sum",
698 evidence.gpu.coverage.kernel_summary,
699 );
700 let mut kernels = if availability.is_available() {
701 evidence.gpu.kernels.iter().collect::<Vec<_>>()
702 } else {
703 Vec::new()
704 };
705 kernels.sort_by(|left, right| {
706 right
707 .total_ns
708 .cmp(&left.total_ns)
709 .then_with(|| left.name.cmp(&right.name))
710 });
711 let normalized_display_total = kernels.len();
712 kernels.truncate(QUERY_ROW_LIMIT);
713 let total = availability
714 .is_available()
715 .then(|| total_report_rows(evidence, "cuda_gpu_kern_sum", normalized_display_total));
716 serde_json::json!({
717 "status": availability.status,
718 "reason": &availability.reason,
719 "provenance_binding": evidence.gpu.provenance.binding,
720 "required_reports": required_report_states(
721 evidence,
722 &[("cuda_gpu_kern_sum", evidence.gpu.coverage.kernel_summary)],
723 ),
724 "clock_plane": "nsight_gpu",
725 "population_total": total,
726 "displayed": kernels.len(),
727 "display_truncated": total.map(|total| kernels.len() < total),
728 "rows": kernels,
729 })
730}
731
732fn query_gpu_attribution_gaps(evidence: &EvidencePacket) -> serde_json::Value {
733 let availability = combined_report_availability(
734 evidence,
735 &[
736 ("nvtx_gpu_proj_trace", evidence.gpu.coverage.nvtx_projection),
737 ("cuda_gpu_trace", evidence.gpu.coverage.gpu_timeline),
738 ],
739 );
740 let required_reports = required_report_states(
741 evidence,
742 &[
743 ("nvtx_gpu_proj_trace", evidence.gpu.coverage.nvtx_projection),
744 ("cuda_gpu_trace", evidence.gpu.coverage.gpu_timeline),
745 ],
746 );
747 if !availability.is_available() {
748 return serde_json::json!({
749 "status": availability.status,
750 "reason": &availability.reason,
751 "provenance_binding": evidence.gpu.provenance.binding,
752 "required_reports": required_reports,
753 "correlation_complete": null,
754 "correlation_reason": null,
755 "missing_expected": null,
756 "unexpected_observed": null,
757 "unexpected_cpu_only": null,
758 "duplicate_labels": null,
759 "matched_without_exact_gpu_busy_attribution": null,
760 "attributed_unexpected": null,
761 "projected_range_rows": null,
762 "exactly_attributed_phase_rows": null,
763 "truncated_reports": null,
764 "diagnostics": bounded_values(&bounded_diagnostics(evidence), QUERY_DIAGNOSTIC_LIMIT),
765 });
766 }
767 let ledger = &evidence.gpu.correlation.ledger;
768 let attributed = evidence
769 .gpu
770 .phase_attribution
771 .iter()
772 .map(|phase| phase.semantic_key.as_str())
773 .collect::<BTreeSet<_>>();
774 let expected = ledger
775 .expected
776 .iter()
777 .map(String::as_str)
778 .collect::<BTreeSet<_>>();
779 let matched_without_attribution = ledger
780 .matched
781 .iter()
782 .filter(|label| !attributed.contains(label.as_str()))
783 .cloned()
784 .collect::<Vec<_>>();
785 let attributed_unexpected = attributed
786 .difference(&expected)
787 .map(|label| (*label).to_string())
788 .collect::<Vec<_>>();
789 let truncated_reports = evidence
790 .gpu
791 .limits
792 .iter()
793 .filter(|(_, limit)| limit.truncated)
794 .map(|(report, limit)| {
795 serde_json::json!({
796 "report": report,
797 "total_rows": limit.total_rows,
798 "displayed_rows": limit.displayed_rows,
799 })
800 })
801 .take(QUERY_ROW_LIMIT)
802 .collect::<Vec<_>>();
803 let diagnostics = bounded_diagnostics(evidence);
804
805 serde_json::json!({
806 "status": availability.status,
807 "reason": &availability.reason,
808 "provenance_binding": evidence.gpu.provenance.binding,
809 "required_reports": required_reports,
810 "correlation_complete": evidence.gpu.correlation.complete,
811 "correlation_reason": &evidence.gpu.correlation.reason,
812 "missing_expected": bounded_values(&ledger.missing_expected, QUERY_LABEL_LIMIT),
813 "unexpected_observed": bounded_values(&ledger.unexpected_observed, QUERY_LABEL_LIMIT),
814 "unexpected_cpu_only": bounded_values(&ledger.unexpected_cpu_only, QUERY_LABEL_LIMIT),
815 "duplicate_labels": bounded_values(&ledger.duplicates, QUERY_LABEL_LIMIT),
816 "matched_without_exact_gpu_busy_attribution": bounded_values(&matched_without_attribution, QUERY_LABEL_LIMIT),
817 "attributed_unexpected": bounded_values(&attributed_unexpected, QUERY_LABEL_LIMIT),
818 "projected_range_rows": total_report_rows(evidence, "nvtx_gpu_proj_trace", evidence.gpu.nvtx_ranges.len()),
819 "exactly_attributed_phase_rows": evidence.gpu.phase_attribution.len(),
820 "truncated_reports": {
821 "total": evidence.gpu.limits.values().filter(|limit| limit.truncated).count(),
822 "displayed": truncated_reports.len(),
823 "truncated": truncated_reports.len() < evidence.gpu.limits.values().filter(|limit| limit.truncated).count(),
824 "rows": truncated_reports,
825 },
826 "diagnostics": bounded_values(&diagnostics, QUERY_DIAGNOSTIC_LIMIT),
827 })
828}
829
830fn gpu_row_counts(evidence: &EvidencePacket) -> serde_json::Value {
831 serde_json::json!({
832 "kernels": report_row_count(evidence, "cuda_gpu_kern_sum", evidence.gpu.coverage.kernel_summary, evidence.gpu.kernels.len()),
833 "runtime_calls": report_row_count(evidence, "cuda_api_sum", evidence.gpu.coverage.runtime_summary, evidence.gpu.runtime_calls.len()),
834 "memory_operations": report_row_count(evidence, "cuda_gpu_mem_time_sum", evidence.gpu.coverage.memory_summary, evidence.gpu.memory_operations.len()),
835 "projected_ranges": report_row_count(evidence, "nvtx_gpu_proj_trace", evidence.gpu.coverage.nvtx_projection, evidence.gpu.nvtx_ranges.len()),
836 "gpu_timeline": report_row_count(evidence, "cuda_gpu_trace", evidence.gpu.coverage.gpu_timeline, evidence.gpu.gpu_timeline.len()),
837 })
838}
839
840fn report_row_count(
841 evidence: &EvidencePacket,
842 report_kind: &str,
843 covered: bool,
844 fallback: usize,
845) -> serde_json::Value {
846 let availability = report_availability(evidence, report_kind, covered);
847 serde_json::json!({
848 "status": availability.status,
849 "reason": &availability.reason,
850 "total": availability.is_available().then(|| total_report_rows(evidence, report_kind, fallback)),
851 })
852}
853
854fn report_availability(
855 evidence: &EvidencePacket,
856 report_kind: &str,
857 covered: bool,
858) -> QueryAvailability {
859 let parse_failed = evidence
860 .gpu
861 .diagnostics
862 .iter()
863 .any(|diagnostic| diagnostic.contains(report_kind));
864 if covered && !parse_failed {
865 return QueryAvailability {
866 status: GpuEvidenceStatus::Available,
867 reason: None,
868 };
869 }
870 let failed = parse_failed || evidence.gpu.status == GpuEvidenceStatus::Failed;
871 QueryAvailability {
872 status: if failed {
873 GpuEvidenceStatus::Failed
874 } else {
875 GpuEvidenceStatus::Unavailable
876 },
877 reason: Some(if failed {
878 format!(
879 "Nsight report `{report_kind}` failed to normalize; its row population is unknown, not zero"
880 )
881 } else {
882 format!(
883 "Nsight report `{report_kind}` was not normalized; its row population is unknown, not zero"
884 )
885 }),
886 }
887}
888
889fn combined_report_availability(
890 evidence: &EvidencePacket,
891 reports: &[(&str, bool)],
892) -> QueryAvailability {
893 let unavailable = reports
894 .iter()
895 .filter_map(|(report, covered)| {
896 let availability = report_availability(evidence, report, *covered);
897 (!availability.is_available()).then_some((*report, availability.status))
898 })
899 .collect::<Vec<_>>();
900 if unavailable.is_empty() {
901 return QueryAvailability {
902 status: GpuEvidenceStatus::Available,
903 reason: None,
904 };
905 }
906 let failed = unavailable
907 .iter()
908 .any(|(_, status)| *status == GpuEvidenceStatus::Failed);
909 let names = unavailable
910 .iter()
911 .map(|(report, _)| *report)
912 .collect::<Vec<_>>();
913 QueryAvailability {
914 status: if failed {
915 GpuEvidenceStatus::Failed
916 } else {
917 GpuEvidenceStatus::Unavailable
918 },
919 reason: Some(format!(
920 "required normalized Nsight report(s) unavailable: {}; the derived result is unknown, not zero",
921 names.join(", ")
922 )),
923 }
924}
925
926fn required_report_states(
927 evidence: &EvidencePacket,
928 reports: &[(&str, bool)],
929) -> serde_json::Value {
930 let states = reports
931 .iter()
932 .map(|(report, covered)| {
933 (
934 (*report).to_string(),
935 serde_json::to_value(report_availability(evidence, report, *covered))
936 .expect("query availability is serializable"),
937 )
938 })
939 .collect::<std::collections::BTreeMap<_, _>>();
940 serde_json::to_value(states).expect("required report states are serializable")
941}
942
943fn total_report_rows(evidence: &EvidencePacket, report_kind: &str, fallback: usize) -> usize {
944 let total = evidence
945 .gpu
946 .limits
947 .iter()
948 .filter(|(name, _)| name.contains(report_kind))
949 .fold(0_usize, |sum, (_, limit)| {
950 sum.saturating_add(limit.total_rows)
951 });
952 total.max(fallback)
953}
954
955fn bounded_diagnostics(evidence: &EvidencePacket) -> Vec<String> {
956 let mut diagnostics = evidence
957 .gpu
958 .provenance
959 .diagnostics
960 .iter()
961 .chain(&evidence.gpu.diagnostics)
962 .cloned()
963 .collect::<Vec<_>>();
964 diagnostics.sort();
965 diagnostics.dedup();
966 diagnostics
967}
968
969fn bounded_values<T: Serialize>(values: &[T], limit: usize) -> serde_json::Value {
970 let displayed = values.len().min(limit);
971 serde_json::json!({
972 "total": values.len(),
973 "displayed": displayed,
974 "truncated": displayed < values.len(),
975 "rows": &values[..displayed],
976 })
977}
978
979fn sorted_nodes(
980 graph: &ExecutionGraph,
981 include: impl Fn(&GraphNode) -> bool,
982 value: impl Fn(&GraphNode) -> u64,
983) -> Vec<&GraphNode> {
984 let mut nodes = graph
985 .spans
986 .iter()
987 .filter(|node| include(node))
988 .collect::<Vec<_>>();
989 nodes.sort_by(|left, right| {
990 value(right)
991 .cmp(&value(left))
992 .then_with(|| left.id.cmp(&right.id))
993 });
994 nodes.truncate(50);
995 nodes
996}