Skip to main content

pagesift_gpu/
backend.rs

1//! 透明分流:实现 `pagesift_infer::backend::Backend`,让直接调 `ops`、`nn` 的模型(公式、版面)
2//! 不改代码就能把大算子放到 GPU 上。
3//!
4//! 划不划算当场算:CPU 时间 = 乘加次数 / 实测 CPU 算力;GPU 时间 = 实测往返延迟 + 传输字节 / 实测带宽 +
5//! 乘加次数 / 实测 GPU 算力。门槛不是写死的数,换一张卡或一台机器自动变。权重传过一次就留在显存里,
6//! 按内容哈希认(地址可能被别的张量复用),所以长期不变的权重不算传输开销。
7
8use std::sync::Mutex;
9use std::sync::atomic::{AtomicU64, Ordering};
10use std::time::Instant;
11
12use pagesift_infer::backend::Backend;
13use pagesift_infer::{Activation, Tensor, ops};
14
15use crate::exec::Ctx;
16use crate::ops::ConvShape;
17use crate::pool::{bytes, capacity_class};
18use crate::worker::SharedWorker;
19
20/// [策略] 校准用的矩阵边长:512³ ≈ 1.3 亿次乘加,两边都是毫秒级,足够盖过计时噪声又不拖慢启动
21const CAL_GEMM: usize = 512;
22/// [策略] 校准传输用的元素数:2²¹ 个 f32 = 8 MiB,和卷积特征图同一量级
23const CAL_TRANSFER: usize = 1 << 21;
24/// [策略] 校准卷积的通道数和边长:和检测骨干的中段特征同一量级
25const CAL_CONV_C: usize = 64;
26/// [策略] 见 `CAL_CONV_C`
27const CAL_CONV_HW: usize = 256;
28/// [推导] 校准卷积的核边长:骨干里最常见的 3×3
29const CAL_CONV_K: usize = 3;
30/// [协议] MurmurHash3 fmix64 的乘数,用作内容哈希的混合常数
31const HASH_MUL: u64 = 0xff51_afd7_ed55_8ccd;
32
33/// 实测的开销,单位都是秒。
34#[derive(Debug, Clone, Copy, PartialEq)]
35pub struct Costs {
36    /// 一次空的往返(交给工人、启动、等完成)。
37    pub latency: f64,
38    /// 每字节上传、下载。
39    pub up: f64,
40    pub down: f64,
41    /// GPU 上每次乘加:SGEMM、深度卷积的直接内核;im2col 每个元素。
42    pub gpu_gemm: f64,
43    pub gpu_direct: f64,
44    pub gpu_col: f64,
45    /// CPU 上每次乘加:矩阵乘、卷积。
46    pub cpu_gemm: f64,
47    pub cpu_conv: f64,
48    /// 激活函数每个元素:CPU、GPU(以 GELU 为代表,它最贵)。
49    pub cpu_act: f64,
50    pub gpu_act: f64,
51}
52
53impl Costs {
54    /// 一次矩阵乘走 GPU 是否更快。`cached_b` 表示 `b` 已在显存。
55    pub fn gemm_on_gpu(&self, (m, n, k): (usize, usize, usize), cached_b: bool) -> bool {
56        let mads = (m * n * k) as f64;
57        let up = bytes(m * k + if cached_b { 0 } else { k * n }) as f64;
58        let gpu = self.latency + up * self.up + bytes(m * n) as f64 * self.down;
59        gpu + mads * self.gpu_gemm < mads * self.cpu_gemm
60    }
61
62    /// 一次卷积走 GPU 是否更快。`cached_w` 表示权重和偏置已在显存。
63    pub fn conv_on_gpu(&self, s: &ConvShape, cached_w: bool) -> bool {
64        let (oh, ow) = s.out_hw();
65        let cg = s.c / s.groups;
66        let mads = (s.o * oh * ow * cg * s.kh * s.kw) as f64;
67        let wlen = if cached_w { 0 } else { s.o * cg * s.kh * s.kw };
68        let up = bytes(s.c * s.h * s.w + wlen) as f64;
69        let run = if cg == 1 {
70            mads * self.gpu_direct
71        } else if s.needs_col() {
72            mads * self.gpu_gemm + s.col_len() as f64 * self.gpu_col
73        } else {
74            mads * self.gpu_gemm
75        };
76        let gpu = self.latency + up * self.up + bytes(s.o * oh * ow) as f64 * self.down;
77        gpu + run < mads * self.cpu_conv
78    }
79
80    /// 对 `n` 个元素做激活走 GPU 是否更快。
81    pub fn act_on_gpu(&self, n: usize) -> bool {
82        let n_bytes = bytes(n) as f64;
83        let gpu = self.latency + n_bytes * (self.up + self.down) + n as f64 * self.gpu_act;
84        gpu < n as f64 * self.cpu_act
85    }
86}
87
88/// 内容哈希,只用于认出“同一份权重”。
89fn content_hash(data: &[f32]) -> u64 {
90    let mut h = data.len() as u64;
91    for v in data {
92        h = (h ^ u64::from(v.to_bits())).wrapping_mul(HASH_MUL);
93        h ^= h >> 29; // [协议] fmix64 的移位量(高位折回低位,避免只有高位变化)
94    }
95    h
96}
97
98/// 透明分流的后端。
99pub struct GpuBackend {
100    worker: SharedWorker,
101    costs: Costs,
102    offloaded: AtomicU64,
103    last_error: Mutex<Option<String>>,
104}
105
106impl GpuBackend {
107    /// 校准开销(几十毫秒)并建好后端。
108    pub fn new(worker: SharedWorker) -> Result<GpuBackend, String> {
109        let costs = calibrate(&worker)?;
110        Ok(GpuBackend {
111            worker,
112            costs,
113            offloaded: AtomicU64::new(0),
114            last_error: Mutex::new(None),
115        })
116    }
117
118    /// 校准出来的开销。
119    pub fn costs(&self) -> Costs {
120        self.costs
121    }
122
123    /// 走了 GPU 的算子个数。
124    pub fn offloaded(&self) -> u64 {
125        self.offloaded.load(Ordering::Relaxed)
126    }
127
128    /// 最近一次 GPU 出错的原因(出错的算子已自动在 CPU 上重算)。
129    pub fn last_error(&self) -> Option<String> {
130        self.last_error.lock().ok().and_then(|g| g.clone())
131    }
132
133    /// 执行一个算子;出错时记下原因并返回 None,调用方退回 CPU。
134    fn run<R: Send>(
135        &self,
136        f: impl for<'c, 'g> FnOnce(&'c Ctx<'g>) -> Result<R, String> + Send,
137    ) -> Option<R> {
138        match self.worker.call(f).and_then(|r| r) {
139            Ok(r) => {
140                self.offloaded.fetch_add(1, Ordering::Relaxed);
141                Some(r)
142            }
143            Err(e) => {
144                if let Ok(mut g) = self.last_error.lock() {
145                    *g = Some(e);
146                }
147                None
148            }
149        }
150    }
151}
152
153impl<'g> Ctx<'g> {
154    /// 按内容哈希取权重的设备地址,不在显存就传上去。
155    fn hashed_weight(&self, data: &[f32], hash: u64) -> Result<u64, String> {
156        if let Some(b) = self.hashed.borrow().get(&hash) {
157            return Ok(b.ptr());
158        }
159        let b = self.upload_or_trim(data)?;
160        let p = b.ptr();
161        self.count_weight(data.len());
162        self.hashed.borrow_mut().insert(hash, b);
163        Ok(p)
164    }
165
166    /// 借一块临时缓冲交给 `f`,用完还回池。
167    fn with_temp<R>(
168        &self,
169        len: usize,
170        f: impl FnOnce(&crate::DeviceBuffer<'g>) -> Result<R, String>,
171    ) -> Result<R, String> {
172        let cap = capacity_class(len);
173        let b = self.pool.borrow_mut().take(self.gpu, cap)?;
174        let r = f(&b);
175        self.pool.borrow_mut().give(cap, b);
176        r
177    }
178}
179
180impl Backend for GpuBackend {
181    fn matmul(&self, a: &Tensor, b: &Tensor, b_is_weight: bool) -> Option<Vec<f32>> {
182        let (m, k) = a.tc();
183        let (_, n) = b.tc();
184        if !self.costs.gemm_on_gpu((m, n, k), b_is_weight) {
185            return None;
186        }
187        let hash = b_is_weight.then(|| content_hash(&b.data));
188        self.run(move |ctx: &Ctx<'_>| {
189            let run = |bp: u64| {
190                ctx.with_temp(m * k, |da| {
191                    da.write_prefix(&a.data)?;
192                    ctx.with_temp(m * n, |dc| {
193                        ctx.gpu
194                            .sgemm_raw(da.ptr(), bp, dc.ptr(), (m, n, k), 1, (0, 0, 0))?;
195                        let mut out = vec![0f32; m * n];
196                        dc.read_prefix(&mut out)?;
197                        Ok(out)
198                    })
199                })
200            };
201            match hash {
202                Some(h) => run(ctx.hashed_weight(&b.data, h)?),
203                None => ctx.with_temp(k * n, |db| {
204                    db.write_prefix(&b.data)?;
205                    run(db.ptr())
206                }),
207            }
208        })
209    }
210
211    fn conv2d(
212        &self,
213        x: &Tensor,
214        w: &Tensor,
215        bias: Option<&[f32]>,
216        stride: (usize, usize),
217        pad: (usize, usize),
218        dilation: (usize, usize),
219        groups: usize,
220    ) -> Option<Vec<f32>> {
221        let (c, h, wd) = x.chw();
222        let s = ConvShape {
223            c,
224            h,
225            w: wd,
226            o: w.shape[0],
227            kh: w.shape[2],
228            kw: w.shape[3],
229            stride,
230            pad,
231            dilation,
232            groups,
233        };
234        if !self.costs.conv_on_gpu(&s, true) {
235            return None;
236        }
237        let (oh, ow) = s.out_hw();
238        let (hw, hb) = (content_hash(&w.data), bias.map(content_hash));
239        self.run(move |ctx: &Ctx<'_>| {
240            let wp = ctx.hashed_weight(&w.data, hw)?;
241            let bp = match (bias, hb) {
242                (Some(b), Some(h)) => Some(ctx.hashed_weight(b, h)?),
243                _ => None,
244            };
245            let col_len = if s.needs_col() { s.col_len() } else { 0 };
246            ctx.with_temp(x.data.len(), |dx| {
247                dx.write_prefix(&x.data)?;
248                ctx.with_temp(s.o * oh * ow, |dout| {
249                    ctx.with_temp(col_len, |dcol| {
250                        let col = s.needs_col().then(|| dcol.ptr());
251                        ctx.gpu.conv_raw(dx.ptr(), wp, bp, &s, 1, dout.ptr(), col)?;
252                        let mut out = vec![0f32; s.o * oh * ow];
253                        dout.read_prefix(&mut out)?;
254                        Ok(out)
255                    })
256                })
257            })
258        })
259    }
260
261    fn activate(&self, x: &mut [f32], a: Activation) -> bool {
262        if !self.costs.act_on_gpu(x.len()) {
263            return false;
264        }
265        self.run(move |ctx: &Ctx<'_>| ctx.gpu.activate(x, a))
266            .is_some()
267    }
268}
269
270/// 秒表:跑两次取第二次,第一次含 JIT 后的首次启动和缓存预热。
271fn timed(mut f: impl FnMut() -> Result<(), String>) -> Result<f64, String> {
272    f()?;
273    let t = Instant::now();
274    f()?;
275    Ok(t.elapsed().as_secs_f64())
276}
277
278/// 量各项开销。时间再小也不让它变成 0,后面要做除数和比较。
279fn calibrate(worker: &SharedWorker) -> Result<Costs, String> {
280    // 一次最小的工作:交给工人、启动内核、等完成
281    let latency = worker.call(|ctx: &Ctx<'_>| -> Result<f64, String> {
282        let mut one = ctx.gpu.upload(&[0.5])?;
283        timed(|| {
284            ctx.gpu.activate_device(&mut one, Activation::Relu)?;
285            ctx.gpu.synchronize()
286        })
287    })??;
288
289    // 传输
290    let host = vec![1f32; CAL_TRANSFER];
291    let n_bytes = bytes(CAL_TRANSFER) as f64;
292    let (t_up, t_down) = worker.call(|ctx: &Ctx<'_>| -> Result<(f64, f64), String> {
293        let d = ctx.gpu.alloc(CAL_TRANSFER)?;
294        d.write_prefix(&host)?;
295        let t = Instant::now();
296        d.write_prefix(&host)?;
297        let up = t.elapsed().as_secs_f64();
298        let mut back = vec![0f32; CAL_TRANSFER];
299        let t = Instant::now();
300        d.read_prefix(&mut back)?;
301        Ok((up, t.elapsed().as_secs_f64()))
302    })??;
303
304    // 矩阵乘
305    let n = CAL_GEMM;
306    let a = Tensor::new(vec![n, n], vec![0.5; n * n]);
307    let mads = (n * n * n) as f64;
308    let cpu_gemm = timed(|| {
309        std::hint::black_box(ops::matmul(&a, &a));
310        Ok(())
311    })? / mads;
312    let gpu_gemm = worker.call(|ctx: &Ctx<'_>| -> Result<f64, String> {
313        let (da, mut dc) = (ctx.gpu.upload(&a.data)?, ctx.gpu.alloc(n * n)?);
314        timed(|| {
315            ctx.gpu.gemm_device(&da, &da, &mut dc, n, n, n)?;
316            ctx.gpu.synchronize()
317        })
318    })?? / mads;
319
320    // 卷积:深度卷积的直接内核,和普通卷积(im2col + SGEMM)里 im2col 的部分
321    let (c, hw, k) = (CAL_CONV_C, CAL_CONV_HW, CAL_CONV_K);
322    let x = Tensor::new(vec![c, hw, hw], vec![0.5; c * hw * hw]);
323    // [策略] 权重取什么值不影响耗时,取个不会溢出的小数
324    let weight = 0.1;
325    let wd = Tensor::new(vec![c, 1, k, k], vec![weight; c * k * k]);
326    let wf = Tensor::new(vec![c, c, k, k], vec![weight; c * c * k * k]);
327    let shape = |groups| ConvShape {
328        c,
329        h: hw,
330        w: hw,
331        o: c,
332        kh: k,
333        kw: k,
334        stride: (1, 1),
335        pad: (k / 2, k / 2),
336        dilation: (1, 1),
337        groups,
338    };
339    let mads_full = (c * hw * hw * c * k * k) as f64;
340    let mads_direct = (c * hw * hw * k * k) as f64;
341    let cpu_conv = timed(|| {
342        std::hint::black_box(ops::conv2d(
343            &x,
344            &wf,
345            None,
346            (1, 1),
347            (k / 2, k / 2),
348            (1, 1),
349            1,
350        ));
351        Ok(())
352    })? / mads_full;
353    let (gpu_direct, t_full, col_len) = worker.call(|ctx: &Ctx<'_>| {
354        let run = |w: &Tensor, s: ConvShape| -> Result<f64, String> {
355            let (dx, dw) = (ctx.gpu.upload(&x.data)?, ctx.gpu.upload(&w.data)?);
356            timed(|| {
357                ctx.gpu.conv2d_device(&dx, &dw, None, &s).map(drop)?;
358                ctx.gpu.synchronize()
359            })
360        };
361        let (s1, s2) = (shape(c), shape(1));
362        Ok::<_, String>((run(&wd, s1)? / mads_direct, run(&wf, s2)?, s2.col_len()))
363    })??;
364    let gpu_col = ((t_full - mads_full * gpu_gemm) / col_len as f64).max(0.0);
365
366    // 激活
367    let act_n = CAL_TRANSFER;
368    let mut buf = Tensor::new(vec![act_n], vec![0.5; act_n]);
369    let cpu_act = timed(|| {
370        Activation::Gelu.apply_all(&mut buf);
371        Ok(())
372    })? / act_n as f64;
373    let gpu_act = worker.call(|ctx: &Ctx<'_>| -> Result<f64, String> {
374        let mut d = ctx.gpu.upload(&buf.data)?;
375        timed(|| {
376            ctx.gpu.activate_device(&mut d, Activation::Gelu)?;
377            ctx.gpu.synchronize()
378        })
379    })?? / act_n as f64;
380
381    Ok(Costs {
382        latency,
383        up: t_up / n_bytes,
384        down: t_down / n_bytes,
385        gpu_gemm,
386        gpu_direct,
387        gpu_col,
388        cpu_gemm,
389        cpu_conv,
390        cpu_act,
391        gpu_act,
392    })
393}