Skip to main content

pagesift_gpu/
lib.rs

1//! NVIDIA GPU 推理后端:给 `pagesift-infer` 的重算子(矩阵乘、卷积、激活)提供同语义的 GPU 版本。
2//!
3//! - 不依赖 CUDA 工具链:运行时 `dlopen("libcuda.so.1")` 调驱动 API(见 `driver.rs`);
4//! - 内核是仓库里 `kernels/` 用 nightly 的 nvptx 目标编出的 PTX,`include_str!` 嵌进来,由驱动 JIT。
5//!   有两份:sm_70 原版给算力 ≥ 7.0 的卡,`.target` 改成 sm_61 的一份给 Pascal(6.1、6.2);
6//! - 没有驱动、没有 N 卡、算力不够时 [`Gpu::open`] 返回错误,调用方退回 CPU。
7//!
8//! 结果与 `pagesift_infer::ops` 的 CPU 实现只差浮点舍入(累加顺序不同、GPU 用 FMA)。
9//!
10//! ```no_run
11//! let gpu = pagesift_gpu::Gpu::open()?;
12//! let a = pagesift_infer::Tensor::new(vec![2, 3], vec![1.0; 6]);
13//! let b = pagesift_infer::Tensor::new(vec![3, 4], vec![1.0; 12]);
14//! let c = gpu.matmul(&a, &b)?; // [2, 4],行优先
15//! assert_eq!(c, vec![3.0; 8]);
16//! # Ok::<(), String>(())
17//! ```
18
19mod backend;
20mod buffer;
21mod driver;
22mod exec;
23mod exec_ops;
24mod ops;
25mod pool;
26mod worker;
27
28use std::ffi::{CStr, c_char, c_int, c_void};
29use std::ptr::null_mut;
30
31pub use backend::{Costs, GpuBackend};
32pub use buffer::{DeviceBuffer, Timer};
33pub use exec::{BatchExec, Ctx, DevTensor, Owner};
34pub use ops::ConvShape;
35pub use pool::{OOM_TAG, is_oom};
36pub use worker::{SharedWorker, WaitError, Worker};
37
38use driver::{
39    ATTR_CC_MAJOR, ATTR_CC_MINOR, Driver, Handle, JIT_ERROR_LOG_BUFFER,
40    JIT_ERROR_LOG_BUFFER_SIZE_BYTES,
41};
42
43/// sm_70 原版 PTX(算力 ≥ 7.0)。末尾补 NUL:`cuModuleLoadData` 要 C 字符串。
44const PTX_SM70: &str = concat!(include_str!("../ptx/kernels_sm70.ptx"), "\0");
45/// `.target` 改成 sm_61 的 PTX(算力 6.1、6.2)。
46const PTX_SM61: &str = concat!(include_str!("../ptx/kernels_sm61.ptx"), "\0");
47
48/// 内核函数句柄,名字与 `kernels/src` 里的 `#[no_mangle]` 函数一一对应。
49pub(crate) struct Kernels {
50    pub sgemm: Handle,
51    pub im2col: Handle,
52    pub depthwise: Handle,
53    pub add_bias: Handle,
54    pub relu: Handle,
55    pub silu: Handle,
56    pub gelu: Handle,
57    pub hardsigmoid: Handle,
58    pub sigmoid: Handle,
59    pub se_gate: Handle,
60    pub zero_cols: Handle,
61    pub pad_rb: Handle,
62    pub pool_max: Handle,
63    pub pool_avg: Handle,
64    pub global_avg: Handle,
65    pub scale_ch: Handle,
66    pub add2: Handle,
67    pub copy_part: Handle,
68    pub interp_nearest: Handle,
69    pub chw_to_seq: Handle,
70    pub conv_transpose: Handle,
71    pub layer_norm: Handle,
72    pub attn_split: Handle,
73    pub attn_softmax: Handle,
74    pub attn_merge: Handle,
75    pub ctc_digest: Handle,
76    pub pad_all: Handle,
77    pub crop_cols: Handle,
78}
79
80/// 一块 N 卡上的上下文、载好的内核模块和函数表。所有方法取 `&self`,可以跨线程共享。
81pub struct Gpu {
82    drv: Driver,
83    dev: c_int,
84    index: usize,
85    ctx: Handle,
86    module: Handle,
87    k: Kernels,
88    name: String,
89    cc: (i32, i32),
90    ptx: &'static Ptx,
91}
92
93// SAFETY: 驱动 API 是线程安全的;上下文是设备的主上下文,每个入口先在当前线程设为当前上下文
94// (`bind`),句柄本身只是驱动内部对象的不透明指针,不在 Rust 侧读写。
95unsafe impl Send for Gpu {}
96unsafe impl Sync for Gpu {}
97
98/// 本机可见的 N 卡数量;没有驱动或驱动初始化失败时为 0。用来区分“没有 GPU”(该退回 CPU)和
99/// “有 GPU 但 [`Gpu::open`] 失败”(PTX、驱动出了问题,应当报出来)。
100pub fn device_count() -> usize {
101    let Ok(drv) = Driver::load() else { return 0 };
102    let mut count = 0;
103    if unsafe { (drv.init)(0) } != 0 || unsafe { (drv.device_get_count)(&mut count) } != 0 {
104        return 0;
105    }
106    count.max(0) as usize
107}
108
109/// 一块 N 卡的情况(给 doctor 和模型选择用)。
110#[derive(Debug, Clone, PartialEq, Eq)]
111pub struct GpuInfo {
112    /// 设备序号(`cuda:N` 的 N)。
113    pub index: usize,
114    /// 设备名,例如 “NVIDIA GeForce GTX 1070”。
115    pub name: String,
116    /// 算力 (主, 次),例如 (6, 1)。
117    pub compute_capability: (i32, i32),
118    /// 显存总量(字节)。
119    pub memory_total: u64,
120    /// 查询时的空闲显存(字节)。
121    pub memory_free: u64,
122    /// 真实驱动版本(来自 NVML,例如 “550.54.14”);没有 NVML 时为 None。
123    pub driver_version: Option<String>,
124    /// 驱动支持的 CUDA API 版本 (主, 次),例如 (12, 4)。
125    pub cuda_version: (i32, i32),
126    /// 选中的 PTX 版本:“sm_61” 或 “sm_70”。
127    pub ptx: &'static str,
128}
129
130/// `PAGESIFT_GPU=off` 时返回原因字符串:这时 [`Gpu::open`]、[`Gpu::open_index`]、[`probe`] 都报错,
131/// 调用方照常退回 CPU。
132pub fn disabled_by_env() -> Option<String> {
133    let v = std::env::var("PAGESIFT_GPU").ok()?;
134    v.eq_ignore_ascii_case("off")
135        .then(|| "GPU disabled by PAGESIFT_GPU=off".to_owned())
136}
137
138/// 驱动里挑出的一个设备:已 `cuInit`,序号也核对过。
139struct Picked {
140    drv: Driver,
141    dev: c_int,
142    cc: (i32, i32),
143    ptx: &'static Ptx,
144    name: String,
145}
146
147/// 载入驱动并取出第 `index` 块卡;不碰上下文。
148fn pick(index: usize) -> Result<Picked, String> {
149    if let Some(why) = disabled_by_env() {
150        return Err(why);
151    }
152    let drv = Driver::load()?;
153    drv.check(unsafe { (drv.init)(0) }, "cuInit")?;
154    let mut count = 0;
155    drv.check(
156        unsafe { (drv.device_get_count)(&mut count) },
157        "cuDeviceGetCount",
158    )?;
159    if count == 0 {
160        return Err("no NVIDIA GPU found".into());
161    }
162    if index >= count as usize {
163        return Err(format!("no GPU {index}: this machine has {count}"));
164    }
165    let mut dev = 0;
166    drv.check(
167        unsafe { (drv.device_get)(&mut dev, index as c_int) },
168        "cuDeviceGet",
169    )?;
170    let attr = |a| -> Result<i32, String> {
171        let mut v = 0;
172        drv.check(
173            unsafe { (drv.device_get_attribute)(&mut v, a, dev) },
174            "cuDeviceGetAttribute",
175        )?;
176        Ok(v)
177    };
178    let cc = (attr(ATTR_CC_MAJOR)?, attr(ATTR_CC_MINOR)?);
179    let ptx = select_ptx(cc)?;
180    let name = device_name(&drv, dev);
181    Ok(Picked {
182        drv,
183        dev,
184        cc,
185        ptx,
186        name,
187    })
188}
189
190/// 取当前上下文所在设备的显存 (空闲, 总量),字节。
191fn memory_info(drv: &Driver) -> Result<(u64, u64), String> {
192    let (mut free, mut total) = (0usize, 0usize);
193    drv.check(
194        unsafe { (drv.mem_get_info)(&mut free, &mut total) },
195        "cuMemGetInfo",
196    )?;
197    Ok((free as u64, total as u64))
198}
199
200/// 汇总当前上下文所在设备的情况。
201fn build_info(
202    drv: &Driver,
203    index: usize,
204    name: &str,
205    cc: (i32, i32),
206    ptx: &Ptx,
207) -> Result<GpuInfo, String> {
208    let (memory_free, memory_total) = memory_info(drv)?;
209    let mut v = 0;
210    drv.check(
211        unsafe { (drv.driver_get_version)(&mut v) },
212        "cuDriverGetVersion",
213    )?;
214    // [协议] cuDriverGetVersion 的编码:主版本 × 1000 + 次版本 × 10
215    let cuda_version = (v / 1000, (v % 1000) / 10);
216    Ok(GpuInfo {
217        index,
218        name: name.to_owned(),
219        compute_capability: cc,
220        memory_total,
221        memory_free,
222        driver_version: driver::nvml_driver_version(),
223        cuda_version,
224        ptx: ptx.name,
225    })
226}
227
228/// 查第 `index` 块 N 卡的情况,不载内核(不 JIT,比 [`Gpu::open_index`] 轻)。
229/// 没有驱动、没有这块卡、算力不够或 `PAGESIFT_GPU=off` 时,错误就是可以给用户看的原因。
230pub fn probe(index: usize) -> Result<GpuInfo, String> {
231    let p = pick(index)?;
232    let mut ctx = null_mut();
233    p.drv.check(
234        unsafe { (p.drv.primary_ctx_retain)(&mut ctx, p.dev) },
235        "cuDevicePrimaryCtxRetain",
236    )?;
237    let r = p
238        .drv
239        .check(unsafe { (p.drv.ctx_set_current)(ctx) }, "cuCtxSetCurrent")
240        .and_then(|()| build_info(&p.drv, index, &p.name, p.cc, p.ptx));
241    unsafe { (p.drv.primary_ctx_release)(p.dev) };
242    r
243}
244
245impl Gpu {
246    /// 打开 0 号 N 卡,同 [`Gpu::open_index`]`(0)`。
247    pub fn open() -> Result<Gpu, String> {
248        Gpu::open_index(0)
249    }
250
251    /// 打开第 `index` 块 N 卡:载入驱动、取主上下文、按算力挑 PTX 并 JIT。
252    /// 没有驱动、没有这块卡、算力低于 6.1、JIT 失败或 `PAGESIFT_GPU=off` 时返回错误(不 panic)。
253    pub fn open_index(index: usize) -> Result<Gpu, String> {
254        let Picked {
255            drv,
256            dev,
257            cc,
258            ptx,
259            name,
260        } = pick(index)?;
261        let mut ctx = null_mut();
262        drv.check(
263            unsafe { (drv.primary_ctx_retain)(&mut ctx, dev) },
264            "cuDevicePrimaryCtxRetain",
265        )?;
266        // 之后出错要释放主上下文:先拼出不带模块的半成品,交给 Drop 收尾
267        let mut gpu = Gpu {
268            drv,
269            dev,
270            index,
271            ctx,
272            module: null_mut(),
273            k: Kernels::null(),
274            name,
275            cc,
276            ptx,
277        };
278        gpu.bind()?;
279        gpu.module = load_module(&gpu.drv, ptx.source)?;
280        gpu.k = Kernels::get(&gpu.drv, gpu.module)?;
281        Ok(gpu)
282    }
283
284    /// 设备名,例如 “NVIDIA GeForce GTX 1070”。
285    pub fn name(&self) -> &str {
286        &self.name
287    }
288
289    /// 算力 (主, 次),例如 (6, 1)。
290    pub fn compute_capability(&self) -> (i32, i32) {
291        self.cc
292    }
293
294    /// 现在的空闲显存(字节)。
295    pub fn memory_free(&self) -> Result<u64, String> {
296        self.bind()?;
297        Ok(memory_info(&self.drv)?.0)
298    }
299
300    /// 型号、算力、显存总量和空闲量、驱动版本、选中的 PTX 版本。
301    pub fn info(&self) -> Result<GpuInfo, String> {
302        self.bind()?;
303        build_info(&self.drv, self.index, &self.name, self.cc, self.ptx)
304    }
305
306    /// 等设备上已提交的工作全部完成。
307    pub fn synchronize(&self) -> Result<(), String> {
308        self.bind()?;
309        self.drv
310            .check(unsafe { (self.drv.ctx_synchronize)() }, "cuCtxSynchronize")
311    }
312
313    /// 把本设备的主上下文设为当前线程的当前上下文。每个公开入口先调它。
314    pub(crate) fn bind(&self) -> Result<(), String> {
315        self.drv.check(
316            unsafe { (self.drv.ctx_set_current)(self.ctx) },
317            "cuCtxSetCurrent",
318        )
319    }
320}
321
322impl Drop for Gpu {
323    fn drop(&mut self) {
324        // 出错也只能忽略:Drop 里没有地方报
325        let _ = self.bind();
326        if !self.module.is_null() {
327            unsafe { (self.drv.module_unload)(self.module) };
328        }
329        unsafe { (self.drv.primary_ctx_release)(self.dev) };
330    }
331}
332
333/// 一份嵌入的 PTX。
334struct Ptx {
335    name: &'static str,
336    source: &'static str,
337}
338
339static SM70: Ptx = Ptx {
340    name: "sm_70",
341    source: PTX_SM70,
342};
343static SM61: Ptx = Ptx {
344    name: "sm_61",
345    source: PTX_SM61,
346};
347
348/// 按算力挑 PTX:≥ 7.0 用 sm_70 原版;6.1、6.2 用改过文件头的 sm_61 版;更老的不支持
349/// (.target sm_61 的 PTX 在 6.0 的卡上 JIT 会失败)。
350fn select_ptx(cc: (i32, i32)) -> Result<&'static Ptx, String> {
351    match cc {
352        // [协议] PTX ISA:sm_70 是 Volta 的算力 7.0;sm_61 覆盖算力 6.1 与 6.2
353        (major, _) if major >= 7 => Ok(&SM70),
354        (6, minor) if minor >= 1 => Ok(&SM61),
355        (major, minor) => Err(format!(
356            "GPU compute capability {major}.{minor} is too old, need at least 6.1 (Pascal)"
357        )),
358    }
359}
360
361fn device_name(drv: &Driver, dev: c_int) -> String {
362    let mut buf = [0 as c_char; 256]; // [策略] 设备名缓冲,驱动给的名字远短于此
363    let r = unsafe { (drv.device_get_name)(buf.as_mut_ptr(), buf.len() as c_int, dev) };
364    if r != 0 {
365        return "unknown NVIDIA GPU".into();
366    }
367    unsafe { CStr::from_ptr(buf.as_ptr()) }
368        .to_string_lossy()
369        .into_owned()
370}
371
372/// JIT 载入 PTX;失败时把驱动的 JIT 错误日志带进错误信息。
373fn load_module(drv: &Driver, ptx: &str) -> Result<Handle, String> {
374    let mut log = vec![0u8; 16 * 1024]; // [策略] JIT 错误日志缓冲,16 KiB 足够容下几十行报错
375    let mut opts = [JIT_ERROR_LOG_BUFFER, JIT_ERROR_LOG_BUFFER_SIZE_BYTES];
376    // 驱动约定:选项值按指针宽度传,缓冲大小也塞在指针里
377    let mut vals = [log.as_mut_ptr() as *mut c_void, log.len() as *mut c_void];
378    let mut module = null_mut();
379    let r = unsafe {
380        (drv.module_load_data_ex)(
381            &mut module,
382            ptx.as_ptr() as *const c_void,
383            opts.len() as u32,
384            opts.as_mut_ptr(),
385            vals.as_mut_ptr(),
386        )
387    };
388    if let Err(e) = drv.check(r, "cuModuleLoadDataEx") {
389        let text = CStr::from_bytes_until_nul(&log)
390            .map(|c| c.to_string_lossy().into_owned())
391            .unwrap_or_default();
392        return Err(format!("{e}; JIT log: {}", text.trim()));
393    }
394    Ok(module)
395}
396
397impl Kernels {
398    fn null() -> Kernels {
399        let n = null_mut();
400        Kernels {
401            sgemm: n,
402            im2col: n,
403            depthwise: n,
404            add_bias: n,
405            relu: n,
406            silu: n,
407            gelu: n,
408            hardsigmoid: n,
409            sigmoid: n,
410            se_gate: n,
411            zero_cols: n,
412            pad_rb: n,
413            pool_max: n,
414            pool_avg: n,
415            global_avg: n,
416            scale_ch: n,
417            add2: n,
418            copy_part: n,
419            interp_nearest: n,
420            chw_to_seq: n,
421            conv_transpose: n,
422            layer_norm: n,
423            attn_split: n,
424            attn_softmax: n,
425            attn_merge: n,
426            ctc_digest: n,
427            pad_all: n,
428            crop_cols: n,
429        }
430    }
431
432    fn get(drv: &Driver, module: Handle) -> Result<Kernels, String> {
433        let f = |name: &CStr| -> Result<Handle, String> {
434            let mut h = null_mut();
435            let what = format!("cuModuleGetFunction({})", name.to_string_lossy());
436            drv.check(
437                unsafe { (drv.module_get_function)(&mut h, module, name.as_ptr()) },
438                &what,
439            )?;
440            Ok(h)
441        };
442        Ok(Kernels {
443            sgemm: f(c"sgemm")?,
444            im2col: f(c"im2col")?,
445            depthwise: f(c"depthwise")?,
446            add_bias: f(c"add_bias")?,
447            relu: f(c"relu")?,
448            silu: f(c"silu")?,
449            gelu: f(c"gelu")?,
450            hardsigmoid: f(c"hardsigmoid")?,
451            sigmoid: f(c"sigmoid")?,
452            se_gate: f(c"se_gate")?,
453            zero_cols: f(c"zero_cols")?,
454            pad_rb: f(c"pad_rb")?,
455            pool_max: f(c"pool_max")?,
456            pool_avg: f(c"pool_avg")?,
457            global_avg: f(c"global_avg")?,
458            scale_ch: f(c"scale_ch")?,
459            add2: f(c"add2")?,
460            copy_part: f(c"copy_part")?,
461            interp_nearest: f(c"interp_nearest")?,
462            chw_to_seq: f(c"chw_to_seq")?,
463            conv_transpose: f(c"conv_transpose")?,
464            layer_norm: f(c"layer_norm")?,
465            attn_split: f(c"attn_split")?,
466            attn_softmax: f(c"attn_softmax")?,
467            attn_merge: f(c"attn_merge")?,
468            ctc_digest: f(c"ctc_digest")?,
469            pad_all: f(c"pad_all")?,
470            crop_cols: f(c"crop_cols")?,
471        })
472    }
473}
474
475#[cfg(test)]
476mod tests {
477    use super::*;
478
479    /// PTX 编译失败要返回错误并带 JIT 日志(调用方据此退回 CPU),不能 panic。没有驱动时跳过。
480    #[test]
481    fn bad_ptx_is_an_error() {
482        let Ok(g) = Gpu::open() else { return };
483        let e = load_module(&g.drv, "this is not ptx\0").err().unwrap();
484        assert!(e.contains("cuModuleLoadDataEx"), "{e}");
485    }
486
487    #[test]
488    fn ptx_by_compute_capability() {
489        let src = |cc| select_ptx(cc).map(|p| p.source);
490        assert!(src((8, 6)).unwrap().contains("\n.target sm_70\n"));
491        assert!(src((7, 0)).unwrap().contains("\n.target sm_70\n"));
492        assert!(src((6, 1)).unwrap().contains("\n.target sm_61\n"));
493        assert!(src((6, 2)).unwrap().contains("\n.target sm_61\n"));
494        assert_eq!(select_ptx((6, 1)).unwrap().name, "sm_61");
495        assert_eq!(select_ptx((8, 6)).unwrap().name, "sm_70");
496        assert!(src((6, 0)).is_err());
497        assert!(src((5, 2)).is_err());
498        assert!(
499            PTX_SM61.is_ascii() && PTX_SM70.is_ascii(),
500            "ptxas 只收 ASCII"
501        );
502        assert!(PTX_SM61.ends_with('\0') && PTX_SM70.ends_with('\0'));
503    }
504}