1. 实例编译 运行
main.cu
//nvcc -g -lineinfo -std=c++17 -arch=native main.cu -o main
#include <iostream>
#include <thrust/device_vector.h>
/*
ldmatrix.sync.aligned.shape.num{.trans}{.ss}.type r, [p];
.shape = {.m8n8};
.num = {.x1, .x2, .x4};
.ss = {.shared{::cta}};
.type = {.b16};
*/
__device__
void ldmatrix_x2(unsigned int (&x)[2], const void* ptr){
asm volatile("ldmatrix.sync.aligned.m8n8.x2.shared.b16 {%0, %1}, [%2];"
: "=r"(x[0]), "=r"(x[1])
: "l"(__cvta_generic_to_shared(ptr)));
}
__global__
void mykernel(const int* loadOffsets, bool print){
alignas(16) __shared__ half A[128 * 16];
for(int i = threadIdx.x; i < 128*16; i += blockDim.x){
A[i] = i;
}
__syncthreads();
const int lane = threadIdx.x % 32;
unsigned int result[2];
const int offset = loadOffsets[lane];
ldmatrix_x2(result, &A[offset]);
half2 loaded[2];
memcpy(&loaded[0], &result[0], sizeof(half2) * 2);
if(print){
for(int m = 0; m < 2; m++){
for(int t = 0; t < 32; t++){
if(lane == t){
printf("%4d %4d ", int(loaded[m].x), int(loaded[m].y));
if(lane % 4 == 3){
printf("\n");
}
}
__syncwarp();
}
if(lane == 0){
printf("\n");
}
__syncwarp();
}
}
}
int main(){
thrust::device_vector<int> d_loadOffsets(32, 0);
for(int i = 0; i < 16; i++){
const int row = i % 8;
const int matrix = i / 8;
d_loadOffsets[i] = row * 16 + matrix * 8;
}
mykernel<<<1,32>>>(d_loadOffsets.data().get(), true);
cudaDeviceSynchronize();
// Shared Load Matrix: Requests 16.384, Wavefronts 33.393, Bank Conflicts 0
for(int i = 0; i < 16; i++){
const int row = i / 2;
const int matrix = i % 2;
d_loadOffsets[i] = row * 16 + matrix * 8;
}
std::cout << "offsets: ";
for(int i = 0; i < 16; i++){
std::cout << d_loadOffsets[i] << " ";
}
std::cout << "\n";
mykernel<<<1024,512>>>(d_loadOffsets.data().get(), false);
cudaDeviceSynchronize();
// Shared Load Matrix: Requests 16.384, Wavefronts 131.674, Bank Conflicts 98.304
for(int i = 0; i < 16; i++){
const int row = i / 2;
const int matrix = i % 2;
d_loadOffsets[i] = (4*row) * 16 + matrix * 8;
}
std::cout << "offsets: ";
for(int i = 0; i < 16; i++){
std::cout << d_loadOffsets[i] << " ";
}
std::cout << "\n";
mykernel<<<1024,512>>>(d_loadOffsets.data().get(), false);
cudaDeviceSynchronize();
// Shared Load Matrix: Requests 16.384, Wavefronts 66.488, Bank Conflicts 32.768
for(int i = 0; i < 16; i++){
const int row = i % 8;
const int matrix = i / 8;
d_loadOffsets[i] = row * 16 + matrix * 8;
}
std::cout << "offsets: ";
for(int i = 0; i < 16; i++){
std::cout << d_loadOffsets[i] << " ";
}
std::cout << "\n";
mykernel<<<1024,512>>>(d_loadOffsets.data().get(), false);
cudaDeviceSynchronize();
// Shared Load Matrix: Requests 16.384, Wavefronts 263.070, Bank Conflicts 229.376
for(int i = 0; i < 16; i++){
const int row = i % 8;
const int matrix = i / 8;
d_loadOffsets[i] = (4*row) * 16 + matrix * 8;
}
std::cout << "offsets: ";
for(int i = 0; i < 16; i++){
std::cout << d_loadOffsets[i] << " ";
}
std::cout << "\n";
mykernel<<<1024,512>>>(d_loadOffsets.data().get(), false);
cudaDeviceSynchronize();
}
编译运行:
nvcc -g -lineinfo -std=c++17 -arch=native main.cu -o main
或者 device 代码 debug版:
$ nvcc -g -G -std=c++17 -arch=native main.cu -o main
修改程序后,
//nvcc -g -lineinfo -std=c++17 -arch=native main.cu -o main
#include <iostream>
#include <thrust/device_vector.h>
/*
ldmatrix.sync.aligned.shape.num{.trans}{.ss}.type r, [p];
.shape = {.m8n8};
.num = {.x1, .x2, .x4};
.ss = {.shared{::cta}};
.type = {.b16};
*/
__device__
void ldmatrix_x2(unsigned int (&x)[2], const void* ptr){
asm volatile("ldmatrix.sync.aligned.m8n8.x2.shared.b16 {%0, %1}, [%2];"
: "=r"(x[0]), "=r"(x[1])
: "l"(__cvta_generic_to_shared(ptr)));
}
__global__
void mykernel(const int* loadOffsets, bool print){
alignas(16) __shared__ half A[128 * 16];
for(int i = threadIdx.x; i < 128*16; i += blockDim.x){
A[i] = i;
}
__syncthreads();
const int lane = threadIdx.x % 32;
unsigned int result[2];//one int, 2 half mem space
const int offset = loadOffsets[lane];
ldmatrix_x2(result, &A[offset]);
half2 loaded[2];
memcpy(&loaded[0], &result[0], sizeof(half2) * 2);
if(print){
for(int m = 0; m < 2; m++){
for(int t = 0; t < 32; t++){
//if(lane == t){
if(lane == t){
printf("[%2d]%4d %4d ",lane, int(loaded[m].x), int(loaded[m].y));
if(lane % 4 == 3){
printf("\n");
}
}
__syncwarp();
}
if(lane == 0){
printf("\n");
}
__syncwarp();
}
}
}
int main(){
thrust::device_vector<int> d_loadOffsets(32, 0);
for(int i = 0; i < 16; i++){
const int row = i % 8;
const int matrix = i / 8;
d_loadOffsets[i] = row * 16 + matrix * 8;
printf("%d ", row*16 + matrix*8);
}
printf("\n\n");
//thrust::host_vector<int> h_loadOffsets(32, 0);
int * hld = nullptr;
hld = (int*)malloc(32*sizeof(int));
cudaMemcpy(hld, d_loadOffsets.data().get(), sizeof(int)*32, cudaMemcpyDeviceToHost);
for(int i=0; i<32; i++)
printf("%d, ", hld[i]);
printf("\n");
mykernel<<<1,32>>>(d_loadOffsets.data().get(), true);
cudaDeviceSynchronize();
}
同上编译方式,输出:
比较容易发现搬运数据映射关系
这里我们先猜一下其数据关系,
首先,矩阵以m8n8为一个小矩阵加载进warp 的 32个 lane中,每个lane 从这个小矩阵中拿到两个地址连续的变量;x2,是说一次 load 两个8x8小矩阵,这样的话,每个lane 会得到4个变量;x4的话,就是4*2=8个变量。
每个小矩阵需要提供8个行的起始地址,第一个小矩阵的8个行起始地址填写在0~7号 lane 的寄存器中;第二个小矩阵的8个行起始地址填写在 8~15号lane的寄存器中,各个lane中同名寄存器作为 ldmatrix 的参数。即,代码中的 const void* ptr 。
可以推得,如果是x4,4个8x8 的小矩阵,那么需要提供4组8个行的起始地址,这样,32个 lane 每个都持有一个小矩阵的行起始地址。
第二节第三节再深入系统地分析。
2. 实例功能解析
进一步详细解析这个执行了 ldmatrix
的 CUDA Device 函数,这是一个非常经典且高效的用法。
2.1. 函数签名解析
__device__ void ldmatrix_x2(unsigned int (&x)[2], const void* ptr)
__device__
cuda 语法,声明这是一个在 GPU 上执行的函数。
unsigned int (&x)[2]
这是一个对包含 2 个 unsigned int
的数组的引用,C++ 语法。使用引用 (&
) 允许函数直接修改调用者传入的数组元素,避免了传值拷贝。这个数组的两个元素 x[0]
和 x[1]
将被用作内联汇编中的目标寄存器。
const void* ptr
这是一个指向共享内存中某个数据的通用指针(generic)。const
表示函数不会通过这个指针修改数据,void*
提供了灵活性,可以指向任何类型的数据。
2.2. 内联汇编详解
asm volatile("ldmatrix.sync.aligned.m8n8.x2.shared.b16 {%0, %1}, [%2];"
: "=r"(x[0]), "=r"(x[1]) // Output operands
: "l"(__cvta_generic_to_shared(ptr))); // Input operand
我们一点一点地分解:
2.2.1. 汇编模板字符串
("ldmatrix.sync.aligned.m8n8.x2.shared.b16 {%0, %1}, [%2];"
)
这是要执行的 PTX 指令。
ldmatrix
: 指令本身,用来加载矩阵。
.sync
: Warp 级同步指令,确保 Warp 内所有活跃线程协同执行。
.aligned
: 强制要求源内存地址 (ptr
) 必须是 16 字节对齐的。
.m8n8
: 指定从内存中加载的数据布局对应于一个 8 行 x 8 列的矩阵。这个形状的矩阵数据元素只能是 16bit的。还可以有 .m16n16、.m8n16,这是对应 8bit/6bit/4bit 矩阵元素。
.x2
: 指定一次执行 ldmatrix 时,加载 2 个 m8n8 的小矩阵。
The values .x1
, .x2
and .x4
for .num
indicate one, two or four matrices respectively. When .shape
is .m16n16
, only .x1
and .x2
are valid values for .num
.
.shared
: 明确指定源数据位于共享内存(Shared Memory) 中。
.b16
: 指定内存访问模式。.b16
表示这是一次 16 字节的访问。这与共享内存的 bank 宽度和高效访问模式有关。
{%0, %1}
: 这是目标操作数列表。占位符 %0
和 %1
将会被编译器替换为后面约束列表中找到的实际寄存器。这里它要求 2 个 32 位的寄存器。
为什么是 2 个? 一个 8x8 的矩阵,每个元素 2 字节,总大小为 8 * 8 * 2 = 128
字节。一个 Warp 有 32 个线程。ldmatrix
指令将这 128 字节的数据转置后,分布到整个 Warp 的线程寄存器中。每个线程负责 128/32 = 4
字节的数据。一个 32 位寄存器是 4 字节,所以每个线程需要 1 个寄存器来存储它的那部分数据。那么为什么这里列表里有 2 个?实际上,这条指令是在加载2个这样的 8x8 矩阵。这里的关键在于指令的变体。在 SM_70+ 上,ldmatrix
可以加载 1、2 或 4 个矩阵。
[%2]
: 这是源操作数。它是一个包含共享内存地址的寄存器,地址为16bit aligned。%2
将被替换为输入操作数提供的值。
2.2.2. 输出操作数
(: "=r"(x[0]), "=r"(x[1])
)
"=r"
: 约束修饰符。
=
表示这是一个只写的输出操作数。
r
register 之意,表示要求编译器分配一个32 位通用寄存器来保存这个值。
(x[0]), (x[1])
: 对应的 C++ 变量。指令执行后,目标寄存器 %0
和 %1
中的值会被写回到数组 x
的这两个元素中。
作用:告诉编译器:“请为 x[0]
和 x[1]
分配两个寄存器。执行汇编指令后,结果将在这两个寄存器中,请将它们写回 x[0]
和 x[1]
。”
2.2.3. 输入操作数
(: "l"(__cvta_generic_to_shared(ptr))
)
这是最精妙和关键的部分。
__cvta_generic_to_shared(ptr)
: 这是一个 CUDA 内部函数。
作用:它将一个通用指针 (ptr
) 转换为其对应的共享内存空间下的地址值。
原理:在 PTX 中,不同的内存空间(全局、共享、本地等)有独立的地址空间。一个通用(generic)的 void*
指针不能直接用于 ldmatrix
的 shared
操作。这个函数执行必要的位操作,提取出专用于共享内存地址空间的地址比特位。
"l"
: 这是一个约束修饰符。
l
location register 之意。表示一个 32 位的专用寄存器,通常用于存储地址**。这与通用寄存器 r
略有不同,编译器知道这个寄存器将用于寻址。
作用:告诉编译器:“计算 __cvta_generic_to_shared(ptr)
这个表达式的值,并将其放入一个专用的地址寄存器中,然后在汇编模板中用 %2
来引用这个寄存器。”
2.2.4. volatile
关键字
防止编译器优化掉这条汇编指令(例如,因为它看起来没有使用输出 x
),或者将其移出循环。确保指令严格按照代码中的位置和执行次数运行。
2.3. 函数功能总结
这个 ldmatrix_x2
函数的功能是:
让一个 Warp(32 个线程)协同工作,从共享内存中 ptr
所指的、16 字节对齐的地址开始,加载 2 个连续的 8x8 矩阵(每个元素 2 字节)。数据在加载过程中会被重新排列(转置)。加载完成后,每个线程会获得 8 字节(2 个 unsigned int
)的数据,存储在其 x[0]
和 x[1]
中。
这些数据通常是更大矩阵乘法操作中的一个小块(Tile)。每个线程持有的 x[0]
和 x[1]
是转置后矩阵的一小部分,它们的形式非常适合直接作为输入喂给后续的 mma
(矩阵乘加)指令,从而实现极其高效的矩阵计算。
注意事项:
调用约定:这个函数必须由整个 Warp 的线程同时调用,且
ptr
的值在 Warp 内必须一致(通常是通过广播获得)。对齐:
ptr
必须是 16 字节对齐的,否则行为未定义。数据布局:共享内存中的数据必须按照
ldmatrix
指令所期望的布局进行排列,这通常由之前的数据存储步骤(例如使用st.shared.v2.b32
之类的指令)来保证。
这个函数是手动优化 CUDA 核函数、充分发挥 Tensor Core 性能的典型代表。
3. ldmatrix 功能系统解析
CUDA PTX 中的 ldmatrix
指令是高效利用 Tensor Cores(张量核心)进行矩阵计算的关键所在。接着前面的具体实例,这里更为系统第介绍一下 ldmatrix 指令的原理用法。
3.1. 指令概述与原理
目的
ldmatrix
(Load Matrix)指令用于从一个线程束(Warp)内线程协同访问的连续共享内存区域中,高效地加载一个小的、密集的矩阵块(如 8x8),并将其转置后分布到该 Warp 中多个线程的寄存器中。
核心思想
Tensor Cores 执行的是 D = A * B + C
操作,其中 A、B、C、D 都是小矩阵。然而,全局内存或共享内存中的数据通常按行主序或列主序存储。ldmatrix
指令在数据从共享内存加载到寄存器的过程中,巧妙地完成了数据重排(转置),使得数据在寄存器中的布局恰好符合 Tensor Cores 所期望的输入格式,从而避免了显式的转置操作,极大提升了效率。
工作原理
一个 Warp(32 个线程)共同协作,从共享内存中读取一片连续的数据。每个线程负责读取数据的一部分。指令会自动地将这些数据重新组织(转置),并存入指定线程的指定寄存器。最终,整个 Warp 的寄存器合在一起,就构成了一个完整的、经过转置的矩阵。
3.2. 指令语法格式
完整的 PTX 语法如下:
ldmatrix.sync.aligned.{num}{.trans}{.ss}.type [rd1, rd2, ...], [rs1, rs2];
// 或者更常见的格式,指定矩阵形状:
ldmatrix.sync.aligned.shape.{num}{.trans}{.ss}.rspace [rd1, rd2, ...], [rs];
3.3. 指令中各域详解
.sync
(Synchronization)
作用
指定这是一个Warp-level 同步指令。指令的执行会涉及 Warp 中所有活跃线程的协同操作。.sync
后缀确保所有线程在逻辑上同时参与此次加载。
可选值
在较新的架构中,可以指定 .sync.syncid
以实现更细粒度的同步,但通常直接使用 .sync
。
.aligned
(Alignment)
作用
指定共享内存的源地址必须是 16 字节对齐的。这是为了满足内存子系统的高效访问要求。如果地址未对齐,执行结果将是未定义的。
注意
这是一个强制要求,不是可选项。你必须确保传入的共享内存指针是 16 字节对齐的。
.{num}
(Number of Matrices)
作用
指定一次指令调用要加载的矩阵数量。
可选值
.1
:加载 1 个矩阵;
.2
:加载 2 个矩阵;.4
:加载 4 个矩阵;
影响
加载的矩阵数量直接决定了目标寄存器的数量。例如,加载一个 8x8x16 的矩阵(.m8n8
+ .x2
)需要 4 个寄存器(8*8*2/32/1
?更正:通常加载 1 个 .m8n8.x4
矩阵需要 8 个寄存器)。加载 .4
个矩阵就需要 4 倍数量的寄存器。
.{trans}
(Transposition)
作用
指定是否对加载的矩阵进行转置。
可选值
(空):不进行转置,按原样加载;
.trans
:对加载的矩阵进行转置;
这是关键
这个功能是为了适配 Tensor Cores 的输入。例如,在计算 A * B 时,可能需要将 B 矩阵转置后再输入给 Tensor Core。使用 .trans
可以在加载时一步完成,无需后续单独的转置指令。
.{ss}
(Element Size / Storage Spacing)
作用
指定源数据中每个矩阵元素的大小和存储间隔。
可选值
.x1
:8 位元素(如 char
, uint8_t
);.x2
:16 位元素(如 half
, __half
, short
)。这是用于 FP16 张量计算最常见的大小;
.x4
:32 位元素(如 float
, int
);
.{type}
/ .{rspace}
(Type / Resource Space)
作用
指定源数据所在的内存空间。
可选值
.shared
:源数据位于共享内存中。这是 ldmatrix
最常用、最主要的使用场景;
.global
:源数据位于全局内存中。(在某些架构上支持,但不如从共享内存加载高效);
.[rd1, rd2, ...]
(Destination Registers)
作用
目标操作数,是一个寄存器列表,用于接收加载来的矩阵数据。
要求
寄存器的数量取决于 {num}
, {ss}
和矩阵形状。例如,加载 1 个 8x8 的矩阵(.m8n8
),每个元素是 32位(.x4
),则需要 (8 * 8 * 4) / 32 = 8
个 32 位寄存器;
寄存器必须是 32 位宽的(例如 %r0
, %f1
);
列表中的寄存器必须是连续的;
.[rs1, rs2]
/ [rs]
(Source Address)
作用
源操作数,是包含共享内存地址的寄存器。
要求
通常是一个包含 32 位地址的寄存器(例如 %r0
);
该地址必须指向共享内存,并且必须是 16 字节对齐的(由 .aligned
保证);
.{shape}
(Matrix Shape - 替代方案)
作用
另一种语法是明确指定矩阵的形状,这通常更直观。
可选值
.m8n8
:加载一个 8x8 的矩阵。这是最常用的形状;
.m8n8k4
等:用于更复杂的加载模式,但 .m8n8
是基础;
3.4. 用法示例与解释
假设我们要从共享内存加载一个 8x8 的 FP16 矩阵,并对其进行转置,然后分布到寄存器中。
PTX 代码:
ldmatrix.sync.aligned.m8n8.x2.trans.shared.b16 {%0, %1, %2, %3}, [%4];
分解:
.sync.aligned
:Warp 同步且地址对齐;
.m8n8
:加载 8x8 的矩阵;
.x2
:源元素是 16 位(FP16);
.trans
:加载时进行转置;
.shared.b16
:从共享内存以 16 字节的访问模式读取;
{%0, %1, %2, %3}
:需要 4 个 32 位目标寄存器;
*计算:一个 8x8 FP16 矩阵总大小 = 8 * 8 * 2字节 = 128 字节。一个 Warp 有 32 个线程,每个线程负责 128 / 32 = 4 字节的数据。一个 32 位寄存器正好是 4 字节,所以每个线程需要 1 个寄存器。但为什么这里有 4 个?实际上,ldmatrix
指令的寄存器列表是每个线程持有的寄存器数量?不,更准确的说法是:这条指令为整个 Warp 指定了 4 个连续的寄存器,但每个线程看到的是这些寄存器中的不同部分。通常,加载一个 .m8n8.x2
矩阵需要 4 个目标寄存器。
[%4]
:源地址寄存器,其值是一个 16 字节对齐的共享内存地址;
在 CUDA C++ 中的内联汇编用法:
__shared__ half smem_buffer[64]; // 8x8 FP16 矩阵
asm volatile (
"ldmatrix.sync.aligned.m8n8.x2.trans.shared.b16 {%0, %1, %2, %3}, [%4];"
: "=r"(reg0), "=r"(reg1), "=r"(reg2), "=r"(reg3) // 4个输出寄存器
: "r"(smem_buffer) // 输入:共享内存地址
// 可能还需要 clobber 列表,但有时可省略
);
3.5. 小结
ldmatrix
是一条极其强大的指令,它将数据加载和数据重排(转置) 两个耗时的操作合并为一条高效的硬件指令。它的设计完美契合了 Tensor Cores 的工作方式,是实现高性能矩阵乘法(尤其是深度学习推理和训练)的核心原语之一。理解其各个参数的含义对于在 PTX 或 CUDA 内联汇编中正确使用它至关重要。
4. 附录
4.1. 示例的 ptx生成
生成 ptx 文件:
nvcc -ptx -lineinfo -std=c++17 -arch=native main.cu -o main.ptx
或者不带源码行号
$ nvcc -ptx --gpu-architecture=sm_120 main.cu -o main_sm_120.ptx
4.2. m8n8.x4 的示例
//nvcc -g -lineinfo -std=c++17 -arch=native main.cu -o main
#include <iostream>
#include <thrust/device_vector.h>
/*
ldmatrix.sync.aligned.shape.num{.trans}{.ss}.type r, [p];
.shape = {.m8n8};
.num = {.x1, .x2, .x4};
.ss = {.shared{::cta}};
.type = {.b16};
*/
__device__
void ldmatrix_x2(unsigned int (&x)[4], const void* ptr){
asm volatile("ldmatrix.sync.aligned.m8n8.x4.shared.b16 {%0, %1, %2, %3}, [%4];"
: "=r"(x[0]), "=r"(x[1]), "=r"(x[2]), "=r"(x[3])
: "l"(__cvta_generic_to_shared(ptr)));
}
__global__
void mykernel(const int* loadOffsets, bool print){
alignas(16) __shared__ half A[128 * 16];
for(int i = threadIdx.x; i < 128*16; i += blockDim.x){
A[i] = i;
}
__syncthreads();
const int lane = threadIdx.x % 32;
unsigned int result[4];//one int, 2 half mem space
const int offset = loadOffsets[lane];
ldmatrix_x2(result, &A[offset]);
half2 loaded[4];
memcpy(&loaded[0], &result[0], sizeof(half2) * 4);
if(print){
for(int m = 0; m < 4; m++){
for(int t = 0; t < 32; t++){
//if(lane == t){
if(lane == t){
printf("[%2d]%4d %4d ",lane, int(loaded[m].x), int(loaded[m].y));
if(lane % 4 == 3){
printf("\n");
}
}
__syncwarp();
}
if(lane == 0){
printf("\n");
}
__syncwarp();
}
}
}
int main(){
thrust::device_vector<int> d_loadOffsets(32, 0);
for(int i = 0; i < 32; i++){
const int row = i % 8 + (i/16)*8; // row of m8n8
const int matrix = (i%16) / 8; // colum of m8n8
d_loadOffsets[i] = row * 16 + matrix * 8;
printf("%d ", row*16 + matrix*8);
}
printf("\n\n");
//thrust::host_vector<int> h_loadOffsets(32, 0);
int * hld = nullptr;
hld = (int*)malloc(32*sizeof(int));
cudaMemcpy(hld, d_loadOffsets.data().get(), sizeof(int)*32, cudaMemcpyDeviceToHost);
for(int i=0; i<32; i++)
printf("%d, ", hld[i]);
printf("\n");
mykernel<<<1,32>>>(d_loadOffsets.data().get(), true);
cudaDeviceSynchronize();
}
运行: