tract_libcli/
profile_autobatch.rs1use std::sync::Arc;
8use std::time::{Duration, Instant};
9
10use tract_core::internal::*;
11use tract_core::num_traits::Zero;
12use tract_core::runtime::Runnable;
13
14use crate::profile::{BenchLimits, reusable_state};
15use crate::tensor::RunTensors;
16
17impl BenchLimits {
18 pub fn bench_streams(
26 &self,
27 runnable: &Arc<dyn Runnable>,
28 inputs: &RunTensors,
29 streams: usize,
30 ) -> TractResult<StreamsBench> {
31 ensure!(streams > 0, "A saturating bench needs a stream at least");
32 let reuse = reusable_state(runnable);
33 let max_loops = if self.max_loops.is_zero() { usize::MAX } else { self.max_loops };
34 let max_time = if self.max_time.is_zero() { Duration::MAX } else { self.max_time };
35 let start = Instant::now();
36 let turns: Vec<Vec<Duration>> = std::thread::scope(|scope| {
37 let running: Vec<_> = (0..streams)
38 .map(|_| {
39 scope.spawn(|| -> TractResult<Vec<Duration>> {
40 let mut state = runnable.spawn()?;
41 let mut turns = vec![];
42 while turns.len() < max_loops && start.elapsed() < max_time {
43 if !reuse {
44 state = runnable.spawn()?;
45 }
46 let turn = Instant::now();
47 state.run(inputs.sources[0].clone())?;
48 turns.push(turn.elapsed());
49 }
50 Ok(turns)
51 })
52 })
53 .collect();
54 running.into_iter().map(|stream| stream.join().unwrap()).collect::<Vec<_>>()
55 })
56 .into_iter()
57 .collect::<TractResult<_>>()?;
58 let wall = start.elapsed();
59 let mut latencies: Vec<Duration> = turns.into_iter().flatten().collect();
60 latencies.sort();
61 Ok(StreamsBench { streams, wall, latencies })
62 }
63
64 pub fn bench_streams_paced(
79 &self,
80 runnable: &Arc<dyn Runnable>,
81 inputs: &RunTensors,
82 streams: usize,
83 pacing: &Pacing,
84 ) -> TractResult<PacedBench> {
85 use rand::SeedableRng;
86 ensure!(streams > 0, "A paced bench needs a stream at least");
87 ensure!(!pacing.period.is_zero(), "A paced bench needs a turn period");
88 let max_loops = if self.max_loops.is_zero() { usize::MAX } else { self.max_loops };
89 let max_time = if self.max_time.is_zero() { Duration::MAX } else { self.max_time };
90 ensure!(
91 max_time >= 4 * pacing.period,
92 "A turn period of {:?} wants four periods of run at least, --max-time gives {max_time:?}",
93 pacing.period
94 );
95 let reuse = reusable_state(runnable);
96 ensure!(
97 pacing.churn.is_none() || reuse,
98 "Churn ends a session and admits another in its place, which only means something for a model carrying a session across turns"
99 );
100 let start = Instant::now();
101 let served: Vec<StreamTurns> = std::thread::scope(|scope| {
102 let running: Vec<_> = (0..streams)
103 .map(|stream| {
104 scope.spawn(move || -> TractResult<StreamTurns> {
105 let mut rng = rand::rngs::StdRng::seed_from_u64(stream as u64);
106 let phase = pacing.period.mul_f64(stream as f64 / streams as f64);
107 let mut turns = StreamTurns::default();
108 let mut grid: Option<Instant> = None;
116 let mut slot = 0usize;
117 while turns.service.len() < max_loops && start.elapsed() < max_time {
118 let (mut state, starved, took) = admit(runnable, &start, max_time)?;
119 turns.admissions += 1;
120 turns.starved += usize::from(starved);
121 turns.admitting.push(took);
122 state.run(inputs.sources[0].clone())?;
126 let admitted = Instant::now();
127 let grid = *grid.get_or_insert(admitted + phase);
128 if let Some(late) = admitted.checked_duration_since(grid) {
134 let missed = late.div_duration_f64(pacing.period).ceil();
135 slot = slot.max(missed as usize);
136 }
137 let hold = pacing
138 .churn
139 .map(|churn| exponential(&mut rng, churn.hold).max(pacing.period));
140 while turns.service.len() < max_loops && start.elapsed() < max_time {
141 if hold.is_some_and(|hold| admitted.elapsed() >= hold) {
142 break;
143 }
144 if !reuse {
145 state = runnable.spawn()?;
146 }
147 let due = grid + pacing.period * slot as u32;
148 slot += 1;
149 if let Some(wait) = due.checked_duration_since(Instant::now()) {
150 std::thread::sleep(wait);
151 }
152 let turn = Instant::now();
153 state.run(inputs.sources[0].clone())?;
154 let done = Instant::now();
155 turns.service.push(done - turn);
156 turns.added.push(done.saturating_duration_since(due));
157 turns.span = done - grid;
158 }
159 }
160 Ok(turns)
161 })
162 })
163 .collect();
164 running.into_iter().map(|stream| stream.join().unwrap()).collect::<Vec<_>>()
165 })
166 .into_iter()
167 .collect::<TractResult<_>>()?;
168 let wall = served.iter().map(|turns| turns.span).max().unwrap_or_default();
169 let admissions = served.iter().map(|turns| turns.admissions).sum();
170 let starved = served.iter().map(|turns| turns.starved).sum();
171 let mut admitting: Vec<Duration> =
172 served.iter().flat_map(|turns| turns.admitting.iter().copied()).collect();
173 admitting.sort();
174 let mut service: Vec<Duration> =
175 served.iter().flat_map(|turns| turns.service.iter().copied()).collect();
176 let mut added: Vec<Duration> = served.into_iter().flat_map(|turns| turns.added).collect();
177 service.sort();
178 added.sort();
179 Ok(PacedBench {
180 streams,
181 deadline: pacing.deadline,
182 wall,
183 service,
184 added,
185 admissions,
186 starved,
187 admitting,
188 })
189 }
190}
191
192fn admit(
198 runnable: &Arc<dyn Runnable>,
199 start: &Instant,
200 max_time: Duration,
201) -> TractResult<(Box<dyn State>, bool, Duration)> {
202 let mut starved = false;
203 let asked = Instant::now();
204 loop {
205 match runnable.spawn() {
206 Ok(state) => return Ok((state, starved, asked.elapsed())),
207 Err(e) => {
208 if start.elapsed() >= max_time {
209 return Err(e).context("Admitting a session");
210 }
211 starved = true;
212 std::thread::sleep(Duration::from_micros(200));
213 }
214 }
215 }
216}
217
218fn exponential(rng: &mut impl rand::RngExt, mean: Duration) -> Duration {
220 let uniform: f64 = rng.random::<f64>();
221 mean.mul_f64(-(1. - uniform).max(f64::MIN_POSITIVE).ln())
222}
223
224#[derive(Clone, Copy, Debug)]
227pub struct Pacing {
228 pub period: Duration,
229 pub deadline: Duration,
230 pub churn: Option<Churn>,
231}
232
233#[derive(Clone, Copy, Debug)]
236pub struct Churn {
237 pub hold: Duration,
238}
239
240#[derive(Default)]
242struct StreamTurns {
243 service: Vec<Duration>,
244 added: Vec<Duration>,
245 span: Duration,
246 admissions: usize,
247 starved: usize,
248 admitting: Vec<Duration>,
249}
250
251pub struct PacedBench {
256 pub streams: usize,
257 pub deadline: Duration,
258 pub wall: Duration,
259 pub service: Vec<Duration>,
261 pub added: Vec<Duration>,
263 pub admissions: usize,
266 pub starved: usize,
268 pub admitting: Vec<Duration>,
275}
276
277impl PacedBench {
278 pub fn admissions_per_second(&self) -> f64 {
280 self.admissions as f64 / self.wall.as_secs_f64()
281 }
282
283 pub fn admission_quantile(&self, q: f64) -> Duration {
285 quantile(&self.admitting, q)
286 }
287
288 pub fn starve_fraction(&self) -> f64 {
290 if self.admissions == 0 {
291 return 0.;
292 }
293 self.starved as f64 / self.admissions as f64
294 }
295
296 pub fn turns(&self) -> usize {
297 self.service.len()
298 }
299
300 pub fn turns_per_second(&self) -> f64 {
301 self.turns() as f64 / self.wall.as_secs_f64()
302 }
303
304 pub fn mean_service(&self) -> Duration {
305 self.service.iter().sum::<Duration>().checked_div(self.turns() as u32).unwrap_or_default()
306 }
307
308 pub fn added_quantile(&self, q: f64) -> Duration {
310 quantile(&self.added, q)
311 }
312
313 pub fn late_fraction(&self) -> f64 {
315 if self.added.is_empty() {
316 return 0.;
317 }
318 let in_time = self.added.partition_point(|added| *added <= self.deadline);
319 (self.turns() - in_time) as f64 / self.turns() as f64
320 }
321
322 pub fn holds(&self, q: f64) -> bool {
325 !self.added.is_empty() && self.added_quantile(q) <= self.deadline
326 }
327}
328
329fn quantile(sorted: &[Duration], q: f64) -> Duration {
331 if sorted.is_empty() {
332 return Duration::default();
333 }
334 sorted[((sorted.len() - 1) as f64 * q).round() as usize]
335}
336
337pub struct StreamsBench {
340 pub streams: usize,
341 pub wall: Duration,
342 pub latencies: Vec<Duration>,
344}
345
346impl StreamsBench {
347 pub fn turns(&self) -> usize {
348 self.latencies.len()
349 }
350
351 pub fn turns_per_second(&self) -> f64 {
354 self.turns() as f64 / self.wall.as_secs_f64()
355 }
356
357 pub fn mean_latency(&self) -> Duration {
358 self.latencies.iter().sum::<Duration>().checked_div(self.turns() as u32).unwrap_or_default()
359 }
360
361 pub fn latency_quantile(&self, q: f64) -> Duration {
363 quantile(&self.latencies, q)
364 }
365}