1use std::sync::atomic::{AtomicBool, Ordering};
11use std::sync::mpsc::{RecvTimeoutError, Sender, channel};
12use std::sync::{Arc, Condvar, Mutex, MutexGuard};
13use std::thread::JoinHandle;
14use std::time::{Duration, Instant};
15
16use pagesift_gpu::{Ctx, Owner, SharedWorker, WaitError, Worker, is_oom};
17use pagesift_infer::Tensor;
18use pagesift_infer::exec::Exec;
19use pagesift_stat::decide::shadow_price;
20use pagesift_stat::regress::RegressionSums;
21
22use crate::det::Det;
23use crate::rec::Rec;
24
25const EWMA: f64 = 0.1;
27
28const GPU_STALL: Duration = Duration::from_secs(300);
30
31pub(crate) enum GpuWait {
33 Failed(String),
35 Stalled,
37 Deadline,
39}
40
41fn wait_limit(deadline: Option<Instant>) -> Duration {
43 deadline.map_or(GPU_STALL, |d| {
44 GPU_STALL.min(d.saturating_duration_since(Instant::now()))
45 })
46}
47
48fn timed_out(deadline: Option<Instant>) -> GpuWait {
49 if deadline.is_some_and(|d| Instant::now() >= d) {
50 GpuWait::Deadline
51 } else {
52 GpuWait::Stalled
53 }
54}
55
56pub(crate) type Digest = Vec<(u32, f32)>;
58
59#[derive(Debug, Clone, Copy, Default, PartialEq)]
61pub struct GpuStats {
62 pub peak_bytes: u64,
64 pub needed_bytes: u64,
66 pub batches: u64,
68 pub lines: u64,
69 pub splits: u64,
71 pub offloaded_ops: u64,
73}
74
75struct Item {
77 x: Tensor,
78 idx: usize,
79 reply: Sender<(usize, Result<Digest, String>)>,
80}
81
82impl Item {
83 fn width(&self) -> usize {
84 self.x.shape[2]
85 }
86}
87
88struct State {
89 pending: Vec<Item>,
90 stop: bool,
91 pages: usize,
93 pages_waiting: usize,
94 arrival_rate: f64,
96 last_arrival: Option<Instant>,
97 stats: GpuStats,
98}
99
100struct Shared {
101 state: Mutex<State>,
102 wake: Condvar,
103}
104
105impl Shared {
106 fn lock(&self) -> MutexGuard<'_, State> {
107 self.state.lock().unwrap_or_else(|e| e.into_inner())
109 }
110}
111
112pub(crate) fn should_wait(
121 n: usize,
122 n_max: usize,
123 producers: usize,
124 arrival_rate: f64,
125 utilization: f64,
126 overhead: f64,
127) -> Option<Duration> {
128 if n >= n_max || producers == 0 || arrival_rate <= 0.0 || overhead <= 0.0 {
129 return None;
130 }
131 let gap = 1.0 / arrival_rate;
132 let hold_cost = n as f64 * gap;
133 let saving = shadow_price(utilization, overhead / (n + 1) as f64);
134 (hold_cost < saving).then(|| Duration::from_secs_f64(gap))
135}
136
137pub(crate) fn batch_len(widths_desc: &[usize], n_max: usize, model: Option<(f64, f64)>) -> usize {
140 let limit = widths_desc.len().min(n_max).max(1);
141 let Some((overhead, per_col)) = model else {
142 return limit;
143 };
144 let wmax = widths_desc[0];
145 let mut waste = 0.0;
146 for (i, &w) in widths_desc.iter().enumerate().take(limit).skip(1) {
147 waste += (wmax - w) as f64 * per_col;
148 if waste > overhead {
149 return i;
150 }
151 }
152 limit
153}
154
155struct BatchModel {
157 sums: RegressionSums,
158}
159
160impl BatchModel {
161 fn new() -> BatchModel {
162 BatchModel {
163 sums: RegressionSums::new(2),
164 }
165 }
166
167 fn observe(&mut self, area: usize, seconds: f64) {
168 self.sums.push(&[1.0, area as f64], seconds);
169 }
170
171 fn params(&self) -> Option<(f64, f64)> {
173 let f = self.sums.fit()?;
174 let (a, t) = (f.beta[0], f.beta[1]);
175 (a > 0.0 && t > 0.0).then_some((a, t))
176 }
177}
178
179pub(crate) struct GpuRunner {
181 worker: SharedWorker,
182 det: Arc<Det>,
183 shared: Arc<Shared>,
184 thread: Option<JoinHandle<()>>,
185 stalled: AtomicBool,
187}
188
189pub(crate) struct PageGuard<'a>(&'a Shared);
191
192impl Drop for PageGuard<'_> {
193 fn drop(&mut self) {
194 self.0.lock().pages -= 1;
195 self.0.wake.notify_all();
196 }
197}
198
199impl GpuRunner {
200 pub fn start(index: usize, det: Arc<Det>, rec: Arc<Rec>) -> Result<GpuRunner, String> {
202 let worker = Arc::new(Worker::spawn(index)?);
203 let shared = Arc::new(Shared {
204 state: Mutex::new(State {
205 pending: Vec::new(),
206 stop: false,
207 pages: 0,
208 pages_waiting: 0,
209 arrival_rate: 0.0,
210 last_arrival: None,
211 stats: GpuStats::default(),
212 }),
213 wake: Condvar::new(),
214 });
215 let thread = {
216 let (worker, shared) = (worker.clone(), shared.clone());
217 std::thread::Builder::new()
218 .name("pagesift-ocr-batch".into())
219 .spawn(move || scheduler(&worker, &shared, &rec))
220 .map_err(|e| format!("failed to start the OCR batch thread: {e}"))?
221 };
222 Ok(GpuRunner {
223 worker,
224 det,
225 shared,
226 thread: Some(thread),
227 stalled: AtomicBool::new(false),
228 })
229 }
230
231 pub fn worker(&self) -> &SharedWorker {
232 &self.worker
233 }
234
235 pub fn page(&self) -> PageGuard<'_> {
236 self.shared.lock().pages += 1;
237 PageGuard(&self.shared)
238 }
239
240 pub fn stats(&self) -> GpuStats {
241 let mut s = self.shared.lock().stats;
242 (s.peak_bytes, s.needed_bytes) = self
244 .worker
245 .call(|ctx: &Ctx<'_>| (ctx.peak_bytes(), ctx.needed_bytes()))
246 .unwrap_or((0, 0));
247 s
248 }
249
250 pub fn prob_map(
252 &self,
253 x: Tensor,
254 deadline: Option<Instant>,
255 ) -> Result<(Vec<f32>, usize, usize), GpuWait> {
256 let det = self.det.clone();
257 let done = self.worker.call_timeout(
258 move |ctx: &Ctx<'_>| -> Result<(Vec<f32>, usize, usize), String> {
259 let owner: Owner = det.clone();
260 let e = ctx.exec(1, owner);
261 let dx = e.upload(&x);
262 let prob = det.prob_on(&e, &dx)?;
263 let shape = e.shape(&prob);
264 let data = e.download(&prob)?;
265 Ok((data, shape[2], shape[1]))
266 },
267 wait_limit(deadline),
268 );
269 match done {
270 Ok(r) => r.map_err(GpuWait::Failed),
271 Err(WaitError::Failed(e)) => Err(GpuWait::Failed(e)),
272 Err(WaitError::TimedOut) => Err(self.on_timeout(deadline)),
273 }
274 }
275
276 fn on_timeout(&self, deadline: Option<Instant>) -> GpuWait {
277 let why = timed_out(deadline);
278 if matches!(why, GpuWait::Stalled) {
279 self.stalled.store(true, Ordering::Relaxed);
280 }
281 why
282 }
283
284 pub fn recognize_many(
287 &self,
288 xs: Vec<Tensor>,
289 deadline: Option<Instant>,
290 ) -> Result<Vec<Result<Digest, String>>, GpuWait> {
291 let n = xs.len();
292 let (tx, rx) = channel();
293 {
294 let mut st = self.shared.lock();
295 let now = Instant::now();
296 if let Some(last) = st.last_arrival {
297 let dt = now.duration_since(last).as_secs_f64();
298 if dt > 0.0 {
299 st.arrival_rate = ewma(st.arrival_rate, n as f64 / dt);
300 }
301 }
302 st.last_arrival = Some(now);
303 st.pages_waiting += 1;
304 st.pending
305 .extend(xs.into_iter().enumerate().map(|(idx, x)| Item {
306 x,
307 idx,
308 reply: tx.clone(),
309 }));
310 }
311 drop(tx);
312 self.shared.wake.notify_all();
313 let mut out: Vec<Result<Digest, String>> = (0..n)
314 .map(|_| Err("the OCR batch thread exited".to_owned()))
315 .collect();
316 let mut gave_up = None;
317 loop {
318 match rx.recv_timeout(wait_limit(deadline)) {
319 Ok((i, r)) => out[i] = r,
320 Err(RecvTimeoutError::Disconnected) => break,
321 Err(RecvTimeoutError::Timeout) => {
322 gave_up = Some(self.on_timeout(deadline));
323 break;
324 }
325 }
326 }
327 self.shared.lock().pages_waiting -= 1;
328 self.shared.wake.notify_all();
329 match gave_up {
330 Some(why) => Err(why),
331 None => Ok(out),
332 }
333 }
334}
335
336impl Drop for GpuRunner {
337 fn drop(&mut self) {
338 self.shared.lock().stop = true;
339 self.shared.wake.notify_all();
340 if self.stalled.load(Ordering::Relaxed) {
342 std::mem::forget(self.worker.clone());
343 self.thread.take();
344 return;
345 }
346 if let Some(t) = self.thread.take() {
347 let _ = t.join();
349 }
350 }
351}
352
353fn ewma(old: f64, new: f64) -> f64 {
354 if old == 0.0 {
355 new
356 } else {
357 old + EWMA * (new - old)
358 }
359}
360
361fn plan(worker: &Worker, rec: &Arc<Rec>, w: usize) -> Result<usize, String> {
363 worker.call(|ctx: &Ctx<'_>| {
364 let owner: Owner = rec.clone();
365 let e = ctx.dry(1, owner);
366 let x = e.placeholder(vec![rec.input_channels(), rec.input_height(), w]);
367 let logits = rec.logits_on(&e, &x);
368 e.ctc_digest(&logits);
369 let (work, weights) = e.dry_usage();
370 let avail = ctx.available_bytes()?.saturating_sub(weights);
371 Ok(((avail / work.max(1)) as usize).max(1))
372 })?
373}
374
375fn run_batch(worker: &Worker, rec: &Arc<Rec>, items: &[Item]) -> Result<Vec<Digest>, String> {
377 let xs: Vec<&Tensor> = items.iter().map(|i| &i.x).collect();
378 worker.call(|ctx: &Ctx<'_>| {
379 let owner: Owner = rec.clone();
380 let e = ctx.exec(xs.len(), owner);
381 let x = e.upload_batch(&xs);
382 let logits = rec.logits_on(&e, &x);
383 let rows = e.ctc_digest(&logits);
384 e.finish()?;
385 Ok(rows)
386 })?
387}
388
389fn run_and_reply(
391 worker: &Worker,
392 rec: &Arc<Rec>,
393 shared: &Shared,
394 mut items: Vec<Item>,
395) -> Option<f64> {
396 let t = Instant::now();
397 match run_batch(worker, rec, &items) {
398 Ok(rows) => {
399 let secs = t.elapsed().as_secs_f64();
400 for (item, r) in items.iter().zip(rows) {
401 let _ = item.reply.send((item.idx, Ok(r)));
403 }
404 Some(secs)
405 }
406 Err(e) if is_oom(&e) && items.len() > 1 => {
407 shared.lock().stats.splits += 1;
408 let tail = items.split_off(items.len() / 2);
409 run_and_reply(worker, rec, shared, items);
410 run_and_reply(worker, rec, shared, tail);
411 None
412 }
413 Err(e) => {
414 for item in &items {
415 let _ = item.reply.send((item.idx, Err(e.clone())));
416 }
417 None
418 }
419 }
420}
421
422fn scheduler(worker: &Worker, shared: &Shared, rec: &Arc<Rec>) {
424 let mut model = BatchModel::new();
425 let started = Instant::now();
426 let mut busy = 0f64;
427 loop {
428 let mut st = shared.lock();
429 while st.pending.is_empty() {
430 if st.stop {
431 return;
432 }
433 st = shared.wake.wait(st).unwrap_or_else(|e| e.into_inner());
434 }
435 st.pending.sort_by_key(|i| std::cmp::Reverse(i.width()));
436 let wmax = st.pending[0].width();
437 drop(st);
438 let n_max = plan(worker, rec, wmax).unwrap_or(1);
440 let mut st = shared.lock();
441 let params = model.params();
442 let producers = st.pages.saturating_sub(st.pages_waiting);
443 let utilization = (busy / started.elapsed().as_secs_f64().max(f64::MIN_POSITIVE)).min(1.0);
444 if !st.stop
445 && let Some(dt) = should_wait(
446 st.pending.len(),
447 n_max,
448 producers,
449 st.arrival_rate,
450 utilization,
451 params.map_or(0.0, |p| p.0),
452 )
453 {
454 drop(
456 shared
457 .wake
458 .wait_timeout(st, dt)
459 .unwrap_or_else(|e| e.into_inner()),
460 );
461 continue;
462 }
463 st.pending.sort_by_key(|i| std::cmp::Reverse(i.width()));
464 let widths: Vec<usize> = st.pending.iter().map(Item::width).collect();
465 let n = batch_len(&widths, n_max, params);
466 let batch: Vec<Item> = st.pending.drain(..n).collect();
467 st.stats.batches += 1;
468 st.stats.lines += n as u64;
469 drop(st);
470 let area = n * batch[0].width();
471 let t = Instant::now();
472 let secs = run_and_reply(worker, rec, shared, batch);
473 busy += t.elapsed().as_secs_f64();
474 if let Some(s) = secs {
475 model.observe(area, s);
476 }
477 }
478}
479
480#[cfg(test)]
481mod tests {
482 use super::*;
483
484 #[test]
485 fn 没有生产者时不等() {
486 assert_eq!(should_wait(3, 100, 0, 1e6, 0.5, 1.0), None);
487 }
488
489 #[test]
490 fn 批满了不等() {
491 assert_eq!(should_wait(100, 100, 2, 1e6, 0.5, 1.0), None);
492 }
493
494 #[test]
495 fn 到达快且开销大时等_到达慢时不等() {
496 assert!(should_wait(1, 100, 1, 1e3, 0.0, 0.01).is_some());
498 assert!(should_wait(1, 100, 1, 1.0, 0.0, 0.01).is_none());
500 assert!(should_wait(1, 100, 1, 50.0, 0.0, 0.01).is_none());
502 assert!(should_wait(1, 100, 1, 50.0, 0.9, 0.01).is_some());
503 }
504
505 #[test]
506 fn 按浪费切批() {
507 assert_eq!(batch_len(&[320, 320, 320], 2, None), 2);
509 assert_eq!(batch_len(&[3200, 320], 8, None), 2);
510 assert_eq!(batch_len(&[320, 320, 320], 8, Some((1e-3, 1e-6))), 3);
512 assert_eq!(batch_len(&[3200, 320], 8, Some((1e-3, 1e-6))), 1);
514 assert_eq!(
516 batch_len(&[800, 400, 400, 400, 400], 8, Some((1e-3, 1e-6))),
517 3
518 );
519 }
520
521 #[test]
522 fn 回归拟合出固定开销和每列时间() {
523 let mut m = BatchModel::new();
524 assert_eq!(m.params(), None);
525 for area in [320, 640, 3200, 6400] {
526 m.observe(area, 0.01 + 1e-6 * area as f64);
527 }
528 let (a, t) = m.params().unwrap();
529 assert!((a - 0.01).abs() < 1e-6, "{a}");
530 assert!((t - 1e-6).abs() < 1e-9, "{t}");
531 }
532}