大模型推理基石:如何用 C++ 封装 CUDA API?(含源码与原理解析)

在大模型时代,算法与系统的边界日益模糊。想要复现 DeepMind 或 OpenAI 的工作,光会设计 Loss Function 已经不够了,必须深入理解底层的算力调度。

本文开始从零手写 CUDA Runtime API 的过程。要求在不依赖高级框架的前提下,直接通过 C++ 和 CUDA Driver API 实现设备管理、内存分配(Pinned Memory)以及异步流(Stream)调度。

对于习惯了 Python 动态类型的我来说,这是一次对“第一性原理”的艰难回归。

0. 什么是 Runtime API?

在 AI 系统中,Runtime 是连接 上层算法(Python)下层硬件(GPU) 的桥梁。

  • CPU 像是一个精算师,负责发号施令。

  • GPU 像是一个拥有几千工人的大工厂,负责并行计算。

我们要写的代码,就是让 CPU 能指挥 NVIDIA GPU 干活的“指令集”。

我们需要用 NVIDIA 提供的原生库 CUDA 来填充这些空函数。我把任务拆成三个核心模块来讲:管设备、管内存、管搬运

首先,在代码最上面,你需要引入 CUDA 的官方头文件,否则编译器看不懂什么是 cudaMalloc

#include "../runtime_api.hpp" 
// 核心:引入 CUDA 运行时库,所有的 cudaMalloc, cudaMemcpy 都在这里定义
#include <cuda_runtime.h> 
#include <cstdlib>
#include <cstring>
#include <cstdio>

namespace llaisys::device::nvidia {
namespace runtime_api {

// 类型转换助手
// 我们的系统定义了一套 memcpy 类型(如 HostToDevice),CUDA 也有自己的一套。
// 虽然它们底层代表的数字可能一样,但 C++ 类型检查很严,必须做一个显式转换。
inline cudaMemcpyKind toCudaKind(llaisysMemcpyKind_t kind) {
    return static_cast<cudaMemcpyKind>(kind);
}

原理cuda_runtime.h 里定义了所有 cuda 开头的函数(如 cudaMalloc)。不加这个,编译器会报错说“我不认识这些词”。

模块一:设备管理

原理: 你电脑上可能插了 2 张显卡,也可能 1 张。代码需要知道有多少个“工厂”(GPU),并且指定你要用哪一个。

  • 同步(Synchronize):CPU 发完命令通常扭头就走(异步),但有时候 CPU 必须停下来,等 GPU 把活儿干完才能进行下一步。这就叫“设备同步”。

getDeviceCount (数人头):

int getDeviceCount() {
    int count = 0;
    // cudaGetDeviceCount 会把显卡数量写入 count 变量
    cudaError_t err = cudaGetDeviceCount(&count);
    // 如果返回错误(比如没装驱动),就返回 0
    if (err != cudaSuccess) return 0;
    return count;
}

setDevice (点名):

void setDevice(int device_id) {
    // 告诉系统:接下来的命令,都是发给第 device_id 号显卡的
    cudaSetDevice(device_id);
}

deviceSynchronize (全员停手)

  • 原理:CPU 发命令(比如“去算个矩阵乘法”)是异步的,发完命令 CPU 就继续往下跑了,根本不管 GPU 做没做完。

  • 这个函数的作用是:CPU 在这里死等,直到 GPU 把手头所有的活儿都干完了,CPU 才能继续。

void deviceSynchronize() {
    // CPU 发出计算命令后通常会直接往下跑(异步),不管 GPU 做没做完。
    // 这个函数的作用是:让 CPU 在这里死等,直到 GPU 把手头所有的活儿干完。
    // 在做 Benchmark 或者调试时,这一步必不可少。
    cudaDeviceSynchronize();
}

模块二:流管理(Stream = 流水线)

  • 原理:Stream 就像流水线

    • 如果你只有一个流水线(默认流),任务只能排队:A做完 -> B做完 -> C做完。

    • 如果你创建了多个流,任务可以并行:流水线1做任务A,流水线2做任务B。这样能榨干显卡性能。

