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