1use 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
13pub 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 #[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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
37pub struct TraceRunMeta {
38 pub run_id: String,
40 pub correlation_id: String,
42 pub entrypoint: String,
44 pub phase: ExecutionPhase,
46 pub timestamp: String,
48 pub capture_step: u64,
50 pub warmup_steps: u64,
52 pub device: String,
54 #[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 pub tags: BTreeMap<String, String>,
64 #[serde(default, skip_serializing_if = "Option::is_none")]
66 pub candle_version: Option<String>,
67}
68
69impl TraceRunMeta {
70 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 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#[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#[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#[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#[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 pub measured: bool,
231 pub start_ns: u64,
233 #[serde(default)]
235 pub closed: bool,
236 #[serde(default)]
238 pub duration_ns: u64,
239 #[serde(default, skip_serializing_if = "Option::is_none")]
240 pub step: Option<ExecutionStep>,
241}