use std::sync::Mutex;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Instant;
use pagesift_infer::backend::Backend;
use pagesift_infer::{Activation, Tensor, ops};
use crate::exec::Ctx;
use crate::ops::ConvShape;
use crate::pool::{bytes, capacity_class};
use crate::worker::SharedWorker;
const CAL_GEMM: usize = 512;
const CAL_TRANSFER: usize = 1 << 21;
const CAL_CONV_C: usize = 64;
const CAL_CONV_HW: usize = 256;
const CAL_CONV_K: usize = 3;
const HASH_MUL: u64 = 0xff51_afd7_ed55_8ccd;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Costs {
pub latency: f64,
pub up: f64,
pub down: f64,
pub gpu_gemm: f64,
pub gpu_direct: f64,
pub gpu_col: f64,
pub cpu_gemm: f64,
pub cpu_conv: f64,
pub cpu_act: f64,
pub gpu_act: f64,
}
impl Costs {
pub fn gemm_on_gpu(&self, (m, n, k): (usize, usize, usize), cached_b: bool) -> bool {
let mads = (m * n * k) as f64;
let up = bytes(m * k + if cached_b { 0 } else { k * n }) as f64;
let gpu = self.latency + up * self.up + bytes(m * n) as f64 * self.down;
gpu + mads * self.gpu_gemm < mads * self.cpu_gemm
}
pub fn conv_on_gpu(&self, s: &ConvShape, cached_w: bool) -> bool {
let (oh, ow) = s.out_hw();
let cg = s.c / s.groups;
let mads = (s.o * oh * ow * cg * s.kh * s.kw) as f64;
let wlen = if cached_w { 0 } else { s.o * cg * s.kh * s.kw };
let up = bytes(s.c * s.h * s.w + wlen) as f64;
let run = if cg == 1 {
mads * self.gpu_direct
} else if s.needs_col() {
mads * self.gpu_gemm + s.col_len() as f64 * self.gpu_col
} else {
mads * self.gpu_gemm
};
let gpu = self.latency + up * self.up + bytes(s.o * oh * ow) as f64 * self.down;
gpu + run < mads * self.cpu_conv
}
pub fn act_on_gpu(&self, n: usize) -> bool {
let n_bytes = bytes(n) as f64;
let gpu = self.latency + n_bytes * (self.up + self.down) + n as f64 * self.gpu_act;
gpu < n as f64 * self.cpu_act
}
}
fn content_hash(data: &[f32]) -> u64 {
let mut h = data.len() as u64;
for v in data {
h = (h ^ u64::from(v.to_bits())).wrapping_mul(HASH_MUL);
h ^= h >> 29; }
h
}
pub struct GpuBackend {
worker: SharedWorker,
costs: Costs,
offloaded: AtomicU64,
last_error: Mutex<Option<String>>,
}
impl GpuBackend {
pub fn new(worker: SharedWorker) -> Result<GpuBackend, String> {
let costs = calibrate(&worker)?;
Ok(GpuBackend {
worker,
costs,
offloaded: AtomicU64::new(0),
last_error: Mutex::new(None),
})
}
pub fn costs(&self) -> Costs {
self.costs
}
pub fn offloaded(&self) -> u64 {
self.offloaded.load(Ordering::Relaxed)
}
pub fn last_error(&self) -> Option<String> {
self.last_error.lock().ok().and_then(|g| g.clone())
}
fn run<R: Send>(
&self,
f: impl for<'c, 'g> FnOnce(&'c Ctx<'g>) -> Result<R, String> + Send,
) -> Option<R> {
match self.worker.call(f).and_then(|r| r) {
Ok(r) => {
self.offloaded.fetch_add(1, Ordering::Relaxed);
Some(r)
}
Err(e) => {
if let Ok(mut g) = self.last_error.lock() {
*g = Some(e);
}
None
}
}
}
}
impl<'g> Ctx<'g> {
fn hashed_weight(&self, data: &[f32], hash: u64) -> Result<u64, String> {
if let Some(b) = self.hashed.borrow().get(&hash) {
return Ok(b.ptr());
}
let b = self.upload_or_trim(data)?;
let p = b.ptr();
self.count_weight(data.len());
self.hashed.borrow_mut().insert(hash, b);
Ok(p)
}
fn with_temp<R>(
&self,
len: usize,
f: impl FnOnce(&crate::DeviceBuffer<'g>) -> Result<R, String>,
) -> Result<R, String> {
let cap = capacity_class(len);
let b = self.pool.borrow_mut().take(self.gpu, cap)?;
let r = f(&b);
self.pool.borrow_mut().give(cap, b);
r
}
}
impl Backend for GpuBackend {
fn matmul(&self, a: &Tensor, b: &Tensor, b_is_weight: bool) -> Option<Vec<f32>> {
let (m, k) = a.tc();
let (_, n) = b.tc();
if !self.costs.gemm_on_gpu((m, n, k), b_is_weight) {
return None;
}
let hash = b_is_weight.then(|| content_hash(&b.data));
self.run(move |ctx: &Ctx<'_>| {
let run = |bp: u64| {
ctx.with_temp(m * k, |da| {
da.write_prefix(&a.data)?;
ctx.with_temp(m * n, |dc| {
ctx.gpu
.sgemm_raw(da.ptr(), bp, dc.ptr(), (m, n, k), 1, (0, 0, 0))?;
let mut out = vec![0f32; m * n];
dc.read_prefix(&mut out)?;
Ok(out)
})
})
};
match hash {
Some(h) => run(ctx.hashed_weight(&b.data, h)?),
None => ctx.with_temp(k * n, |db| {
db.write_prefix(&b.data)?;
run(db.ptr())
}),
}
})
}
fn conv2d(
&self,
x: &Tensor,
w: &Tensor,
bias: Option<&[f32]>,
stride: (usize, usize),
pad: (usize, usize),
dilation: (usize, usize),
groups: usize,
) -> Option<Vec<f32>> {
let (c, h, wd) = x.chw();
let s = ConvShape {
c,
h,
w: wd,
o: w.shape[0],
kh: w.shape[2],
kw: w.shape[3],
stride,
pad,
dilation,
groups,
};
if !self.costs.conv_on_gpu(&s, true) {
return None;
}
let (oh, ow) = s.out_hw();
let (hw, hb) = (content_hash(&w.data), bias.map(content_hash));
self.run(move |ctx: &Ctx<'_>| {
let wp = ctx.hashed_weight(&w.data, hw)?;
let bp = match (bias, hb) {
(Some(b), Some(h)) => Some(ctx.hashed_weight(b, h)?),
_ => None,
};
let col_len = if s.needs_col() { s.col_len() } else { 0 };
ctx.with_temp(x.data.len(), |dx| {
dx.write_prefix(&x.data)?;
ctx.with_temp(s.o * oh * ow, |dout| {
ctx.with_temp(col_len, |dcol| {
let col = s.needs_col().then(|| dcol.ptr());
ctx.gpu.conv_raw(dx.ptr(), wp, bp, &s, 1, dout.ptr(), col)?;
let mut out = vec![0f32; s.o * oh * ow];
dout.read_prefix(&mut out)?;
Ok(out)
})
})
})
})
}
fn activate(&self, x: &mut [f32], a: Activation) -> bool {
if !self.costs.act_on_gpu(x.len()) {
return false;
}
self.run(move |ctx: &Ctx<'_>| ctx.gpu.activate(x, a))
.is_some()
}
}
fn timed(mut f: impl FnMut() -> Result<(), String>) -> Result<f64, String> {
f()?;
let t = Instant::now();
f()?;
Ok(t.elapsed().as_secs_f64())
}
fn calibrate(worker: &SharedWorker) -> Result<Costs, String> {
let latency = worker.call(|ctx: &Ctx<'_>| -> Result<f64, String> {
let mut one = ctx.gpu.upload(&[0.5])?;
timed(|| {
ctx.gpu.activate_device(&mut one, Activation::Relu)?;
ctx.gpu.synchronize()
})
})??;
let host = vec![1f32; CAL_TRANSFER];
let n_bytes = bytes(CAL_TRANSFER) as f64;
let (t_up, t_down) = worker.call(|ctx: &Ctx<'_>| -> Result<(f64, f64), String> {
let d = ctx.gpu.alloc(CAL_TRANSFER)?;
d.write_prefix(&host)?;
let t = Instant::now();
d.write_prefix(&host)?;
let up = t.elapsed().as_secs_f64();
let mut back = vec![0f32; CAL_TRANSFER];
let t = Instant::now();
d.read_prefix(&mut back)?;
Ok((up, t.elapsed().as_secs_f64()))
})??;
let n = CAL_GEMM;
let a = Tensor::new(vec![n, n], vec![0.5; n * n]);
let mads = (n * n * n) as f64;
let cpu_gemm = timed(|| {
std::hint::black_box(ops::matmul(&a, &a));
Ok(())
})? / mads;
let gpu_gemm = worker.call(|ctx: &Ctx<'_>| -> Result<f64, String> {
let (da, mut dc) = (ctx.gpu.upload(&a.data)?, ctx.gpu.alloc(n * n)?);
timed(|| {
ctx.gpu.gemm_device(&da, &da, &mut dc, n, n, n)?;
ctx.gpu.synchronize()
})
})?? / mads;
let (c, hw, k) = (CAL_CONV_C, CAL_CONV_HW, CAL_CONV_K);
let x = Tensor::new(vec![c, hw, hw], vec![0.5; c * hw * hw]);
let weight = 0.1;
let wd = Tensor::new(vec![c, 1, k, k], vec![weight; c * k * k]);
let wf = Tensor::new(vec![c, c, k, k], vec![weight; c * c * k * k]);
let shape = |groups| ConvShape {
c,
h: hw,
w: hw,
o: c,
kh: k,
kw: k,
stride: (1, 1),
pad: (k / 2, k / 2),
dilation: (1, 1),
groups,
};
let mads_full = (c * hw * hw * c * k * k) as f64;
let mads_direct = (c * hw * hw * k * k) as f64;
let cpu_conv = timed(|| {
std::hint::black_box(ops::conv2d(
&x,
&wf,
None,
(1, 1),
(k / 2, k / 2),
(1, 1),
1,
));
Ok(())
})? / mads_full;
let (gpu_direct, t_full, col_len) = worker.call(|ctx: &Ctx<'_>| {
let run = |w: &Tensor, s: ConvShape| -> Result<f64, String> {
let (dx, dw) = (ctx.gpu.upload(&x.data)?, ctx.gpu.upload(&w.data)?);
timed(|| {
ctx.gpu.conv2d_device(&dx, &dw, None, &s).map(drop)?;
ctx.gpu.synchronize()
})
};
let (s1, s2) = (shape(c), shape(1));
Ok::<_, String>((run(&wd, s1)? / mads_direct, run(&wf, s2)?, s2.col_len()))
})??;
let gpu_col = ((t_full - mads_full * gpu_gemm) / col_len as f64).max(0.0);
let act_n = CAL_TRANSFER;
let mut buf = Tensor::new(vec![act_n], vec![0.5; act_n]);
let cpu_act = timed(|| {
Activation::Gelu.apply_all(&mut buf);
Ok(())
})? / act_n as f64;
let gpu_act = worker.call(|ctx: &Ctx<'_>| -> Result<f64, String> {
let mut d = ctx.gpu.upload(&buf.data)?;
timed(|| {
ctx.gpu.activate_device(&mut d, Activation::Gelu)?;
ctx.gpu.synchronize()
})
})?? / act_n as f64;
Ok(Costs {
latency,
up: t_up / n_bytes,
down: t_down / n_bytes,
gpu_gemm,
gpu_direct,
gpu_col,
cpu_gemm,
cpu_conv,
cpu_act,
gpu_act,
})
}