1use 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
20const CAL_GEMM: usize = 512;
22const CAL_TRANSFER: usize = 1 << 21;
24const CAL_CONV_C: usize = 64;
26const CAL_CONV_HW: usize = 256;
28const CAL_CONV_K: usize = 3;
30const HASH_MUL: u64 = 0xff51_afd7_ed55_8ccd;
32
33#[derive(Debug, Clone, Copy, PartialEq)]
35pub struct Costs {
36 pub latency: f64,
38 pub up: f64,
40 pub down: f64,
41 pub gpu_gemm: f64,
43 pub gpu_direct: f64,
44 pub gpu_col: f64,
45 pub cpu_gemm: f64,
47 pub cpu_conv: f64,
48 pub cpu_act: f64,
50 pub gpu_act: f64,
51}
52
53impl Costs {
54 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 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 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
88fn 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; }
95 h
96}
97
98pub struct GpuBackend {
100 worker: SharedWorker,
101 costs: Costs,
102 offloaded: AtomicU64,
103 last_error: Mutex<Option<String>>,
104}
105
106impl GpuBackend {
107 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 pub fn costs(&self) -> Costs {
120 self.costs
121 }
122
123 pub fn offloaded(&self) -> u64 {
125 self.offloaded.load(Ordering::Relaxed)
126 }
127
128 pub fn last_error(&self) -> Option<String> {
130 self.last_error.lock().ok().and_then(|g| g.clone())
131 }
132
133 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 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 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
270fn 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
278fn calibrate(worker: &SharedWorker) -> Result<Costs, String> {
280 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 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 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 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 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 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}