news 2026/6/23 6:32:27

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
大模型推理基石:如何用 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); }
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/23 17:19:29

水文监测站:水资源管理的“千里眼”与“顺风耳”

水文监测站是对江、河、湖泊、水库、渠道和地下水等水文要素进行实时监测的专业站点&#xff0c;在水资源管理、防洪减灾、水利工程建设与运行、生态环境保护等诸多方面都发挥着极为重要的作用。一、定义与功能水文监测站又称雷达水位监测站、自动水文监测设备&#xff0c;是一…

作者头像 李华
网站建设 2026/6/12 9:22:31

白银波动幅度大于黄金的原因:市场规模与属性差异深度解析

想象一下&#xff0c;你正站在2025年的码头上&#xff0c;面前停着两艘船。一艘是排水量十万吨的超级豪华邮轮&#xff0c;上面写着“黄金号”&#xff1b;另一艘是动力强劲的快艇&#xff0c;船身印着“白银号”。这时候&#xff0c;海上突然刮起了一阵名为“美联储降息”的巨…

作者头像 李华
网站建设 2026/6/18 19:26:53

【2026版】Spring Boot面试题

这篇文章涵盖了Spring Boot的核心概念和高级主题&#xff0c;包括自动配置、多数据源、Actuator、缓存、AOP、异步编程、事务管理、安全性、消息队列和微服务架构。这些问题可以帮助您评估候选人的技能和经验&#xff0c;同时也是提高自己Spring Boot技能的好方法。 1. Spring …

作者头像 李华
网站建设 2026/6/23 5:01:11

办公小程序开发----提高工作效率

文章目录 前言一、批量html文件转化为pdf1. 问题描述2.代码3.程序下载链接 总结 前言 办公过程中&#xff0c;总是会遇到一些大量重复做的事情&#xff0c;通过生成一些小程序&#xff0c;提高办公效率。 一、批量html文件转化为pdf 1. 问题描述 我需要将若干的html文件转换…

作者头像 李华
网站建设 2026/6/22 0:23:31

Jmeter 命令行压测生成HTML测试报告

&#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快通常Jmeter的 GUI 模式仅用于调试&#xff0c;在实际的压测项目中&#xff0c;为了让压测机有更好的性能&#xff0c;多用 Jmeter 命令行来进行压测。同时&#xff…

作者头像 李华
网站建设 2026/6/23 11:57:01

AI编程系列——mcp与skill

mcp是啥 Model Context Protocol MCP&#xff1a;AI Agent 工具托管协议及应用 简单来说 就是让ai可以调用外部服务&#xff0c;比如你们公司的cicd功能、让你部署的deepseek连上A股实时行情 变成你的ai炒股小助理 抓取网页爬虫…… MCP 协议概述定义: MCP (Model Context Pro…

作者头像 李华