C++ CUDA Tile 高性能 GPU 内核开发指南
对于长期使用CUDA进行GPU编程的开发者来说,一个好消息是:现在,你可以在现有的大型C++ GPU代码库中,直接使用NVIDIA CUDA Tile编程模型来开发高度优化的内核了。
这项能力随着CUDA 13.3的发布而到来。回想一下,CUDA Tile最初在CUDA 13.1中引入,它为GPU带来了基于“分块”(Tile)的编程范式。其设计包含一个顶层语言层和一个中间层,任何高级编程语言都可以以其为目标。它的妙处在于,能够自动利用NVIDIA硬件的先进特性——比如张量核心、共享内存和内存翻跟斗——而无需应用程序直接针对它们进行繁琐的底层编码。
最初,Python是首个支持分块GPU应用的语言。而新发布的CUDA 13.3,则增加了对C++编写分块内核的支持,这让开发者构建高度优化的GPU内核如虎添翼。
什么是CUDA Tile C++?
简单来说,CUDA Tile C++是CUDA Tile编程模型在C++中的实现,它构建在CUDA Tile IR规范之上。它允许开发者用C++编写分块内核,并使用基于“分块”的模型来表达GPU内核,这可以作为传统单指令多线程(SIMT)模型的一种补充或替代。
我们来快速回顾一下分块模型的核心概念:
- 多维数组是主要的数据存储单元。
- 分块(Tile)是数组中被内核操作的部分。
- 内核(Kernel)是由多个块(Block)并行执行的函数。
- 块(Block)是GPU的子集;对分块的操作会在每个块内的所有线程间并行化。
CUDA Tile C++的便利之处在于,它自动处理了块内的并行性、异步操作、内存移动等GPU编程中的底层细节。更重要的是,它具备跨不同NVIDIA GPU架构的移植性,这意味着开发者无需重写代码,就能利用最新硬件的特性。
从向量加法看CUDA Tile C++
熟悉CUDA C++ SIMT编程的开发者,肯定都写过经典的向量加法内核。假设数据已在GPU上,一个SIMT风格的向量加法内核接收两个向量,进行逐元素相加,并输出第三个向量。这是最简单的CUDA内核之一,代码通常长这样:
__global__ void vecAdd(float* A, float* B, float* C, int vectorLength){
int workIndex = threadIdx.x + blockIdx.x*blockDim.x;
if(workIndex < vectorLength) {
C[workIndex] = A[workIndex] + B[workIndex];
}
}
在这个内核中,每个线程的工作被显式指定,程序员在启动内核时,还需要明确指定要启动的块和线程数量。
那么,用CUDA Tile C++编写的等效代码是怎样的呢?最大的区别在于,你不再需要指定每个线程具体做什么。你只需要将数据分解成块(Tile),并指定对这些块进行何种数学运算即可,其他的一切都由框架自动处理。
下面是一个完整的CUDA Tile C++向量加法内核示例,它详细展示了每一步:
#include "cuda_tile.h"
__tile_global__ void vectorAdd(float* a, float* b, float* out, size_t n) {
namespace ct = cuda::tiles;
using namespace ct::literals;
auto aSpan = ct::tensor_span{a, ct::extents{n}};
auto bSpan = ct::tensor_span{b, ct::extents{n}};
auto oSpan = ct::tensor_span{out, ct::extents{n}};
auto aView = ct::partition_view{aSpan, ct::shape{8_ic}};
auto bView = ct::partition_view{bSpan, ct::shape{8_ic}};
auto oView = ct::partition_view{oSpan, ct::shape{8_ic}};
int bx = ct::bid().x;
auto aTile = aView.load(bx);
auto bTile = bView.load(bx);
auto oTile = aTile + bTile;
oView.store(oTile, bx);
}
乍一看,为了一个简单的向量加法写这么多代码,似乎有点“杀鸡用牛刀”。别担心,上面这个版本是为了清晰展示所有步骤。实际上,我们可以把它写得更简洁。我们来拆解一下关键部分:
- 内核声明:使用
__tile_global__来告诉编译器这是一个分块内核。参数传递和SIMT内核类似。 - 命名空间:设置
cuda::tiles命名空间及其字面量,方便使用编译时常量(如8_ic)。 - 创建张量跨度:使用
ct::tensor_span为每个数组创建跨度。它类似于C++23的std::mdspan,封装了指向多维数组的指针及其形状(维度)和布局信息。ct::extents{n}指明这是一个大小为n的一维数组。 - 创建分区视图:通过
ct::partition_view将张量跨度包装为一系列不重叠的、固定大小的分区。这里的ct::shape{8_ic}指定每个分区(即分块)的大小为8。 - 加载输入分块:通过
ct::bid().x获取当前块在X维度的索引,然后使用分区视图的load函数,自动获取并加载对应索引的数据块到分块对象中。 - 计算与存储:直接对分块对象进行逐元素加法运算,结果存储到输出分块,最后用
store函数写回对应的分区视图。
可以看到,计算逻辑(aTile + bTile)变得异常简洁,而数据移动和索引管理的复杂性被隐藏了起来。
完整的向量加法示例与优化
下面是一个完整、可运行的C++代码示例,展示了如何调用上述内核,并融入了一些关键的编译器优化提示:
#include
#include
#include "cuda_tile.h"
__tile_global__ void vectorAdd(float* __restrict__ a, float* __restrict__ b,
float* __restrict__ out, size_t n) {
namespace ct = cuda::tiles;
using namespace ct::literals;
a = ct::assume_aligned(a, 16_ic);
b = ct::assume_aligned(b, 16_ic);
out = ct::assume_aligned(out, 16_ic);
int bx = ct::bid().x;
auto aTile = ct::partition_view{ct::tensor_span{a, ct::extents{n}}, ct::shape{1024_ic}}.load_masked(bx);
auto bTile = ct::partition_view{ct::tensor_span{b, ct::extents{n}}, ct::shape{1024_ic}}.load_masked(bx);
auto oTile = aTile + bTile;
auto oView = ct::partition_view{ct::tensor_span{out, ct::extents{n}}, ct::shape{1024_ic}};
oView.store_masked(oTile, bx);
}
// ... (错误检查宏和main函数,包含主机端内存分配、初始化、数据拷贝等)
int main() {
constexpr size_t N = 2ULL << 25;
constexpr int TILE_SIZE = 1024;
constexpr int BLOCKS = (N + TILE_SIZE - 1) / TILE_SIZE;
// ... 主机和设备内存分配、初始化、数据拷贝代码 ...
// 启动内核
vectorAdd<<>>(d_a, d_b, d_out, N);
// ... 同步设备、检查错误、验证结果、清理资源 ...
}
这个版本做了几处重要优化:
- 使用
__restrict__:告知编译器指针之间没有别名,有助于生成更优的代码。 - 内存对齐提示:通过
ct::assume_aligned<16>告诉编译器指针是16字节对齐的(cudaMalloc分配的内存默认256字节对齐,满足此条件),以生成更高效的内存访问指令。 - 更大的分块尺寸:使用了1024作为分块大小,而非之前的8,以提升效率。
- 掩码加载/存储:使用
load_masked和store_masked来处理数据大小不能被分块尺寸整除的情况,保证边界安全。
对于熟悉启动SIMT内核的开发者,启动分块内核的过程类似,但有一个关键区别:
vectorAdd<<>>(d_a, d_b, d_out, N);
启动配置 << 中,第一个参数是分块块的数量(对应于SIMT中的线程块数量)。第二个参数必须固定为1。用于执行内核的线程数由编译器决定,因此在启动分块内核时,第二个维度始终设为1。
在CUDA 13.3或更高版本上,使用计算能力8.0(Ampere架构)或更新的GPU,用以下命令编译和运行:
$ nvcc -std=c++20 --enable-tile -arch sm_80 -o vectorAdd vectorAdd.cu
$ ./vectorAdd
N: 67108864
Max error: 0.000000e+00
至此,你的第一个CUDA Tile C++程序就成功运行了。
开发者工具支持
分块C++内核可以像SIMT内核一样,使用NVIDIA Nsight Compute进行性能剖析。以下命令展示了如何创建剖析文件:
$ ncu -o VecAddProfile --set detailed ./vectorAdd
在Nsight Compute的图形界面中打开生成的报告后:
- 从下拉菜单中选择
vectorAdd内核。 - 选择 Details 标签页。
- 展开 Tile Statistics 报告部分。
在 Tile Statistics 部分,你可以看到指定的分块块数量、编译器选择的块大小以及其他分块特有的信息。源码页面同样支持分块内核,可以像CUDA C++内核一样查看源码行级别的性能指标。
矩阵乘法示例
向量加法的例子详细展示了加载和存储分区视图的细节。下面这个矩阵乘法示例则能更好地体现如何用简洁的代码表达复杂计算。
这个内核计算一个MxK矩阵与一个KxN矩阵的乘积,得到MxN矩阵。本例中M=8,N=16,K可以是变量(只要它是8的倍数),这里设K=24。使用小尺寸仅是为了演示概念。
#include "cuda_tile.h"
__tile_global__ void kernel(float* __restrict__ a, float* __restrict__ b,
size_t length, float* __restrict__ c) {
namespace ct = cuda::tiles;
using namespace ct::literals;
a = ct::assume_aligned(a, 16_ic);
b = ct::assume_aligned(b, 16_ic);
c = ct::assume_aligned(c, 16_ic);
auto aShape = ct::extents{8_ic, length};
auto bShape = ct::extents{length, 16_ic};
auto cShape = ct::extents{8_ic, 16_ic};
auto aSpan = ct::tensor_span{a, aShape};
auto bSpan = ct::tensor_span{b, bShape};
auto cSpan = ct::tensor_span{c, cShape};
auto aView = ct::partition_view{aSpan, ct::shape{4_ic, 8_ic}};
auto bView = ct::partition_view{bSpan, ct::shape{8_ic, 4_ic}};
auto cView = ct::partition_view{cSpan, ct::shape{4_ic, 4_ic}};
using f32x4x4 = ct::tile>;
auto accTile = ct::full(0);
auto [xBlock, yBlock, dummy] = ct::bid();
for (auto idx : ct::irange(0, 1 + int(length - 1) / 8)) {
auto aTile = aView.load_masked(xBlock, idx);
auto bTile = bView.load_masked(idx, yBlock);
accTile = ct::mma(aTile, bTile, accTile);
}
cView.store_masked(accTile, xBlock, yBlock);
}
这个内核的核心步骤可以概括为:
- 定义矩阵形状:为输入输出矩阵创建
ct::extents对象。 - 创建张量跨度:基于形状和原始指针创建张量跨度。
- 创建分区视图:为矩阵创建分区视图。例如,将8x24的A矩阵划分为4x8的分块视图,将24x16的B矩阵划分为8x4的视图。这决定了C矩阵的视图是4x4的。
- 初始化累加分块:创建一个与C视图形状匹配的4x4分块,用于累加计算结果。
- 执行计算循环:获取当前块在网格中的二维索引。循环遍历K维度(除以8,与A、B视图的K维度匹配)。在循环内,加载A和B的对应分块,调用
ct::mma(矩阵乘加)函数进行计算并累加。 - 存储结果:将累加结果分块存储到C的对应分区视图中。
值得注意的是,内核中大部分代码用于设置数据视图和加载/存储数据,而真正的计算部分(ct::mma)非常简洁。
启动内核:由于C矩阵是8x16,而每个块计算一个4x4的分块,因此需要在X维度启动2个块(8/4),在Y维度启动4个块(16/4)。
kernel<<>>(d_a, d_b, K, d_c);
开始使用CUDA Tile C++
要运行CUDA Tile C++程序,你需要满足以下条件:
- 计算能力为8.x或更新的GPU。
- NVIDIA R580或更高版本的驱动程序。如果分块内核需要JIT编译,则驱动程序版本必须等于或高于用于生成代码的CUDA工具包所对应的版本。例如,CUDA Toolkit 13.3需要R610或更新的驱动。
- CUDA Toolkit 13.3。
分块编程的强大能力现已向C++开发者开放。查阅相关文档、API参考手册,并下载CUDA Toolkit 13.3,立即开始编写你的分块C++内核,亲身体验加速计算的新标准。
相关攻略
CUDA13 3版本新增了对C++编写分块内核的支持,使开发者能在现有C++GPU代码库中直接使用CUDATile编程模型。该模型基于“分块”概念,自动利用张量核心、共享内存等硬件特性,无需繁琐的底层编码。与传统SIMT模型相比,它通过声明数据分块和运算逻辑,隐藏了并行、内存移动等细节,简化了开发过程,并具备跨GPU架构的移植性。
智谱开源模型GLM-5 1在长程任务处理上取得突破,仅用14小时即将CUDAKernel性能提升数十倍,全程自主完成分析、重写与测试。该模型在SWE-benchPro基准测试中位列全球第一,能力已从代码生成升级为完整项目交付,例如一小时构建MacOS桌面环境,大幅提升工程效率。这标志着AI正从“对话者”转向“执行者”,推动软件工程生产力变革。
在人工智能与高性能计算领域,CUDA编程是释放GPU极致算力的核心技术。它要求开发者不仅精通算法逻辑,更要深刻理解GPU硬件架构与并行计算原理,掌握复杂的内存管理与性能调优技巧。作为现代AI计算的基石,其陡峭的学习门槛让众多开发者望而却步。如今,来自清华大学智能产业研究院(AIR)与字节跳动种子团队
QLoRA微调Gemma模型时CUDA设备断言失败的完整解决方案 本文详解QLoRA+PEFT微调Gemma等大模型时,因CUDA上下文未正确初始化导致的device >= 0 && device < num_gpus断言错误,提供从环境重置、配置修正到稳健训练的全流程避坑指南。 如果你正在使用QL
中国寻求突破NVIDIA CUDA护城河:一种前所未有的新方式 4月8日消息,在寻求突破NVIDIA CUDA生态壁垒的种种尝试中,有一个战略级的变通方案,其思路相当值得深入探讨。 时间拉回到不久前的SEMICON CHINA 2026全球半导体产业战略峰会。会上,中国半导体行业协会副理事长、IC设
热门专题
热门推荐
来看一组让人揪心的数字:截至5月28日,超过半数的委内瑞拉民众,选择支持经济“美元化”——他们想要用美元来对抗全球数一数二的恶性通胀。根据AtlasIntel的调研,31%的受访者明确支持美元化,另有26%的人表示强烈支持,加起来支持率高达57%;而明确反对或强烈反对的,合计只有30%。换句话说,在
游戏开局,玩家第一眼看到的主角是谁?没错,就是零。不过这里有个挺常见的误会——很多人会下意识觉得零是女主角,那是不是还有个男主角?其实不然。进入游戏之后,外观是可以自由选择的,性别、形象都由你定,男女主角本质上都是同一个人。两种造型唯一的区别就是视觉风格,至于基础属性、成长路线、技能体系,完全一致。
或许有人觉得,AI音乐生成工具不过是图个新鲜感,与专业音乐制作相距甚远。但5月28日,ElevenLabs推出的Music v2,很可能改变这一印象。这次升级版音乐生成模型,已不再停留在去年那个“新手友好”的初级阶段,而是在工作流、版权合规和落地场景上都做了充分布局。 一、核心进化:创作从“一次性生
iPhone20周年纪念款将采用四曲面屏与圆润边框设计,边框仅1 1毫米,但边缘亮度存在失真问题,苹果正与三星、LG合作解决。若无法攻克,可能沿用平面边框。该款预计2027年亮相,属于Pro系列,含双版本,并计划采用屏下前摄与FaceID。
对于技术从业者而言,面试备考始终是一个老生常谈却又不断变化的话题。时间碎片化、知识点庞杂、实战表达欠缺,每一项都可能成为关键时刻的瓶颈。有没有一种方法,能让我们把通勤、运动等零散时间充分利用起来,高效地“打磨技能”呢?今天要介绍的「播面」,或许就是一个值得关注的解题新思路。 播面是什么 简单来说,「





