如何建设数据库搜索网站,a8直播免费版,保健品网站可以做网站,wps怎么做网页在大模型时代#xff0c;算法与系统的边界日益模糊。想要复现 DeepMind 或 OpenAI 的工作#xff0c;光会设计 Loss Function 已经不够了#xff0c;必须深入理解底层的算力调度。本文开始从零手写 CUDA Runtime API 的过程。要求在不依赖高级框架的前提下#xff0c;直接通…在大模型时代算法与系统的边界日益模糊。想要复现 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 类型如 HostToDeviceCUDA 也有自己的一套。 // 虽然它们底层代表的数字可能一样但 C 类型检查很严必须做一个显式转换。 inline cudaMemcpyKind toCudaKind(llaisysMemcpyKind_t kind) { return static_castcudaMemcpyKind(kind); }原理cuda_runtime.h里定义了所有cuda开头的函数如cudaMalloc。不加这个编译器会报错说“我不认识这些词”。模块一设备管理原理 你电脑上可能插了 2 张显卡也可能 1 张。代码需要知道有多少个“工厂”GPU并且指定你要用哪一个。同步SynchronizeCPU 发完命令通常扭头就走异步但有时候 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是一个你作业里定义的枚举EnumCUDA 不认识所以我们需要写个转换函数把你的枚举转成 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); }