Skip to main content

candle_graph/
profile.rs

1//! Runtime profiler: attach during train or inference to emit timed operation/edge traces.
2//!
3//! Writes `candle-graph/runtime/3` JSONL compatible with [`crate::runtime`] and mergeable
4//! into the unified model IR via static `static_id` correlation.
5
6use std::collections::HashMap;
7use std::fs::OpenOptions;
8use std::io;
9use std::path::{Path, PathBuf};
10use std::time::Instant;
11
12use anyhow::{Context, Result};
13
14use crate::phase::ExecutionPhase;
15use crate::runtime::{
16    EdgeTimingObservation, OperationObservation, RunMetadata, RuntimeTraceWriter, SCHEMA_V3,
17};
18
19/// Configuration for a profiling session started alongside train or inference.
20#[derive(Debug, Clone)]
21pub struct ProfileConfig {
22    pub entrypoint: String,
23    pub phase: ExecutionPhase,
24    pub profile: String,
25    pub cargo_features: Vec<String>,
26    pub analysis_id: Option<String>,
27    pub build_id: Option<String>,
28}
29
30impl ProfileConfig {
31    pub fn train(entrypoint: impl Into<String>) -> Self {
32        Self {
33            entrypoint: entrypoint.into(),
34            phase: ExecutionPhase::Train,
35            profile: "release".into(),
36            cargo_features: Vec::new(),
37            analysis_id: None,
38            build_id: None,
39        }
40    }
41
42    pub fn infer(entrypoint: impl Into<String>) -> Self {
43        Self {
44            entrypoint: entrypoint.into(),
45            phase: ExecutionPhase::Infer,
46            profile: "release".into(),
47            cargo_features: Vec::new(),
48            analysis_id: None,
49            build_id: None,
50        }
51    }
52}
53
54struct ActiveOp {
55    static_id: String,
56    op: String,
57    inputs: Vec<String>,
58    started: Instant,
59}
60
61/// Streaming profiler session writing JSONL runtime v3 events.
62pub struct ProfileSession {
63    path: PathBuf,
64    writer: RuntimeTraceWriter<io::BufWriter<std::fs::File>>,
65    phase: ExecutionPhase,
66    step: u64,
67    active: HashMap<String, ActiveOp>,
68    event_counter: u64,
69}
70
71impl ProfileSession {
72    /// Open (or truncate) a JSONL profile trace at `path`.
73    pub fn open(path: impl AsRef<Path>, config: ProfileConfig) -> Result<Self> {
74        let path = path.as_ref().to_path_buf();
75        if let Some(parent) = path.parent() {
76            std::fs::create_dir_all(parent)
77                .with_context(|| format!("create profile dir {}", parent.display()))?;
78        }
79        let file = OpenOptions::new()
80            .create(true)
81            .write(true)
82            .truncate(true)
83            .open(&path)
84            .with_context(|| format!("open profile trace {}", path.display()))?;
85        let run = RunMetadata {
86            entrypoint: config.entrypoint,
87            profile: config.profile,
88            cargo_features: config.cargo_features,
89            cfg: Vec::new(),
90            analysis_id: config.analysis_id,
91            build_id: config.build_id,
92            phase: Some(config.phase.as_str().to_string()),
93        };
94        let writer = RuntimeTraceWriter::new_with_schema(io::BufWriter::new(file), SCHEMA_V3, run)?;
95        Ok(Self {
96            path,
97            writer,
98            phase: config.phase,
99            step: 0,
100            active: HashMap::new(),
101            event_counter: 0,
102        })
103    }
104
105    pub fn phase(&self) -> ExecutionPhase {
106        self.phase
107    }
108
109    pub fn step(&self) -> u64 {
110        self.step
111    }
112
113    pub fn set_step(&mut self, step: u64) {
114        self.step = step;
115    }
116
117    /// Begin timing an operation; returns an `event_id` for [`Self::end_operation`].
118    pub fn begin_operation(
119        &mut self,
120        static_id: impl Into<String>,
121        op: impl Into<String>,
122        inputs: &[String],
123    ) -> Result<String> {
124        self.event_counter += 1;
125        let event_id = format!("{}-op-{}-{}", self.phase.as_str(), self.step, self.event_counter);
126        self.active.insert(
127            event_id.clone(),
128            ActiveOp {
129                static_id: static_id.into(),
130                op: op.into(),
131                inputs: inputs.to_vec(),
132                started: Instant::now(),
133            },
134        );
135        Ok(event_id)
136    }
137
138    /// End a timed operation and emit a runtime v3 operation observation.
139    pub fn end_operation(&mut self, event_id: &str, output: Option<String>) -> Result<u64> {
140        let active = self
141            .active
142            .remove(event_id)
143            .with_context(|| format!("unknown profile operation `{event_id}`"))?;
144        let duration_ns = active.started.elapsed().as_nanos().min(u64::MAX as u128) as u64;
145        self.writer.operation(OperationObservation {
146            event_id: event_id.to_string(),
147            op: active.op,
148            static_id: Some(active.static_id),
149            source: None,
150            inputs: active.inputs,
151            output,
152            step: Some(self.step),
153            duration_ns: Some(duration_ns),
154        })?;
155        Ok(duration_ns)
156    }
157
158    /// Record average-friendly edge timing between two static tensor/operation ids.
159    pub fn record_edge_timing(
160        &mut self,
161        from_static_id: impl Into<String>,
162        to_static_id: impl Into<String>,
163        duration_ns: u64,
164    ) -> Result<()> {
165        self.event_counter += 1;
166        let event_id = format!(
167            "{}-edge-{}-{}",
168            self.phase.as_str(),
169            self.step,
170            self.event_counter
171        );
172        self.writer.edge_timing(EdgeTimingObservation {
173            event_id,
174            from_static_id: from_static_id.into(),
175            to_static_id: to_static_id.into(),
176            duration_ns,
177            step: Some(self.step),
178        })
179    }
180
181    pub fn flush(&mut self) -> Result<()> {
182        self.writer.flush()
183    }
184
185    pub fn finish(self) -> Result<PathBuf> {
186        self.writer.finish()?;
187        Ok(self.path)
188    }
189}
190
191/// Convenience RAII guard for [`ProfileSession::begin_operation`] / [`ProfileSession::end_operation`].
192pub struct TimedOperation<'a> {
193    session: &'a mut ProfileSession,
194    event_id: String,
195}
196
197impl<'a> TimedOperation<'a> {
198    pub fn begin(
199        session: &'a mut ProfileSession,
200        static_id: impl Into<String>,
201        op: impl Into<String>,
202        inputs: &[String],
203    ) -> Result<Self> {
204        let event_id = session.begin_operation(static_id, op, inputs)?;
205        Ok(Self { session, event_id })
206    }
207}
208
209impl Drop for TimedOperation<'_> {
210    fn drop(&mut self) {
211        let _ = self.session.end_operation(&self.event_id, None);
212    }
213}