1use std::collections::{BTreeMap, BTreeSet, HashSet};
7use std::fmt;
8use std::io::Write;
9
10use anyhow::{bail, Context, Result};
11use serde::{Deserialize, Serialize};
12
13pub const SCHEMA: &str = "candle-graph/runtime/1";
15pub const SCHEMA_V2: &str = "candle-graph/runtime/2";
16pub const SCHEMA_V3: &str = "candle-graph/runtime/3";
17
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20pub struct RunMetadata {
21 pub entrypoint: String,
23 pub profile: String,
25 #[serde(default)]
27 pub cargo_features: Vec<String>,
28 #[serde(default)]
30 pub cfg: Vec<String>,
31 #[serde(default, skip_serializing_if = "Option::is_none")]
34 pub analysis_id: Option<String>,
35 #[serde(default, skip_serializing_if = "Option::is_none")]
38 pub build_id: Option<String>,
39 #[serde(default, skip_serializing_if = "Option::is_none")]
41 pub phase: Option<String>,
42}
43
44#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
49pub struct ExpectedIdentity {
50 #[serde(default, skip_serializing_if = "Option::is_none")]
51 pub analysis_id: Option<String>,
52 #[serde(default, skip_serializing_if = "Option::is_none")]
53 pub build_id: Option<String>,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
58#[serde(rename_all = "snake_case")]
59pub enum IdentityField {
60 AnalysisId,
61 BuildId,
62}
63
64impl fmt::Display for IdentityField {
65 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66 match self {
67 Self::AnalysisId => write!(f, "analysis_id"),
68 Self::BuildId => write!(f, "build_id"),
69 }
70 }
71}
72
73#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
75pub struct IdentityMismatch {
76 pub field: IdentityField,
77 pub expected: String,
78 pub observed: String,
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
85#[serde(rename_all = "snake_case")]
86pub enum ObservationConfidence {
87 Proven,
89 Unknown,
91}
92
93#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
95pub struct TensorObservation {
96 pub event_id: String,
98 #[serde(default, skip_serializing_if = "Option::is_none")]
100 pub static_id: Option<String>,
101 #[serde(default, skip_serializing_if = "Option::is_none")]
103 pub source: Option<String>,
104 #[serde(default, skip_serializing_if = "Option::is_none")]
106 pub step: Option<u64>,
107 pub shape: Vec<usize>,
108 pub dtype: String,
109 pub device: String,
110 pub contiguous: bool,
112 pub requires_grad: bool,
113 #[serde(default, skip_serializing_if = "Option::is_none")]
115 pub storage_id: Option<String>,
116}
117
118#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
120pub struct OperationObservation {
121 pub event_id: String,
122 pub op: String,
124 #[serde(default, skip_serializing_if = "Option::is_none")]
126 pub static_id: Option<String>,
127 #[serde(default, skip_serializing_if = "Option::is_none")]
129 pub source: Option<String>,
130 #[serde(default)]
132 pub inputs: Vec<String>,
133 #[serde(default, skip_serializing_if = "Option::is_none")]
135 pub output: Option<String>,
136 #[serde(default, skip_serializing_if = "Option::is_none")]
138 pub step: Option<u64>,
139 #[serde(default, skip_serializing_if = "Option::is_none")]
141 pub duration_ns: Option<u64>,
142}
143
144#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
146pub struct EdgeTimingObservation {
147 pub event_id: String,
148 pub from_static_id: String,
149 pub to_static_id: String,
150 pub duration_ns: u64,
151 #[serde(default, skip_serializing_if = "Option::is_none")]
152 pub step: Option<u64>,
153}
154
155#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
157pub struct ParamIdentity {
158 pub root: String,
160 pub key: String,
162}
163
164#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
166#[serde(rename_all = "snake_case")]
167pub enum GradientState {
168 Present,
169 Missing,
170 Zero,
171 NonFinite,
172}
173
174impl fmt::Display for GradientState {
175 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
176 match self {
177 Self::Present => write!(f, "present"),
178 Self::Missing => write!(f, "missing"),
179 Self::Zero => write!(f, "zero"),
180 Self::NonFinite => write!(f, "non_finite"),
181 }
182 }
183}
184
185#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
187pub struct GradientFact {
188 pub event_id: String,
189 pub root: String,
190 pub key: String,
191 pub state: GradientState,
192 #[serde(default, skip_serializing_if = "Option::is_none")]
194 pub step: Option<u64>,
195 #[serde(default, skip_serializing_if = "Option::is_none")]
197 pub norm: Option<f64>,
198}
199
200impl GradientFact {
201 pub fn identity(&self) -> ParamIdentity {
202 ParamIdentity {
203 root: self.root.clone(),
204 key: self.key.clone(),
205 }
206 }
207
208 pub fn validate(&self) -> Result<()> {
210 match self.state {
211 GradientState::Missing => {
212 if self.norm.is_some() {
213 bail!(
214 "invalid gradient fact {}:{}: missing state must not carry a norm",
215 self.root,
216 self.key
217 );
218 }
219 }
220 GradientState::Zero => {
221 if let Some(n) = self.norm {
222 if n != 0.0 || !n.is_finite() {
223 bail!(
224 "invalid gradient fact {}:{}: zero state requires norm 0.0 when set, got {n}",
225 self.root,
226 self.key
227 );
228 }
229 }
230 }
231 GradientState::Present => {
232 if let Some(n) = self.norm {
233 if !n.is_finite() {
234 bail!(
235 "invalid gradient fact {}:{}: present state cannot have non-finite norm",
236 self.root,
237 self.key
238 );
239 }
240 if n == 0.0 {
241 bail!(
242 "invalid gradient fact {}:{}: present state cannot have zero norm (use zero)",
243 self.root,
244 self.key
245 );
246 }
247 }
248 }
249 GradientState::NonFinite => {
250 if let Some(n) = self.norm {
251 if n.is_finite() {
252 bail!(
253 "invalid gradient fact {}:{}: non_finite state cannot have finite norm {n}",
254 self.root,
255 self.key
256 );
257 }
258 }
259 }
260 }
261 Ok(())
262 }
263}
264
265#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
267pub struct ValueObservation {
268 pub event_id: String,
269 #[serde(default, skip_serializing_if = "Option::is_none")]
270 pub static_id: Option<String>,
271 #[serde(default, skip_serializing_if = "Option::is_none")]
272 pub source: Option<String>,
273 #[serde(default, skip_serializing_if = "Option::is_none")]
274 pub step: Option<u64>,
275 pub min: f64,
276 pub max: f64,
277 pub abs_max: f64,
278 #[serde(default)]
279 pub nonfinite_count: u64,
280 #[serde(default)]
281 pub saturated_count: u64,
282}
283
284#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
286pub struct RuntimeTrace {
287 pub schema: String,
288 pub run: RunMetadata,
289 #[serde(default)]
290 pub tensors: Vec<TensorObservation>,
291 #[serde(default)]
292 pub operations: Vec<OperationObservation>,
293 #[serde(default)]
294 pub gradients: Vec<GradientFact>,
295 #[serde(default)]
296 pub values: Vec<ValueObservation>,
297 #[serde(default)]
298 pub edge_timings: Vec<EdgeTimingObservation>,
299}
300
301#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
303#[serde(tag = "kind", rename_all = "snake_case")]
304pub enum RuntimeEvent {
305 Meta {
307 schema: String,
308 #[serde(flatten)]
309 run: RunMetadata,
310 },
311 Tensor(TensorObservation),
312 Operation(OperationObservation),
313 Gradient(GradientFact),
314 Value(ValueObservation),
315 EdgeTiming(EdgeTimingObservation),
316}
317
318pub struct RuntimeTraceWriter<W: Write> {
324 writer: W,
325 seen_event_ids: HashSet<String>,
326}
327
328impl<W: Write> RuntimeTraceWriter<W> {
329 pub fn new(writer: W, run: RunMetadata) -> Result<Self> {
331 Self::new_with_schema(writer, SCHEMA, run)
332 }
333
334 pub fn new_with_schema(writer: W, schema: &str, run: RunMetadata) -> Result<Self> {
336 let mut output = Self {
337 writer,
338 seen_event_ids: HashSet::new(),
339 };
340 output.write_event(&RuntimeEvent::Meta {
341 schema: schema.to_string(),
342 run,
343 })?;
344 Ok(output)
345 }
346
347 pub fn tensor(&mut self, observation: TensorObservation) -> Result<()> {
349 self.reserve_event_id(&observation.event_id)?;
350 self.write_event(&RuntimeEvent::Tensor(observation))
351 }
352
353 pub fn operation(&mut self, observation: OperationObservation) -> Result<()> {
355 self.reserve_event_id(&observation.event_id)?;
356 self.write_event(&RuntimeEvent::Operation(observation))
357 }
358
359 pub fn gradient(&mut self, fact: GradientFact) -> Result<()> {
361 fact.validate()?;
362 self.reserve_event_id(&fact.event_id)?;
363 self.write_event(&RuntimeEvent::Gradient(fact))
364 }
365
366 pub fn value(&mut self, observation: ValueObservation) -> Result<()> {
368 self.reserve_event_id(&observation.event_id)?;
369 self.write_event(&RuntimeEvent::Value(observation))
370 }
371
372 pub fn edge_timing(&mut self, observation: EdgeTimingObservation) -> Result<()> {
374 self.reserve_event_id(&observation.event_id)?;
375 self.write_event(&RuntimeEvent::EdgeTiming(observation))
376 }
377
378 pub fn flush(&mut self) -> Result<()> {
379 self.writer.flush().context("flushing runtime JSONL trace")
380 }
381
382 pub fn finish(mut self) -> Result<W> {
384 self.writer
385 .flush()
386 .context("flushing runtime JSONL trace")?;
387 Ok(self.writer)
388 }
389
390 fn reserve_event_id(&mut self, event_id: &str) -> Result<()> {
391 if event_id.is_empty() {
392 bail!("empty event_id is not allowed");
393 }
394 if !self.seen_event_ids.insert(event_id.to_string()) {
395 bail!("duplicate event_id `{event_id}`");
396 }
397 Ok(())
398 }
399
400 fn write_event(&mut self, event: &RuntimeEvent) -> Result<()> {
401 let mut line = serde_json::to_vec(event).context("serializing runtime JSONL event")?;
402 line.push(b'\n');
403 self.writer
404 .write_all(&line)
405 .context("writing runtime JSONL event")
406 }
407}
408
409#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
411#[serde(rename_all = "snake_case")]
412pub enum TensorConflictKind {
413 Dtype,
414 Device,
415 Shape,
416 Layout,
417 RequiresGrad,
418}
419
420fn unknown_confidence() -> ObservationConfidence {
421 ObservationConfidence::Unknown
422}
423
424#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
426pub struct TensorConflict {
427 pub static_id: String,
428 pub kind: TensorConflictKind,
429 pub values: Vec<String>,
431 #[serde(default = "unknown_confidence")]
433 pub confidence: ObservationConfidence,
434}
435
436#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
438pub struct GradientConflict {
439 pub identity: ParamIdentity,
440 pub states: Vec<String>,
442 pub event_ids: Vec<String>,
444 #[serde(default = "unknown_confidence")]
446 pub confidence: ObservationConfidence,
447}
448
449#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
451pub struct RuntimeAudit {
452 pub missing_gradients: Vec<ParamIdentity>,
453 pub non_finite_gradients: Vec<ParamIdentity>,
454 pub zero_gradients: Vec<ParamIdentity>,
455 #[serde(default)]
456 pub tensor_conflicts: Vec<TensorConflict>,
457 #[serde(default)]
458 pub gradient_conflicts: Vec<GradientConflict>,
459 #[serde(default)]
460 pub identity_mismatches: Vec<IdentityMismatch>,
461 #[serde(default, skip_serializing_if = "Option::is_none")]
463 pub first_non_finite_step: Option<u64>,
464 #[serde(default)]
466 pub saturating_activations: Vec<ParamIdentity>,
467}
468
469impl RuntimeAudit {
470 pub fn is_clean(&self) -> bool {
471 self.missing_gradients.is_empty()
472 && self.non_finite_gradients.is_empty()
473 && self.zero_gradients.is_empty()
474 && self.tensor_conflicts.is_empty()
475 && self.gradient_conflicts.is_empty()
476 && self.identity_mismatches.is_empty()
477 }
478
479 pub fn has_unknown_evidence(&self) -> bool {
481 !self.tensor_conflicts.is_empty()
482 || !self.gradient_conflicts.is_empty()
483 || !self.identity_mismatches.is_empty()
484 }
485}
486
487impl RuntimeTrace {
488 pub fn normalize(&mut self) {
490 self.run.cargo_features.sort();
491 self.run.cargo_features.dedup();
492 self.run.cfg.sort();
493 self.run.cfg.dedup();
494
495 self.tensors.sort_by(|a, b| {
496 a.event_id
497 .cmp(&b.event_id)
498 .then_with(|| a.static_id.cmp(&b.static_id))
499 .then_with(|| a.source.cmp(&b.source))
500 });
501 self.operations.sort_by(|a, b| {
502 a.event_id
503 .cmp(&b.event_id)
504 .then_with(|| a.op.cmp(&b.op))
505 .then_with(|| a.static_id.cmp(&b.static_id))
506 });
507 self.gradients.sort_by(|a, b| {
508 a.root
509 .cmp(&b.root)
510 .then_with(|| a.key.cmp(&b.key))
511 .then_with(|| a.step.cmp(&b.step))
512 .then_with(|| a.event_id.cmp(&b.event_id))
513 });
514 self.values.sort_by(|a, b| {
515 a.event_id
516 .cmp(&b.event_id)
517 .then_with(|| a.step.cmp(&b.step))
518 .then_with(|| a.source.cmp(&b.source))
519 });
520 self.edge_timings.sort_by(|a, b| {
521 a.from_static_id
522 .cmp(&b.from_static_id)
523 .then_with(|| a.to_static_id.cmp(&b.to_static_id))
524 .then_with(|| a.step.cmp(&b.step))
525 .then_with(|| a.event_id.cmp(&b.event_id))
526 });
527 }
528
529 pub fn validate(&self) -> Result<()> {
534 if self.schema != SCHEMA && self.schema != SCHEMA_V2 && self.schema != SCHEMA_V3 {
535 bail!(
536 "unsupported runtime schema {:?}; expected {:?}, {:?}, or {:?}",
537 self.schema,
538 SCHEMA,
539 SCHEMA_V2,
540 SCHEMA_V3
541 );
542 }
543
544 let mut seen = HashSet::new();
545 let mut push_id = |id: &str| -> Result<()> {
546 if id.is_empty() {
547 bail!("empty event_id is not allowed");
548 }
549 if !seen.insert(id.to_string()) {
550 bail!("duplicate event_id `{id}`");
551 }
552 Ok(())
553 };
554
555 for t in &self.tensors {
556 push_id(&t.event_id)?;
557 }
558 for op in &self.operations {
559 push_id(&op.event_id)?;
560 }
561 for g in &self.gradients {
562 push_id(&g.event_id)?;
563 g.validate()?;
564 }
565 for v in &self.values {
566 push_id(&v.event_id)?;
567 }
568 for edge in &self.edge_timings {
569 push_id(&edge.event_id)?;
570 }
571 Ok(())
572 }
573
574 pub fn finalize(mut self) -> Result<Self> {
576 self.normalize();
577 self.validate()?;
578 Ok(self)
579 }
580
581 pub fn check_identity(&self, expected: &ExpectedIdentity) -> Vec<IdentityMismatch> {
586 let mut out = Vec::new();
587 if let (Some(expected_id), Some(observed)) = (
588 expected.analysis_id.as_deref(),
589 self.run.analysis_id.as_deref(),
590 ) {
591 if expected_id != observed {
592 out.push(IdentityMismatch {
593 field: IdentityField::AnalysisId,
594 expected: expected_id.to_string(),
595 observed: observed.to_string(),
596 });
597 }
598 }
599 if let (Some(expected_id), Some(observed)) =
600 (expected.build_id.as_deref(), self.run.build_id.as_deref())
601 {
602 if expected_id != observed {
603 out.push(IdentityMismatch {
604 field: IdentityField::BuildId,
605 expected: expected_id.to_string(),
606 observed: observed.to_string(),
607 });
608 }
609 }
610 out
611 }
612
613 pub fn require_identity(&self, expected: &ExpectedIdentity) -> Result<()> {
617 let mismatches = self.check_identity(expected);
618 if let Some(first) = mismatches.first() {
619 bail!(
620 "runtime {} mismatch: expected {:?}, observed {:?}",
621 first.field,
622 first.expected,
623 first.observed
624 );
625 }
626 Ok(())
627 }
628
629 pub fn tensors_by_static_id(&self, static_id: &str) -> Vec<&TensorObservation> {
631 self.tensors
632 .iter()
633 .filter(|t| t.static_id.as_deref() == Some(static_id))
634 .collect()
635 }
636
637 pub fn agreed_tensor(&self, static_id: &str) -> Option<&TensorObservation> {
642 let obs = self.tensors_by_static_id(static_id);
643 if obs.is_empty() {
644 return None;
645 }
646 let first = obs[0];
647 for other in &obs[1..] {
648 if other.shape != first.shape
649 || other.dtype != first.dtype
650 || other.device != first.device
651 || other.contiguous != first.contiguous
652 || other.requires_grad != first.requires_grad
653 {
654 return None;
655 }
656 }
657 Some(first)
658 }
659
660 pub fn tensor_confidence(&self, static_id: &str) -> ObservationConfidence {
662 match self.agreed_tensor(static_id) {
663 Some(_) => ObservationConfidence::Proven,
664 None => ObservationConfidence::Unknown,
665 }
666 }
667
668 pub fn gradients_for(&self, root: &str, key: &str) -> Vec<&GradientFact> {
670 self.gradients
671 .iter()
672 .filter(|g| g.root == root && g.key == key)
673 .collect()
674 }
675
676 pub fn gradient(&self, root: &str, key: &str) -> Option<&GradientFact> {
681 let facts = self.gradients_for(root, key);
682 agreed_gradient(&facts)
683 }
684
685 pub fn gradient_confidence(&self, root: &str, key: &str) -> ObservationConfidence {
687 match self.gradient(root, key) {
688 Some(_) => ObservationConfidence::Proven,
689 None => ObservationConfidence::Unknown,
690 }
691 }
692
693 pub fn audit(&self) -> RuntimeAudit {
695 self.audit_with_identity(None)
696 }
697
698 pub fn audit_with_identity(&self, expected: Option<&ExpectedIdentity>) -> RuntimeAudit {
700 let gradient_conflicts = self.collect_gradient_conflicts();
701 let conflicted: HashSet<ParamIdentity> = gradient_conflicts
702 .iter()
703 .map(|c| c.identity.clone())
704 .collect();
705
706 let mut missing = BTreeSet::new();
707 let mut non_finite = BTreeSet::new();
708 let mut zero = BTreeSet::new();
709
710 let mut by_param: BTreeMap<ParamIdentity, Vec<&GradientFact>> = BTreeMap::new();
712 for g in &self.gradients {
713 by_param.entry(g.identity()).or_default().push(g);
714 }
715 for (id, facts) in by_param {
716 if conflicted.contains(&id) {
717 continue;
718 }
719 let Some(g) = latest_agreed_gradient(&facts).or_else(|| agreed_gradient(&facts)) else {
720 continue;
721 };
722 match g.state {
723 GradientState::Missing => {
724 missing.insert(id);
725 }
726 GradientState::NonFinite => {
727 non_finite.insert(id);
728 }
729 GradientState::Zero => {
730 zero.insert(id);
731 }
732 GradientState::Present => {}
733 }
734 }
735
736 let mut by_static: BTreeMap<&str, Vec<&TensorObservation>> = BTreeMap::new();
737 for t in &self.tensors {
738 if let Some(sid) = t.static_id.as_deref() {
739 by_static.entry(sid).or_default().push(t);
740 }
741 }
742
743 let mut conflicts = Vec::new();
744 for (static_id, obs) in by_static {
745 if obs.len() < 2 {
746 continue;
747 }
748 push_conflict(
749 &mut conflicts,
750 static_id,
751 TensorConflictKind::Dtype,
752 |t| t.dtype.clone(),
753 &obs,
754 );
755 push_conflict(
756 &mut conflicts,
757 static_id,
758 TensorConflictKind::Device,
759 |t| t.device.clone(),
760 &obs,
761 );
762 push_conflict(
763 &mut conflicts,
764 static_id,
765 TensorConflictKind::Shape,
766 |t| format_shape(&t.shape),
767 &obs,
768 );
769 push_conflict(
770 &mut conflicts,
771 static_id,
772 TensorConflictKind::Layout,
773 |t| t.contiguous.to_string(),
774 &obs,
775 );
776 push_conflict(
777 &mut conflicts,
778 static_id,
779 TensorConflictKind::RequiresGrad,
780 |t| t.requires_grad.to_string(),
781 &obs,
782 );
783 }
784 conflicts.sort_by(|a, b| {
785 a.static_id
786 .cmp(&b.static_id)
787 .then_with(|| a.kind.cmp(&b.kind))
788 });
789
790 let identity_mismatches = expected
791 .map(|expected| self.check_identity(expected))
792 .unwrap_or_default();
793
794 let first_non_finite_step = self
795 .gradients
796 .iter()
797 .filter(|g| matches!(g.state, GradientState::NonFinite))
798 .filter_map(|g| g.step)
799 .min();
800
801 let mut saturating = BTreeSet::new();
802 for value in &self.values {
803 if value.saturated_count == 0 {
804 continue;
805 }
806 if let Some(source) = &value.source {
807 saturating.insert(ParamIdentity {
808 root: "value".into(),
809 key: source.clone(),
810 });
811 }
812 }
813
814 RuntimeAudit {
815 missing_gradients: missing.into_iter().collect(),
816 non_finite_gradients: non_finite.into_iter().collect(),
817 zero_gradients: zero.into_iter().collect(),
818 tensor_conflicts: conflicts,
819 gradient_conflicts,
820 identity_mismatches,
821 first_non_finite_step,
822 saturating_activations: saturating.into_iter().collect(),
823 }
824 }
825
826 fn collect_gradient_conflicts(&self) -> Vec<GradientConflict> {
827 let mut by_param: BTreeMap<ParamIdentity, Vec<&GradientFact>> = BTreeMap::new();
828 for g in &self.gradients {
829 by_param.entry(g.identity()).or_default().push(g);
830 }
831 let mut out = Vec::new();
832 for (identity, facts) in by_param {
833 if facts.len() < 2 {
834 continue;
835 }
836 if facts.iter().all(|g| g.step.is_some()) {
838 let steps: BTreeSet<_> = facts.iter().filter_map(|g| g.step).collect();
839 if steps.len() == facts.len() {
840 continue;
841 }
842 }
843 if latest_agreed_gradient(&facts).is_some() || agreed_gradient(&facts).is_some() {
844 continue;
845 }
846 let mut states: BTreeSet<String> = BTreeSet::new();
847 let mut event_ids: BTreeSet<String> = BTreeSet::new();
848 for g in &facts {
849 states.insert(g.state.to_string());
850 event_ids.insert(g.event_id.clone());
851 }
852 let mut norms: BTreeSet<String> = BTreeSet::new();
854 for g in &facts {
855 norms.insert(match g.norm {
856 Some(n) => format!("{n}"),
857 None => "none".to_string(),
858 });
859 }
860 if states.len() <= 1 && norms.len() <= 1 {
861 continue;
862 }
863 out.push(GradientConflict {
864 identity,
865 states: states.into_iter().collect(),
866 event_ids: event_ids.into_iter().collect(),
867 confidence: ObservationConfidence::Unknown,
868 });
869 }
870 out.sort_by(|a, b| a.identity.cmp(&b.identity));
871 out
872 }
873}
874
875fn agreed_gradient<'a>(facts: &[&'a GradientFact]) -> Option<&'a GradientFact> {
877 let first = facts.first().copied()?;
878 for other in facts.iter().skip(1) {
879 if other.state != first.state {
880 return None;
881 }
882 match (first.norm, other.norm) {
883 (None, None) => {}
884 (Some(a), Some(b)) if float_eq(a, b) => {}
885 (None, Some(_)) | (Some(_), None) => return None,
886 (Some(_), Some(_)) => return None,
887 }
888 }
889 Some(first)
890}
891
892fn latest_agreed_gradient<'a>(facts: &[&'a GradientFact]) -> Option<&'a GradientFact> {
894 if facts.is_empty() || !facts.iter().all(|g| g.step.is_some()) {
895 return None;
896 }
897 facts.iter().copied().max_by_key(|g| g.step.unwrap_or(0))
898}
899
900fn float_eq(a: f64, b: f64) -> bool {
901 if a.is_nan() && b.is_nan() {
902 return true;
903 }
904 a == b
905}
906
907fn format_shape(shape: &[usize]) -> String {
908 format!(
909 "[{}]",
910 shape
911 .iter()
912 .map(|d| d.to_string())
913 .collect::<Vec<_>>()
914 .join(", ")
915 )
916}
917
918fn push_conflict(
919 out: &mut Vec<TensorConflict>,
920 static_id: &str,
921 kind: TensorConflictKind,
922 project: impl Fn(&TensorObservation) -> String,
923 obs: &[&TensorObservation],
924) {
925 let mut values: BTreeSet<String> = BTreeSet::new();
926 for t in obs {
927 values.insert(project(t));
928 }
929 if values.len() > 1 {
930 out.push(TensorConflict {
931 static_id: static_id.to_string(),
932 kind,
933 values: values.into_iter().collect(),
934 confidence: ObservationConfidence::Unknown,
935 });
936 }
937}
938
939pub fn parse_json(input: &str) -> Result<RuntimeTrace> {
941 let trace: RuntimeTrace =
942 serde_json::from_str(input).context("failed to parse runtime JSON document")?;
943 trace.finalize()
944}
945
946pub fn parse_jsonl(input: &str) -> Result<RuntimeTrace> {
948 let mut schema: Option<String> = None;
949 let mut run: Option<RunMetadata> = None;
950 let mut tensors = Vec::new();
951 let mut operations = Vec::new();
952 let mut gradients = Vec::new();
953 let mut values = Vec::new();
954 let mut edge_timings = Vec::new();
955
956 for (line_no, line) in input.lines().enumerate() {
957 let line = line.trim();
958 if line.is_empty() {
959 continue;
960 }
961 let event: RuntimeEvent = serde_json::from_str(line)
962 .with_context(|| format!("failed to parse runtime JSONL line {}", line_no + 1))?;
963 match event {
964 RuntimeEvent::Meta {
965 schema: s,
966 run: meta,
967 } => {
968 if schema.is_some() || run.is_some() {
969 bail!(
970 "duplicate meta event on JSONL line {}; only one meta record is allowed",
971 line_no + 1
972 );
973 }
974 schema = Some(s);
975 run = Some(meta);
976 }
977 RuntimeEvent::Tensor(t) => tensors.push(t),
978 RuntimeEvent::Operation(op) => operations.push(op),
979 RuntimeEvent::Gradient(g) => gradients.push(g),
980 RuntimeEvent::Value(v) => values.push(v),
981 RuntimeEvent::EdgeTiming(edge) => edge_timings.push(edge),
982 }
983 }
984
985 let schema = schema.unwrap_or_else(|| SCHEMA.to_string());
986 let run = run.context("JSONL stream is missing a meta event with run metadata")?;
987
988 RuntimeTrace {
989 schema,
990 run,
991 tensors,
992 operations,
993 gradients,
994 values,
995 edge_timings,
996 }
997 .finalize()
998}
999
1000pub fn parse(input: &str) -> Result<RuntimeTrace> {
1002 let trimmed = input.trim();
1003 if trimmed.is_empty() {
1004 bail!("empty runtime evidence input");
1005 }
1006 if trimmed.starts_with('{') && !trimmed.contains('\n') {
1008 return parse_json(trimmed);
1009 }
1010 if trimmed.starts_with('{') {
1011 if let Ok(value) = serde_json::from_str::<serde_json::Value>(trimmed) {
1013 if value.is_object() && value.get("schema").is_some() && value.get("run").is_some() {
1014 let trace: RuntimeTrace = serde_json::from_value(value)
1015 .context("failed to parse runtime JSON document")?;
1016 return trace.finalize();
1017 }
1018 }
1019 }
1020 parse_jsonl(input)
1021}