在大模型时代,算法与系统的边界日益模糊。想要复现 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);
}
1348

被折叠的 条评论
为什么被折叠?



