Skip to main content

candle_graph/trace/
schema.rs

1//! Schema types for the current candle-graph execution-evidence stream.
2
3use std::collections::BTreeMap;
4use std::fmt;
5
6use crate::phase::ExecutionPhase;
7use crate::phase::ExecutionStep;
8
9use serde::{Deserialize, Serialize};
10
11use crate::capability::CaptureContract;
12
13/// Schema identifier written into every trace document and expected on parse.
14pub const SCHEMA: &str = "candle-graph/trace/10";
15pub const PREVIOUS_SCHEMA: &str = "candle-graph/trace/9";
16
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
18pub struct ComparisonIdentity {
19    /// Stable caller-owned identity for the implementation or build under test.
20    #[serde(default, skip_serializing_if = "Option::is_none")]
21    pub implementation_id: Option<String>,
22    pub workload_id: String,
23    pub model_id: String,
24    pub config_id: String,
25    pub data_id: String,
26    pub seed_policy: String,
27    pub physical_batch: u64,
28    pub accumulation_steps: u64,
29    pub precision: String,
30    pub device_state: String,
31    #[serde(default, skip_serializing_if = "Option::is_none")]
32    pub pair_id: Option<String>,
33}
34
35/// Run metadata carried in the first JSONL `meta` event.
36#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
37pub struct TraceRunMeta {
38    /// Stable identifier for this probe run (UUID, build id, or caller-defined key).
39    pub run_id: String,
40    /// Stable caller-owned key shared with NVTX range labels and external artifacts.
41    pub correlation_id: String,
42    /// Analyzed entrypoint, e.g. `train::leworld_loss` or `Model::forward`.
43    pub entrypoint: String,
44    /// Execution phase: `train` or `infer`.
45    pub phase: ExecutionPhase,
46    /// ISO-8601 timestamp when the trace was captured.
47    pub timestamp: String,
48    /// One-based optimizer update or inference invocation selected for capture.
49    pub capture_step: u64,
50    /// Completed invocations before the selected capture.
51    pub warmup_steps: u64,
52    /// Device requested by the caller (`cpu`, `cuda:0`, ...).
53    pub device: String,
54    /// Whether the single measured region is bounded by device synchronizations.
55    /// Nested span durations remain governed by `timing_mode`.
56    #[serde(default)]
57    pub measured_region_device_synchronized: bool,
58    pub timing_mode: TimingMode,
59    pub capture_contract: CaptureContract,
60    #[serde(default, skip_serializing_if = "Option::is_none")]
61    pub comparison_identity: Option<ComparisonIdentity>,
62    /// Workload-specific provenance such as lesson, batch size, and source revision.
63    pub tags: BTreeMap<String, String>,
64    /// Optional candle crate version observed at probe time.
65    #[serde(default, skip_serializing_if = "Option::is_none")]
66    pub candle_version: Option<String>,
67}
68
69impl TraceRunMeta {
70    /// Validate provenance domain invariants before trusting a parsed or constructed run.
71    /// Producers enforce these at capture time; consumers re-check them because traces can
72    /// come from external tools or hand-edited files.
73    pub fn validate(&self) -> anyhow::Result<()> {
74        for (label, value) in [
75            ("run_id", &self.run_id),
76            ("correlation_id", &self.correlation_id),
77            ("entrypoint", &self.entrypoint),
78            ("timestamp", &self.timestamp),
79            ("device", &self.device),
80        ] {
81            anyhow::ensure!(
82                !value.trim().is_empty(),
83                "run provenance {label} must not be empty"
84            );
85        }
86        anyhow::ensure!(
87            self.capture_step > 0,
88            "capture_step must be one-based and greater than zero"
89        );
90        anyhow::ensure!(
91            self.warmup_steps < self.capture_step,
92            "warmup_steps ({}) must be fewer than the one-based capture_step ({})",
93            self.warmup_steps,
94            self.capture_step
95        );
96        if let Some(identity) = &self.comparison_identity {
97            identity.validate()?;
98        }
99        Ok(())
100    }
101}
102
103impl ComparisonIdentity {
104    /// Validate that every declared identity dimension carries a usable value.
105    pub fn validate(&self) -> anyhow::Result<()> {
106        for (label, value) in [
107            ("workload_id", &self.workload_id),
108            ("model_id", &self.model_id),
109            ("config_id", &self.config_id),
110            ("data_id", &self.data_id),
111            ("seed_policy", &self.seed_policy),
112            ("precision", &self.precision),
113            ("device_state", &self.device_state),
114        ] {
115            anyhow::ensure!(
116                !value.trim().is_empty(),
117                "comparison identity {label} must not be empty"
118            );
119        }
120        for (label, value) in [
121            ("implementation_id", &self.implementation_id),
122            ("pair_id", &self.pair_id),
123        ] {
124            if let Some(value) = value {
125                anyhow::ensure!(
126                    !value.trim().is_empty(),
127                    "comparison identity {label} must not be empty when declared"
128                );
129            }
130        }
131        anyhow::ensure!(
132            self.physical_batch > 0,
133            "comparison identity physical_batch must be greater than zero"
134        );
135        anyhow::ensure!(
136            self.accumulation_steps > 0,
137            "comparison identity accumulation_steps must be greater than zero"
138        );
139        Ok(())
140    }
141}
142
143#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
144#[serde(rename_all = "snake_case")]
145pub enum TimingMode {
146    Host,
147    DeviceSynchronized,
148}
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
151#[serde(rename_all = "snake_case")]
152pub enum RunOutcome {
153    Complete,
154    Failed,
155}
156
157/// Span classification — mirrors profiler function / op / module hierarchy.
158#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
159#[serde(rename_all = "snake_case")]
160pub enum SpanKind {
161    Function,
162    Op,
163    Module,
164}
165
166impl fmt::Display for SpanKind {
167    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168        match self {
169            Self::Function => write!(f, "function"),
170            Self::Op => write!(f, "op"),
171            Self::Module => write!(f, "module"),
172        }
173    }
174}
175
176/// Observed gradient presence / quality.
177#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
178#[serde(rename_all = "snake_case")]
179pub enum GradientState {
180    Present,
181    Missing,
182    Zero,
183    NonFinite,
184}
185
186impl GradientState {
187    pub(crate) fn norm_is_valid(self, norm: Option<f64>) -> bool {
188        match (self, norm) {
189            (Self::Present, Some(norm)) => norm.is_finite() && norm > 0.0,
190            (Self::Zero, Some(norm)) => norm == 0.0 && !norm.is_sign_negative(),
191            (Self::Missing | Self::NonFinite, None) => true,
192            _ => false,
193        }
194    }
195}
196
197impl fmt::Display for GradientState {
198    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199        match self {
200            Self::Present => write!(f, "present"),
201            Self::Missing => write!(f, "missing"),
202            Self::Zero => write!(f, "zero"),
203            Self::NonFinite => write!(f, "non_finite"),
204        }
205    }
206}
207
208/// Aggregated profiler statistics derived from a [`super::document::TraceDocument`].
209#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
210pub struct TraceSummary {
211    pub op_count: usize,
212    pub total_ns: u64,
213    pub span_count: usize,
214    pub root_span_count: usize,
215    pub max_depth: usize,
216    pub alloc_count: usize,
217    pub free_count: usize,
218    pub logical_peak_bytes: Option<u64>,
219}
220
221/// Resolved span node assembled from `span_start` / `span_end` events.
222#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
223pub struct SpanRecord {
224    pub id: String,
225    #[serde(default, skip_serializing_if = "Option::is_none")]
226    pub parent_id: Option<String>,
227    pub name: String,
228    pub kind: SpanKind,
229    /// True only for the caller-controlled region used as the run's performance total.
230    pub measured: bool,
231    /// Monotonic timestamp in nanoseconds since the profile run started.
232    pub start_ns: u64,
233    /// True when a matching `span_end` was observed.
234    #[serde(default)]
235    pub closed: bool,
236    /// Wall duration from `span_end` (nanoseconds).
237    #[serde(default)]
238    pub duration_ns: u64,
239    #[serde(default, skip_serializing_if = "Option::is_none")]
240    pub step: Option<ExecutionStep>,
241}