mod backend;
mod buffer;
mod driver;
mod exec;
mod exec_ops;
mod ops;
mod pool;
mod worker;
use std::ffi::{CStr, c_char, c_int, c_void};
use std::ptr::null_mut;
pub use backend::{Costs, GpuBackend};
pub use buffer::{DeviceBuffer, Timer};
pub use exec::{BatchExec, Ctx, DevTensor, Owner};
pub use ops::ConvShape;
pub use pool::{OOM_TAG, is_oom};
pub use worker::{SharedWorker, WaitError, Worker};
use driver::{
ATTR_CC_MAJOR, ATTR_CC_MINOR, Driver, Handle, JIT_ERROR_LOG_BUFFER,
JIT_ERROR_LOG_BUFFER_SIZE_BYTES,
};
const PTX_SM70: &str = concat!(include_str!("../ptx/kernels_sm70.ptx"), "\0");
const PTX_SM61: &str = concat!(include_str!("../ptx/kernels_sm61.ptx"), "\0");
pub(crate) struct Kernels {
pub sgemm: Handle,
pub im2col: Handle,
pub depthwise: Handle,
pub add_bias: Handle,
pub relu: Handle,
pub silu: Handle,
pub gelu: Handle,
pub hardsigmoid: Handle,
pub sigmoid: Handle,
pub se_gate: Handle,
pub zero_cols: Handle,
pub pad_rb: Handle,
pub pool_max: Handle,
pub pool_avg: Handle,
pub global_avg: Handle,
pub scale_ch: Handle,
pub add2: Handle,
pub copy_part: Handle,
pub interp_nearest: Handle,
pub chw_to_seq: Handle,
pub conv_transpose: Handle,
pub layer_norm: Handle,
pub attn_split: Handle,
pub attn_softmax: Handle,
pub attn_merge: Handle,
pub ctc_digest: Handle,
pub pad_all: Handle,
pub crop_cols: Handle,
}
pub struct Gpu {
drv: Driver,
dev: c_int,
index: usize,
ctx: Handle,
module: Handle,
k: Kernels,
name: String,
cc: (i32, i32),
ptx: &'static Ptx,
}
unsafe impl Send for Gpu {}
unsafe impl Sync for Gpu {}
pub fn device_count() -> usize {
let Ok(drv) = Driver::load() else { return 0 };
let mut count = 0;
if unsafe { (drv.init)(0) } != 0 || unsafe { (drv.device_get_count)(&mut count) } != 0 {
return 0;
}
count.max(0) as usize
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GpuInfo {
pub index: usize,
pub name: String,
pub compute_capability: (i32, i32),
pub memory_total: u64,
pub memory_free: u64,
pub driver_version: Option<String>,
pub cuda_version: (i32, i32),
pub ptx: &'static str,
}
pub fn disabled_by_env() -> Option<String> {
let v = std::env::var("PAGESIFT_GPU").ok()?;
v.eq_ignore_ascii_case("off")
.then(|| "GPU disabled by PAGESIFT_GPU=off".to_owned())
}
struct Picked {
drv: Driver,
dev: c_int,
cc: (i32, i32),
ptx: &'static Ptx,
name: String,
}
fn pick(index: usize) -> Result<Picked, String> {
if let Some(why) = disabled_by_env() {
return Err(why);
}
let drv = Driver::load()?;
drv.check(unsafe { (drv.init)(0) }, "cuInit")?;
let mut count = 0;
drv.check(
unsafe { (drv.device_get_count)(&mut count) },
"cuDeviceGetCount",
)?;
if count == 0 {
return Err("no NVIDIA GPU found".into());
}
if index >= count as usize {
return Err(format!("no GPU {index}: this machine has {count}"));
}
let mut dev = 0;
drv.check(
unsafe { (drv.device_get)(&mut dev, index as c_int) },
"cuDeviceGet",
)?;
let attr = |a| -> Result<i32, String> {
let mut v = 0;
drv.check(
unsafe { (drv.device_get_attribute)(&mut v, a, dev) },
"cuDeviceGetAttribute",
)?;
Ok(v)
};
let cc = (attr(ATTR_CC_MAJOR)?, attr(ATTR_CC_MINOR)?);
let ptx = select_ptx(cc)?;
let name = device_name(&drv, dev);
Ok(Picked {
drv,
dev,
cc,
ptx,
name,
})
}
fn memory_info(drv: &Driver) -> Result<(u64, u64), String> {
let (mut free, mut total) = (0usize, 0usize);
drv.check(
unsafe { (drv.mem_get_info)(&mut free, &mut total) },
"cuMemGetInfo",
)?;
Ok((free as u64, total as u64))
}
fn build_info(
drv: &Driver,
index: usize,
name: &str,
cc: (i32, i32),
ptx: &Ptx,
) -> Result<GpuInfo, String> {
let (memory_free, memory_total) = memory_info(drv)?;
let mut v = 0;
drv.check(
unsafe { (drv.driver_get_version)(&mut v) },
"cuDriverGetVersion",
)?;
let cuda_version = (v / 1000, (v % 1000) / 10);
Ok(GpuInfo {
index,
name: name.to_owned(),
compute_capability: cc,
memory_total,
memory_free,
driver_version: driver::nvml_driver_version(),
cuda_version,
ptx: ptx.name,
})
}
pub fn probe(index: usize) -> Result<GpuInfo, String> {
let p = pick(index)?;
let mut ctx = null_mut();
p.drv.check(
unsafe { (p.drv.primary_ctx_retain)(&mut ctx, p.dev) },
"cuDevicePrimaryCtxRetain",
)?;
let r = p
.drv
.check(unsafe { (p.drv.ctx_set_current)(ctx) }, "cuCtxSetCurrent")
.and_then(|()| build_info(&p.drv, index, &p.name, p.cc, p.ptx));
unsafe { (p.drv.primary_ctx_release)(p.dev) };
r
}
impl Gpu {
pub fn open() -> Result<Gpu, String> {
Gpu::open_index(0)
}
pub fn open_index(index: usize) -> Result<Gpu, String> {
let Picked {
drv,
dev,
cc,
ptx,
name,
} = pick(index)?;
let mut ctx = null_mut();
drv.check(
unsafe { (drv.primary_ctx_retain)(&mut ctx, dev) },
"cuDevicePrimaryCtxRetain",
)?;
let mut gpu = Gpu {
drv,
dev,
index,
ctx,
module: null_mut(),
k: Kernels::null(),
name,
cc,
ptx,
};
gpu.bind()?;
gpu.module = load_module(&gpu.drv, ptx.source)?;
gpu.k = Kernels::get(&gpu.drv, gpu.module)?;
Ok(gpu)
}
pub fn name(&self) -> &str {
&self.name
}
pub fn compute_capability(&self) -> (i32, i32) {
self.cc
}
pub fn memory_free(&self) -> Result<u64, String> {
self.bind()?;
Ok(memory_info(&self.drv)?.0)
}
pub fn info(&self) -> Result<GpuInfo, String> {
self.bind()?;
build_info(&self.drv, self.index, &self.name, self.cc, self.ptx)
}
pub fn synchronize(&self) -> Result<(), String> {
self.bind()?;
self.drv
.check(unsafe { (self.drv.ctx_synchronize)() }, "cuCtxSynchronize")
}
pub(crate) fn bind(&self) -> Result<(), String> {
self.drv.check(
unsafe { (self.drv.ctx_set_current)(self.ctx) },
"cuCtxSetCurrent",
)
}
}
impl Drop for Gpu {
fn drop(&mut self) {
let _ = self.bind();
if !self.module.is_null() {
unsafe { (self.drv.module_unload)(self.module) };
}
unsafe { (self.drv.primary_ctx_release)(self.dev) };
}
}
struct Ptx {
name: &'static str,
source: &'static str,
}
static SM70: Ptx = Ptx {
name: "sm_70",
source: PTX_SM70,
};
static SM61: Ptx = Ptx {
name: "sm_61",
source: PTX_SM61,
};
fn select_ptx(cc: (i32, i32)) -> Result<&'static Ptx, String> {
match cc {
(major, _) if major >= 7 => Ok(&SM70),
(6, minor) if minor >= 1 => Ok(&SM61),
(major, minor) => Err(format!(
"GPU compute capability {major}.{minor} is too old, need at least 6.1 (Pascal)"
)),
}
}
fn device_name(drv: &Driver, dev: c_int) -> String {
let mut buf = [0 as c_char; 256]; let r = unsafe { (drv.device_get_name)(buf.as_mut_ptr(), buf.len() as c_int, dev) };
if r != 0 {
return "unknown NVIDIA GPU".into();
}
unsafe { CStr::from_ptr(buf.as_ptr()) }
.to_string_lossy()
.into_owned()
}
fn load_module(drv: &Driver, ptx: &str) -> Result<Handle, String> {
let mut log = vec![0u8; 16 * 1024]; let mut opts = [JIT_ERROR_LOG_BUFFER, JIT_ERROR_LOG_BUFFER_SIZE_BYTES];
let mut vals = [log.as_mut_ptr() as *mut c_void, log.len() as *mut c_void];
let mut module = null_mut();
let r = unsafe {
(drv.module_load_data_ex)(
&mut module,
ptx.as_ptr() as *const c_void,
opts.len() as u32,
opts.as_mut_ptr(),
vals.as_mut_ptr(),
)
};
if let Err(e) = drv.check(r, "cuModuleLoadDataEx") {
let text = CStr::from_bytes_until_nul(&log)
.map(|c| c.to_string_lossy().into_owned())
.unwrap_or_default();
return Err(format!("{e}; JIT log: {}", text.trim()));
}
Ok(module)
}
impl Kernels {
fn null() -> Kernels {
let n = null_mut();
Kernels {
sgemm: n,
im2col: n,
depthwise: n,
add_bias: n,
relu: n,
silu: n,
gelu: n,
hardsigmoid: n,
sigmoid: n,
se_gate: n,
zero_cols: n,
pad_rb: n,
pool_max: n,
pool_avg: n,
global_avg: n,
scale_ch: n,
add2: n,
copy_part: n,
interp_nearest: n,
chw_to_seq: n,
conv_transpose: n,
layer_norm: n,
attn_split: n,
attn_softmax: n,
attn_merge: n,
ctc_digest: n,
pad_all: n,
crop_cols: n,
}
}
fn get(drv: &Driver, module: Handle) -> Result<Kernels, String> {
let f = |name: &CStr| -> Result<Handle, String> {
let mut h = null_mut();
let what = format!("cuModuleGetFunction({})", name.to_string_lossy());
drv.check(
unsafe { (drv.module_get_function)(&mut h, module, name.as_ptr()) },
&what,
)?;
Ok(h)
};
Ok(Kernels {
sgemm: f(c"sgemm")?,
im2col: f(c"im2col")?,
depthwise: f(c"depthwise")?,
add_bias: f(c"add_bias")?,
relu: f(c"relu")?,
silu: f(c"silu")?,
gelu: f(c"gelu")?,
hardsigmoid: f(c"hardsigmoid")?,
sigmoid: f(c"sigmoid")?,
se_gate: f(c"se_gate")?,
zero_cols: f(c"zero_cols")?,
pad_rb: f(c"pad_rb")?,
pool_max: f(c"pool_max")?,
pool_avg: f(c"pool_avg")?,
global_avg: f(c"global_avg")?,
scale_ch: f(c"scale_ch")?,
add2: f(c"add2")?,
copy_part: f(c"copy_part")?,
interp_nearest: f(c"interp_nearest")?,
chw_to_seq: f(c"chw_to_seq")?,
conv_transpose: f(c"conv_transpose")?,
layer_norm: f(c"layer_norm")?,
attn_split: f(c"attn_split")?,
attn_softmax: f(c"attn_softmax")?,
attn_merge: f(c"attn_merge")?,
ctc_digest: f(c"ctc_digest")?,
pad_all: f(c"pad_all")?,
crop_cols: f(c"crop_cols")?,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bad_ptx_is_an_error() {
let Ok(g) = Gpu::open() else { return };
let e = load_module(&g.drv, "this is not ptx\0").err().unwrap();
assert!(e.contains("cuModuleLoadDataEx"), "{e}");
}
#[test]
fn ptx_by_compute_capability() {
let src = |cc| select_ptx(cc).map(|p| p.source);
assert!(src((8, 6)).unwrap().contains("\n.target sm_70\n"));
assert!(src((7, 0)).unwrap().contains("\n.target sm_70\n"));
assert!(src((6, 1)).unwrap().contains("\n.target sm_61\n"));
assert!(src((6, 2)).unwrap().contains("\n.target sm_61\n"));
assert_eq!(select_ptx((6, 1)).unwrap().name, "sm_61");
assert_eq!(select_ptx((8, 6)).unwrap().name, "sm_70");
assert!(src((6, 0)).is_err());
assert!(src((5, 2)).is_err());
assert!(
PTX_SM61.is_ascii() && PTX_SM70.is_ascii(),
"ptxas 只收 ASCII"
);
assert!(PTX_SM61.ends_with('\0') && PTX_SM70.ends_with('\0'));
}
}