Skip to main content

pagesift_ocr/
gpu_run.rs

1//! GPU 上跑检测和识别:页面线程把裁好的行图交到队列,调度线程拼批后交给 GPU 工人。
2//!
3//! 三个决定都不写死数字:
4//! - **批多大**:一个样本的显存峰值由试算([`pagesift_gpu::Ctx::dry`],走真实的前向代码)得出,
5//!   批大小 = 可用显存 / 单样本峰值;估多了分配会失败,那时对半切开重来。
6//! - **哪些行拼一批**:宽度差得多就浪费算力,分开又多一次固定开销。批耗时按 `a + t·N·W` 在线回归,
7//!   某行补边浪费的时间累计超过 `a` 就另起一批。
8//! - **等不等下一批**:见 [`should_wait`],用排队论的影子价格算。
9
10use 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
25/// [策略] 利用率和到达率的指数平滑系数:大约记住最近 10 个批次
26const EWMA: f64 = 0.1;
27
28/// [策略] 这么久等不到 GPU 的任何回复就当它卡死了:一批至多几秒,满载排队时也有几十倍余量
29const GPU_STALL: Duration = Duration::from_secs(300);
30
31/// 等 GPU 没等到结果的原因。
32pub(crate) enum GpuWait {
33    /// GPU 报了错。
34    Failed(String),
35    /// GPU 卡死了([`GPU_STALL`])。
36    Stalled,
37    /// 请求的截止时间到了;不是 GPU 的问题,GPU 上别的页还在正常跑。
38    Deadline,
39}
40
41/// 这次最多等多久,不超过请求的截止时间。
42fn 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
56/// CTC 摘要:每步的(最大类,其概率)。
57pub(crate) type Digest = Vec<(u32, f32)>;
58
59/// 显存和批次的统计(给评测和报告用)。
60#[derive(Debug, Clone, Copy, Default, PartialEq)]
61pub struct GpuStats {
62    /// 占用显存的峰值(缓冲池 + 权重,字节)。
63    pub peak_bytes: u64,
64    /// 真正需要的显存峰值(在用的张量 + 权重,字节),比 `peak_bytes` 少池里闲着的部分。
65    pub needed_bytes: u64,
66    /// 跑过的识别批次数和行数,行数 / 批次数就是平均批大小。
67    pub batches: u64,
68    pub lines: u64,
69    /// 显存不够、对半切开重来的次数。
70    pub splits: u64,
71    /// 透明分流(公式、版面模型)走了 GPU 的算子个数。
72    pub offloaded_ops: u64,
73}
74
75/// 队列里的一行。
76struct 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    /// 正在 [`Engine::run`](crate::pipeline::Engine::run) 里的页数,和其中已交完行、在等结果的页数。
92    pages: usize,
93    pages_waiting: usize,
94    /// 到达率(行/秒)的平滑值;还没有估计时为 0。
95    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        // 持锁时不做可能 panic 的工作;真被毒化了状态也还一致
108        self.state.lock().unwrap_or_else(|e| e.into_inner())
109    }
110}
111
112/// 现在开跑还是再等一个到达。
113///
114/// 开跑一批要付一次固定开销 `a`(秒)。再等一行并进来,这行就不用单独分摊那份开销:平均省
115/// `a/(n+1)` 的 GPU 时间;省下的 GPU 时间按影子价格 `W/(1−ρ)` 折成所有排队者少等的时间
116/// (`pagesift_stat::decide::shadow_price`,M/M/1 的逗留时间)。代价是已在队列里的 `n` 行每行多等
117/// 一个到达间隔 `1/λ`。收益大于代价才等。
118///
119/// `producers` 是还会交新行的页数:都在等结果时等也等不来,立刻开跑。
120pub(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
137/// 从队列里挑一批:从最宽的开始往窄走,补边浪费的时间累计超过一次固定开销就停,最多 `n_max` 行。
138/// `model` 还没拟合好时(返回 None)就只受 `n_max` 限制。
139pub(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
155/// 批耗时模型 `a + t·(N·W)` 的在线回归。
156struct 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    /// (固定开销 a, 每列时间 t);数据不够、回归不合理(病态或出负值)时为 None。
172    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
179/// 把 GPU 工人、模型和调度线程捆在一起。
180pub(crate) struct GpuRunner {
181    worker: SharedWorker,
182    det: Arc<Det>,
183    shared: Arc<Shared>,
184    thread: Option<JoinHandle<()>>,
185    /// GPU 卡死过:丢掉时不能再等工人线程退出。
186    stalled: AtomicBool,
187}
188
189/// 页面在 `run` 里时持有。
190pub(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    /// 启动工人(打开设备、JIT 内核)和调度线程。打不开时返回原因。
201    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        // 工人线程查不到就保留 0
243        (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    /// 检测网络:`[3, H, W]` → 概率图 (数据, 宽, 高)。输入交给工人:等不到结果时调用方可以走开。
251    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    /// 识别网络:一组 `[3, 48, W]`,返回各行的摘要,顺序同输入。出错的行各自带原因。
285    /// 迟迟等不到回复时放弃整页(还在队列里的行照常算完,结果没人要)。
286    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        // 卡死的 GPU 上线程永远等不回来:不等,也不让工人句柄在这里等(泄漏到进程结束)
341        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            // 线程 panic 也不再往外传:Drop 里没有地方报
348            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
361/// 宽度为 `w` 的一行能拼多大的批:可用显存 / 单行的工作内存(试算得出)。
362fn 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
375/// 真跑一批。
376fn 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
389/// 跑一批并回复;显存不够就对半切开。返回用掉的时间,切开过时不计入模型(返回 None)。
390fn 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                // 回复通道断开说明那一页已经不要了
402                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
422/// 调度线程。
423fn 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        // 试算出错(工人线程没了)时按 1 行一批走下去,真跑时会把错误报给调用方
439        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            // 超时或有新行都回到循环开头重新判断
455            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        // 1 行在队列、每毫秒来一行、固定开销 10 ms:多等 1 ms,省 5 ms
497        assert!(should_wait(1, 100, 1, 1e3, 0.0, 0.01).is_some());
498        // 每秒才来一行:多等 1 s,只省 5 ms
499        assert!(should_wait(1, 100, 1, 1.0, 0.0, 0.01).is_none());
500        // 忙的时候省下的 GPU 时间更值钱,同样的到达率也值得等
501        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        // 没有模型时只受上限限制
508        assert_eq!(batch_len(&[320, 320, 320], 2, None), 2);
509        assert_eq!(batch_len(&[3200, 320], 8, None), 2);
510        // 相同宽度没有浪费
511        assert_eq!(batch_len(&[320, 320, 320], 8, Some((1e-3, 1e-6))), 3);
512        // 宽差 2880 列 × 1 µs ≈ 2.9 ms,超过 1 ms 的固定开销,分开
513        assert_eq!(batch_len(&[3200, 320], 8, Some((1e-3, 1e-6))), 1);
514        // 累计浪费:每行差 400 列 0.4 ms,第 3 行起超过 1 ms
515        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}