    • 代码实现: 注意 llaisysStream_t 只是一个空壳(通常是 void*),我们需要把它转成 CUDA 真正的 cudaStream_t

// 1. 创建流
llaisysStream_t createStream() {
    cudaStream_t stream;
    // 创建一个新的异步任务队列
    cudaStreamCreate(&stream);
    // (llaisysStream_t) 是强制类型转换。
    // 我们把 CUDA 的流对象伪装成一个通用指针传出去。
    return (llaisysStream_t)stream;
}

// 2. 销毁流
void destroyStream(llaisysStream_t stream) {
    // 用完了记得拆掉,防止内存泄漏
    cudaStreamDestroy((cudaStream_t)stream);
}

// 3. 流同步
void streamSynchronize(llaisysStream_t stream) {
    // 只等待这一条特定流水线上的任务做完,不影响其他流水线。
    cudaStreamSynchronize((cudaStream_t)stream);
}

模块三:内存管理

原理

  • Host 内存:CPU 的内存(内存条)。

  • Device 内存:GPU 的显存。 CPU 不能直接读写显存,必须调用特殊的函数去分配。

  • mallocDevice = 在显卡上圈一块地。

  • mallocHost = 在 CPU 内存里圈一块特殊的地(锁页内存 Pinned Memory)。这种地很特殊,GPU 可以直接通过 PCIE 总线快速吸数据,比普通的 CPU 内存更快。

模块四:内存拷贝

原理: 数据在 CPU 和 GPU 之间移动,必须告诉 CUDA 搬运的方向。 你的 llaisysMemcpyKind_t 是一个你作业里定义的枚举(Enum),CUDA 不认识,所以我们需要写个转换函数,把你的枚举转成 CUDA 的 cudaMemcpyKind

  • Sync (同步拷贝):搬砖的时候,CPU 盯着看,搬完才准走。

  • Async (异步拷贝):CPU 喊一声“搬!”,然后立刻去干别的事,GPU 自己在后台慢慢搬。

1. 显存分配 (mallocDevice)

这是在显卡上申请地盘。

void *mallocDevice(size_t size) {
    void *ptr = nullptr;
    // 为什么要传 &ptr?
    // 因为 cudaMalloc 需要修改 ptr 的值,让它指向显存地址。
    // C语言基础:想在函数里修改指针的值,必须传指针的地址(二级指针)。
    cudaMalloc(&ptr, size);
    return ptr;
}

void freeDevice(void *ptr) {
    cudaFree(ptr);
}
2. 主机内存分配 (mallocHost) —— 这是一个巨大的考点!

用户可能会问:“为什么不在 CPU 上直接用 malloc,而要用 mallocHost?”

  • 原理(锁页内存 / Pinned Memory)

    • 普通的 malloc 申请的内存,操作系统可能会把它移来移去(换页),物理地址不固定。

    • GPU 的搬运工(DMA 控制器)很笨,它需要一个绝对固定的物理地址才能全速搬运数据。

    • cudaMallocHost 申请的是锁页内存。它把这块内存“钉”在物理内存条上,不准操作系统移动它。

    • 好处:CPU <-> GPU 传输速度快一倍,而且支持异步传输

void *mallocHost(size_t size) {
    void *ptr = nullptr;
    // 使用 cudaMallocHost 而不是 malloc
    cudaMallocHost(&ptr, size);
    return ptr;
}

void freeHost(void *ptr) {
    cudaFreeHost(ptr); // 必须用专门的 free 函数
}
3. 搬运数据 (memcpy)

把数据从 CPU 搬到 GPU,或者反过来。

同步搬运 (memcpySync): CPU 说:“搬!”,然后 CPU 盯着看,直到搬完才走。

void memcpySync(void *dst, const void *src, size_t size, llaisysMemcpyKind_t kind) {
    // toCudaKind 是我们最开始写的那个辅助函数
    cudaMemcpy(dst, src, size, toCudaKind(kind));
}

异步搬运 (memcpyAsync): CPU 说:“搬!”,然后 CPU 直接去做下一行代码了,GPU 自己在后台慢慢搬。

void memcpyAsync(void *dst, const void *src, size_t size, llaisysMemcpyKind_t kind, llaisysStream_t stream) {
    // 这里的 stream 参数决定了这次搬运在哪条流水线上跑
    cudaMemcpyAsync(dst, src, size, toCudaKind(kind), (cudaStream_t)stream);
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值