串行算法介绍
QR分解
本次实验选择的是瘦高型矩阵的QR分解,具体而言是针对32列矩阵的QR分解。
QR分解是一种基本的矩阵分解方法。
对于一个实矩阵:
A ∈ R m × n , ( m ≥ n ) A \in R^{m \times n}, (m \geq n)
A ∈ R m × n , ( m ≥ n )
可以将其分解为:
A = Q R A = QR
A = Q R
其中:
Q ∈ R m × n : 列 正 交 矩 阵 Q \in R^{m\times n}:列正交矩阵 Q ∈ R m × n : 列 正 交 矩 阵
R ∈ R n × n : 上 三 角 矩 阵 R\in R^{n\times n}:上三角矩阵 R ∈ R n × n : 上 三 角 矩 阵
算法步骤
采用MGS算法。
假设矩阵A的列为:
A = [ a 0 , a 1 , . . . , a n − 1 ] A = [a_0, a_1, ..., a_{n-1}]
A = [ a 0 , a 1 , . . . , a n − 1 ]
我们要逐列构造:
Q = [ q 0 , q 1 , . . . , q n − 1 ] Q = [q_0, q_1, ..., q_{n-1}]
Q = [ q 0 , q 1 , . . . , q n − 1 ]
对每一列a j a_j a j :
初始化,v = a_j. 即当前列作为起始向量。
去掉前面所有正交向量的分量,对所有k = 0 , 1 , … , j − 1 k = 0,1,\dots ,j-1 k = 0 , 1 , … , j − 1 (即前面所有已经算了的列):
计算v v v 在q k q_k q k 上的长度:R k , j = q k T v R_{k,j} = q_k^Tv R k , j = q k T v .
计算v v v 在q k q_k q k 上的投影向量:R k , j q k R_{k,j}q_k R k , j q k .
得到垂直于q k q_k q k 的v v v :v = v − R k , j q k v = v-R_{k,j}q_k v = v − R k , j q k .
计算当前向量的长度:R j , j = ∣ ∣ v ∣ ∣ 2 R_{j,j} = ||v||_2 R j , j = ∣ ∣ v ∣ ∣ 2 .
归一化得到新的正交向量:q j = v R j , j q_j = \frac{v}{R_{j,j}} q j = R j , j v .
代码实现
以256 × 36 256\times 36 2 5 6 × 3 6 的子矩阵做一次串行QR分解。
1 __global__ void tsqr_n32_float_simple(int m, float* A, int lda, float* R, int ldr)
m: 整个大矩阵的总行数,不是单个block的行数。
A: 指向输入矩阵的首地址,矩阵按列主序存储。
lda: 大矩阵的真实行跨度,即大矩阵的总行数。
R: 输出矩阵R的批量存储区,大小32 × 32 32\times 32 3 2 × 3 2 。
ldr: R的行数。
1 2 3 constexpr int tsqr_n32_block_size = 256; constexpr int n = 32; constexpr float eps = 1e-12f;
tsqr_n32_block_size: 每个QR子问题的矩阵大小为256.
n: 本文是针对瘦高型矩阵,具体而言是针对32列矩阵写的Kernel.
eps: 判断某一列是否退化的标准,如果某列正交化后范数太小,就认为这一列数值上接近0,不再归一化,而是直接置为0.
1 const int bx = blockIdx.x;
确定当前block处理哪个矩阵。
例如:bx = 0:处理第 0 个 256 × 32 256\times 32 2 5 6 × 3 2 ;bx = 1:处理第 1 个 256 × 32 256\times 32 2 5 6 × 3 2 .
1 2 3 if (threadIdx.x != 0 || threadIdx.y != 0) { return; }
限制一个block中只有一个线程计算,确保串行。
也就是说,对于大小为256 × 32 256\times 32 2 5 6 × 3 2 的子矩阵而言,只有一个线程参与计算,所有循环都是这个线程自己做。
1 2 A += bx * tsqr_n32_block_size; R += bx * (n * n);
移动全局指针,把他移动到当前block处理数据的起点。
1 2 const int block_size = min(tsqr_n32_block_size, m - bx * tsqr_n32_block_size); if (block_size <= 0) return;
考虑大矩阵行数不为 256 256 2 5 6 的整倍数时,采用最后一组剩余的行数作为块的行数。
1 2 3 4 5 for (int j = 0; j < n; ++j) { for (int i = 0; i < n; ++i) { R[i + j * ldr] = 0.0f; } }
健壮性,清空R矩阵。
R是列主序访问,所以i + j × l d r i+j\times ldr i + j × l d r 表示R[i, j].
1 float v[tsqr_n32_block_size];
这是一个临时向量,用来存储正在处理的一列。
在后续操作中:
原始列先拷贝到v.
在v上逐步减去投影。
归一化后写回A.
1 for (int j = 0; j < n; ++j)
这是QR分解的主循环,我们对A采用列主序处理,所以遍历32次,每次处理256维的向量。
1 2 3 for (int i = 0; i < block_size; ++i) { v[i] = A[i + j * lda]; }
复制处理列到v.
1 2 3 for (int k = 0; k < j; ++k) { ... }
遍历当前列之前的列,用之前已经算完的正交向量,去掉当前列在这些方向上的分量,得到与之前所有正交向量正交的分量。
1 2 3 4 float dot = 0.0f; for (int i = 0; i < block_size; ++i) { dot += A[i + k * lda] * v[i]; }
计算投影系数,即计算R k , j = q k T v R_{k,j} = q^T_kv R k , j = q k T v .
需要注意的是,这里的A[i + k * lda]已经不是原始的数值,而是已经完成处理后的列。
系数写入R,这里天然上三角,因为 k<j.
1 2 3 for (int i = 0; i < block_size; ++i) { v[i] -= dot * A[i + k * lda]; }
从v中减去这个方向的分量,即v ← v − R k , j q k v\leftarrow v-R_{k,j}q_k v ← v − R k , j q k .
此时v已经和第k列正交了。
1 2 3 4 5 6 float norm2 = 0.0f; for (int i = 0; i < block_size; ++i) { norm2 += v[i] * v[i]; } float norm = sqrtf(norm2); R[j + j * ldr] = norm;
计算正交化向量的范数,即∣ ∣ v ∣ ∣ 2 = ∑ i v i 2 ||v||_2=\sqrt{\sum_iv_i^2} ∣ ∣ v ∣ ∣ 2 = ∑ i v i 2 .
把它存到R[j,j],这是归一化前的长度。
1 2 3 4 5 6 if (norm > eps) { float inv = 1.0f / norm; for (int i = 0; i < block_size; ++i) { A[i + j * lda] = v[i] * inv; } }
当norm比eps大的时候,就归一化q j = v ∣ ∣ v ∣ ∣ q_j = \frac{v}{||v||} q j = ∣ ∣ v ∣ ∣ v .
结果直接写回A的当前列,此时A逐步变成Q.
如果norm<=eps则v非常接近零向量,此时做除法会很危险,面对这种情况,我直接置为0.
kernel的总体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 __global__ void tsqr_n32_float_simple(int m, float* A, int lda, float* R, int ldr) { constexpr int tsqr_n32_block_size = 256; constexpr int n = 32; constexpr float eps = 1e-12f; const int bx = blockIdx.x; if (threadIdx.x != 0 || threadIdx.y != 0) { return; } A += bx * tsqr_n32_block_size; R += bx * (n * n); const int block_size = min(tsqr_n32_block_size, m - bx * tsqr_n32_block_size); if (block_size <= 0) return; for (int j = 0; j < n; ++j) { for (int i = 0; i < n; ++i) { R[i + j * ldr] = 0.0f; } } float v[tsqr_n32_block_size]; for (int j = 0; j < n; ++j) { for (int i = 0; i < block_size; ++i) { v[i] = A[i + j * lda]; } for (int k = 0; k < j; ++k) { float dot = 0.0f; for (int i = 0; i < block_size; ++i) { dot += A[i + k * lda] * v[i]; } R[k + j * ldr] = dot; for (int i = 0; i < block_size; ++i) { v[i] -= dot * A[i + k * lda]; } } float norm2 = 0.0f; for (int i = 0; i < block_size; ++i) { norm2 += v[i] * v[i]; } float norm = sqrtf(norm2); R[j + j * ldr] = norm; if (norm > eps) { float inv = 1.0f / norm; for (int i = 0; i < block_size; ++i) { A[i + j * lda] = v[i] * inv; } } else { for (int i = 0; i < block_size; ++i) { A[i + j * lda] = 0.0f; } } } }
不过,该算法实际上还是有并行处理,只不过是以block同时为最大与最小单元并行,效率很低。
并行算法介绍
算法步骤
先把当前block处理的小矩阵搬运到片上临时空间。
逐列构造Householder向量。
用该反射向量去消掉当前列下面的元素,并同步更新右边所有列。
全部列处理完后,矩阵成为上三角矩阵,得到R.
在根据前面保存的Householder向量,反向累计恢复Q.
最后把Q写回到原矩阵位置。
取出子矩阵
第一步是把子矩阵A b l o c k ∈ R m b × 32 A_{block} \in R^{m_b\times 32} A b l o c k ∈ R m b × 3 2 拷贝到局部工作区,所有QR操作都在这块区域进行。
构造Householder反射
对子矩阵的每一列,都做一次反射构造。
以col列为例,目标是把这一列从第col行往下的部分,变成一个只有首元素非零,其余元素都为零的形式:
H x = [ ± ∥ x ∥ 0 ⋮ 0 ] Hx = \begin{bmatrix} \pm\|x\| \\ 0 \\ \vdots \\ 0 \end{bmatrix}
H x = ⎣ ⎢ ⎢ ⎢ ⎡ ± ∥ x ∥ 0 ⋮ 0 ⎦ ⎥ ⎥ ⎥ ⎤
直白点说,就是把当前列下面的元素全部变为0.
再直白一些,处理的是x = A ( c o l : m b − 1 , c o l ) x = A(col:m_b-1, col) x = A ( c o l : m b − 1 , c o l ) 的数据,而不是一整列。
反射向量的构造如下:
设当前列尾部向量为x x x ,计算它的2范数∣ ∣ x ∣ ∣ ||x|| ∣ ∣ x ∣ ∣ .
计算α \alpha α : α = − s i g n ( x 0 ) ∣ ∣ x ∣ ∣ \alpha = -sign(x_0)||x|| α = − s i g n ( x 0 ) ∣ ∣ x ∣ ∣ .
构造向量u = x u=x u = x ,并修改u u u 的第一个数值:u 0 = u 0 − α u_0 = u_0 - \alpha u 0 = u 0 − α .
归一化:v = u ∣ ∣ u ∣ ∣ v = \frac{u}{||u||} v = ∣ ∣ u ∣ ∣ u ,H = I − 2 v v T H = I - 2vv^T H = I − 2 v v T .
但是需要指出的是,在代码里,实现的是H = I − u u T H=I-uu^T H = I − u u T ,省一个除法,另外把scale融进去的话,应该还会减少一个指令。
在上述步骤完成后,当前col列对角线一下的元素从原始数据变成了Householder向量的存储区,且对角线位置对应的R(col, col)的值已经确定。
所以,子矩阵在此刻,不再是单纯的保留了原始数据,也不存完整的Q,而是上三角+Householder向量。
用当前反射更新右边所有列
构造出第col列的反射后,并不会只处理这一列,还要把这个反射施加到右边所有剩余列:
A ( : , j ) ← H A ( : , j ) , j > c o l A(:,j)\leftarrow HA(:,j), j>col
A ( : , j ) ← H A ( : , j ) , j > c o l
假设我只把当前列下半打成0,但右边不更新,其实整个矩阵并没有完成等价变化。
QR分解的本质是对整个矩阵左乘一系列反射矩阵,即R = H 31 … H 1 H 0 A R = H_{31}\dots H_{1}H_{0}A R = H 3 1 … H 1 H 0 A .
如果当前Householder向量记为u u u ,那么对任意后续列x x x ,更新公式是x ← x − u ( u T x ) x\leftarrow x-u(u^Tx) x ← x − u ( u T x ) .
和之前串行算法用的MGS比起来,这里是施加一个整体反射,而MGS是逐个去掉旧的正基方向。
再说直白一些:
计算了当前列反射向量与目标列的内积
对目标列更新。
目标列完成对应的正交变换。
提取R
当所有列的Householder变换都做完了,局部矩阵的上三角部分就已经是最终的R R R .
所以,这一阶段只需要:
读出矩阵的上三角区域
写入R R R
R R R 下三角部分置为0.
根据Householder向量反向累积Q
Q = H 0 H 1 … H 31 Q = H_0 H_1 \dots H_{31} Q = H 0 H 1 … H 3 1
恢复Q Q Q 的方法是:
从单位矩阵的列向量出发;
按照Householder反射的顺序,把这些反射依次作用;
最后得到Q Q Q 的每一列。
即,如果我有一组标准基向量:e 0 , e 1 , … , e 31 e_0, e_1, \dots , e_{31} e 0 , e 1 , … , e 3 1 ,对每个基向量,依次施加所有Householder反射;得到Q Q Q 的列向量。
q j = H 0 H 1 … H 31 e j q_j = H_0H_1\dots H_{31}e_j
q j = H 0 H 1 … H 3 1 e j
经过反向累积,得到了显示的Q Q Q .
把Q写回输出位置
前面恢复的Q协会原矩阵A对应的小矩阵block区域。
结果上看,输入的A,被覆盖为Q,R单独输出上三角矩阵。
代码实现
1 __global__ void tsqr_n32_float(int m, float* A, int lda, float* R, int ldr)
这个kernel的目标是,在A A A 中写回Q Q Q ;R R R 中写回32 × 32 32\times 32 3 2 × 3 2 上三角矩阵。
1 2 3 4 5 constexpr int tsqr_n32_block_size = 256; constexpr int tsqr_n32_n = 32; constexpr int tsqr_n32_data_num_per_thread = 8; constexpr int warp_size = 32; constexpr float epsilon = 1e-4;
tsqr_n32_blocksize: 每个block处理的行数。
tsqr_n32_n: 这是在本次实验中限制的矩阵列数32,也是每个block处理的列数。
tsqr_n32_data_num_per_thread: 每个线程处理处理8个元素。
warp_size: 一个warp的线程数。
epsilon: 用来判断是否退化置为0.
需要注意的是,一个block有32个warp,所以每个warp处理一列,共处理32列;一个warp有32个线程,处理256行1列元素,即256个元素,所以一个线程处理8个元素。
1 2 3 const int lane_id = threadIdx.x; const int warp_id = threadIdx.y; const int bx = blockIdx.x;
lane_id: warp内线程的编号,即0~31.
warp_id: 第几个warp,范围0~31.
bx: 第几个block.
需要注意的是,这里的warp_id也表示为,当warp处理的第几列;而land_id,也会参与后续单列中,每个线程的起始位置。
1 2 A += bx * tsqr_n32_block_size; R += bx * (tsqr_n32_n * tsqr_n32_n);
移动全局指针:
A: 整个大矩阵是列主序存储,每个block只处理连续的256行。
B: 每个block有独立的32 × 32 32\times 32 3 2 × 3 2 输出矩阵,所以当前block的R R R 起始地址往后挪对应数量的元素。
1 int block_size = min(tsqr_n32_block_size, m - bx * tsqr_n32_block_size);
与串行实现相同,兼容非256整倍数行数的情形。
1 __shared__ float shared_A[tsqr_n32_block_size * tsqr_n32_n];
申请一个256 × 32 256\times 32 2 5 6 × 3 2 的共享内存数组。
所有后续的Householder分解、列更新、Q恢复,基本都在这块共享内存上完成。
1 2 3 4 5 6 7 8 #pragma unroll for (auto i = 0; i < tsqr_n32_data_num_per_thread; ++i) { auto row_idx = lane_id + i * warp_size; if (row_idx < block_size) { shared_A[row_idx + warp_id * tsqr_n32_block_size] = A[row_idx + warp_id * lda]; } } __syncthreads();
接下来,将当前block对应的子矩阵的数据从A搬运到shared_A:
每个线程负责8个元素,所以循环8次;
考虑线程在warp中的位置:每组warp搬运256个元素,行号从0~256,每个线程负责8个位置,即l a n e , l a n e + 32 , … , l a n e + 224 lane, lane+32, \dots, lane+224 l a n e , l a n e + 3 2 , … , l a n e + 2 2 4 .
接着考虑整个warp负责的block的位置:每个warp负责一整列,直接跨w a r p i d ∗ l d a warp_id * lda w a r p i d ∗ l d a .
最后使用__syncthreads()保证所有线程都完成搬运,再执行后续计算。
1 float q[tsqr_n32_data_num_per_thread];
为每个线程申请一个长度8的寄存器数组。
在后续计算中,它将:
构造反射向量时,临时存储列片段;
更新右侧列时,临时存reflector对应的分量;
恢复Q,存Q列的局部片段。
1 for (auto col = 0; col < tsqr_n32_n; ++col)
接下来要做Householder分解,每次循环处理一列,从左到右依次处理:
构造该列的Householder向量。
用这个反射去消掉该列下面元素。
把同样的反射作用到右边所有列。
1 2 3 4 5 float nu = 0.f; if (warp_id == col) { ... }
对warp处理的列进行限制。
1 2 3 4 5 6 7 8 #pragma unroll for (auto i = 0; i < tsqr_n32_data_num_per_thread; ++i) { auto row_idx = lane_id + i * warp_size; if (row_idx >= col && row_idx < block_size) { q[i] = shared_A[row_idx + col * tsqr_n32_block_size]; nu += q[i] * q[i]; } }
和串行的算法类似,我们只关注尾部向量。
1 float norm_square = warp_all_reduce_sum(nu);
需要注意的是,由于每个线程只算了自己的8个元素的平方和,所以要以warp为单位,进行规约,求整列尾部向量的∣ ∣ x ∣ ∣ 2 ||x||^2 ∣ ∣ x ∣ ∣ 2 .
warp_all_reduce_sum 在后续单独说明
1 2 3 4 5 if (norm_square > epsilon * epsilon) { float norm = __fsqrt_rn(norm_square); float scale = 1.f / norm; ... }
和串行算法类似,对范数大小进行验证,避免范数过小。
当范数足够大时,构造reflector.
n o r m = ∣ ∣ x ∣ ∣ s c a l e = 1 / ∣ ∣ x ∣ ∣ norm = ||x|| \\
scale = 1/||x||
n o r m = ∣ ∣ x ∣ ∣ s c a l e = 1 / ∣ ∣ x ∣ ∣
1 2 3 4 5 6 7 #pragma unroll for (auto i = 0; i < tsqr_n32_data_num_per_thread; ++i) { auto row_idx = lane_id + i * warp_size; if (row_idx >= col && row_idx < block_size) { q[i] *= scale; } }
对当前向量归一化,这一步完成后q存的是x / ∣ ∣ x ∣ ∣ x/||x|| x / ∣ ∣ x ∣ ∣ ,也就是单位化后的当前列尾部向量。
1 2 3 4 5 6 7 float u1 = 0.f; auto thread_off = col / warp_size; if (lane_id == col % warp_size) { q[thread_off] += (q[thread_off] >= 0) ? 1.f : -1.f; u1 = q[thread_off]; R[col + ldr * col] = (u1 >= 0) ? -norm : norm; }
这几步是为了修改首元素,进而构造Householder方向。
其中col/warp_size是为了健壮性而写的:首元素不总是在q[0],一旦遇到超过warp_size的列数,就需要除法;但是本次实验针对的是固定的32列瘦高矩阵,所以thread_off永远为0.
为了确保reflector只修改首元素,必须找到哪个线程负责这个元素:首元素位于全局行号col,这个行号恰好落在col % 32,所以限制land_id与之相等。
随后执行的计算如下:
x ^ 0 ← x ^ 0 + s i g n ( x ^ 0 ) \hat{x}_0 \leftarrow \hat{x}_0 + sign(\hat{x}_0)
x ^ 0 ← x ^ 0 + s i g n ( x ^ 0 )
以构造Householder方向。
Householder反射后该列变为[ ± ∣ ∣ x ∣ ∣ , 0 , … ] T [\pm ||x||, 0, \dots ]^T [ ± ∣ ∣ x ∣ ∣ , 0 , … ] T .
把这里的对角元素写入R(col, col).
1 u1 = __shfl_sync(0xffffffff, u1, col % warp_size);
当一个线程更新了首元素u1后,通过warp shuffle广播给同一warp内其他线程。
1 scale = __frsqrt_rn(fabs(u1));
和串行算法类似,为了节省计算量,采用H = I − u u T H = I - uu^T H = I − u u T . 为此需要把刚构造出的向量再做一次缩放,把常数因子塞进u里。
1 2 3 4 5 6 7 #pragma unroll for (auto i = 0; i < tsqr_n32_data_num_per_thread; ++i) { auto row_idx = lane_id + i * warp_size; if (row_idx >= col && row_idx < block_size) { shared_A[row_idx + col * tsqr_n32_block_size] = q[i] * scale; } }
把Householder向量写回shared_a当前列尾部。
这一步结束后,shared_A(:, col)从col往下部分,不再是原矩阵数据,而是reflector向量u.
在之前讨论的是没有退化的处理,现在说明退化的处理:
1 2 3 4 5 6 7 8 9 10 } else { if (lane_id == col) { R[col + ldr * col] = 0.f; } #pragma unroll for (auto i = 0; i < tsqr_n32_data_num_per_thread; ++i) { auto row_idx = lane_id + i * warp_size; if (row_idx >= col && row_idx < block_size) { shared_A[row_idx + col * tsqr_n32_block_size] = 0.f; } } }
尾部向量近似为0,此刻无法构造有效的reflector,此时将R(col, col)置为0,reflector置为0,相当于跳过这一次反射。
当前列是warp_id == col的warp写到了shared_a(:, col). 接下来其他warp要用它取更新右侧的列,在此之前进行一个block级的同步。
1 if (col < warp_id) { ... }
确定只有当前列的右侧更新,例如第五例reflector算完,就更新6~31列。
1 2 3 4 5 6 7 8 9 10 float nu = 0.f; #pragma unroll for (auto i = 0; i < tsqr_n32_data_num_per_thread; ++i) { auto row_idx = lane_id + i * warp_size; if (row_idx >= col && row_idx < block_size) { q[i] = shared_A[row_idx + col * tsqr_n32_block_size]; nu += q[i] * shared_A[row_idx + warp_id * tsqr_n32_block_size]; } } float utx = warp_all_reduce_sum(nu);
q[i]: 读当前reflector向量u对应的单个数字.
shared_A[row_idx + warp_id * tsqr_n32_block_size]: 待更新的列的单个数字。
所以累计的结果为u T x u^Tx u T x 。
1 2 3 4 5 6 7 #pragma unroll for (auto i = 0; i < tsqr_n32_data_num_per_thread; ++i) { auto row_idx = lane_id + i * warp_size; if (row_idx >= col && row_idx < block_size) { shared_A[row_idx + warp_id * tsqr_n32_block_size] -= utx * q[i]; } }
更新目标列:x ← x − u ( u T x ) x\leftarrow x - u(u^Tx) x ← x − u ( u T x ) .
主循环全部结束后,再做一次block同步。
随后准备利用reflector恢复Q.
1 2 3 4 5 6 7 if (lane_id < warp_id) { R[lane_id + ldr * warp_id] = shared_A[lane_id + warp_id * tsqr_n32_block_size]; shared_A[lane_id + warp_id * tsqr_n32_block_size] = 0.f; } if (lane_id > warp_id) { R[lane_id + ldr * warp_id] = 0.f; }
当lane_id<warp_id是,处于上三角区域,拷贝到R.
否则,直接写0.
另外,顺便把shared_A的上三角清零,因为,后面恢复Q只需要reflector信息,清个零更干净。
1 2 3 4 5 6 #pragma unroll for (auto i = 0; i < tsqr_n32_data_num_per_thread; ++i) { auto row_idx = lane_id + i * warp_size; q[i] = (row_idx == warp_id) ? 1.f : 0.f; } __syncwarp();
为了恢复Q,每个warp先把自己初始化成标准基向量,例如warp0对应e 0 e_0 e 0 .
也就是说,每个warp手里存Q的某一列初始值。
1 2 3 4 for (auto col = tsqr_n32_n - 1; col >= 0; --col) { ... }
恢复Q时,reflector按逆序应用,即Q = H 0 H 1 … H 31 Q = H_0H_1\dots H_{31} Q = H 0 H 1 … H 3 1 .
1 if (warp_id >= col) { ... }
第col个reflector只作用于当前col以后的Q列。
1 2 3 4 5 6 7 8 9 float nu = 0.f; #pragma unroll for (auto i = 0; i < tsqr_n32_data_num_per_thread; ++i) { auto row_idx = lane_id + i * warp_size; if (row_idx < block_size) { nu += q[i] * shared_A[row_idx + col * tsqr_n32_block_size]; } } float utq = warp_all_reduce_sum(nu);
q[i]是当前warp正在恢复的Q的一列。
shared_A[row_idx + col * tsqr_n32_block_size] 是第col个reflector向量。
也就是求u T q u^Tq u T q
1 2 3 4 5 6 7 8 #pragma unroll for (auto i = 0; i < tsqr_n32_data_num_per_thread; ++i) { auto row_idx = lane_id + i * warp_size; if (row_idx < block_size) { q[i] -= utq * shared_A[row_idx + col * tsqr_n32_block_size]; } } __syncwarp();
计算:q ← q − u ( u T q ) q\leftarrow q-u(u^Tq) q ← q − u ( u T q ) .
直白点就是把第col个reflector作用到当前Q列。
1 2 3 4 5 6 7 #pragma unroll for (auto i = 0; i < tsqr_n32_data_num_per_thread; ++i) { auto row_idx = lane_id + i * warp_size; if (row_idx < block_size) { A[row_idx + warp_id * lda] = q[i]; } }
写回A,和最初写入shared_A类似。
测试结果
串行算法:
并行算法:
可以看到后者效率非常高,一方面不仅实现了线程级的并行;此外,Householder更新是列操作,适合warp内做归约计算内积,并并行更新,后者具有更高的并行度,更少的全局内存访存,更高效的共享内存使用。
整体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 #include <iostream> #include <vector> #include <cmath> #include <random> #include <cuda_runtime.h> #define CHECK_CUDA(call) { \ cudaError_t err = call; \ if (err != cudaSuccess) { \ std::cerr << "CUDA Error: " << cudaGetErrorString(err) << " at line " << __LINE__ << std::endl; \ exit(1); \ } \ } template <typename T> static __inline__ __device__ T warp_all_reduce_sum(T val) { for (int mask = warpSize / 2; mask > 0; mask /= 2) { val += __shfl_xor_sync(0xffffffff, val, mask); } return val; } __global__ void tsqr_n32_float(int m, float* A, int lda, float* R, int ldr) { constexpr int tsqr_n32_block_size = 256; constexpr int tsqr_n32_n = 32; constexpr int tsqr_n32_data_num_per_thread = 8; constexpr int warp_size = 32; constexpr float epsilon = 1e-4; const int lane_id = threadIdx.x; const int warp_id = threadIdx.y; const int bx = blockIdx.x; A += bx * tsqr_n32_block_size; R += bx * (tsqr_n32_n * tsqr_n32_n); int block_size = min(tsqr_n32_block_size, m - bx * tsqr_n32_block_size); __shared__ float shared_A[tsqr_n32_block_size * tsqr_n32_n]; #pragma unroll for (auto i = 0; i < tsqr_n32_data_num_per_thread; ++i) { auto row_idx = lane_id + i * warp_size; if (row_idx < block_size) { shared_A[row_idx + warp_id * tsqr_n32_block_size] = A[row_idx + warp_id * lda]; } } __syncthreads(); float q[tsqr_n32_data_num_per_thread]; for (auto col = 0; col < tsqr_n32_n; ++col) { float nu = 0.f; if (warp_id == col) { #pragma unroll for (auto i = 0; i < tsqr_n32_data_num_per_thread; ++i) { auto row_idx = lane_id + i * warp_size; if (row_idx >= col && row_idx < block_size) { q[i] = shared_A[row_idx + col * tsqr_n32_block_size]; nu += q[i] * q[i]; } } float norm_square = warp_all_reduce_sum(nu); if (norm_square > epsilon * epsilon) { float norm = __fsqrt_rn(norm_square); float scale = 1.f / norm; #pragma unroll for (auto i = 0; i < tsqr_n32_data_num_per_thread; ++i) { auto row_idx = lane_id + i * warp_size; if (row_idx >= col && row_idx < block_size) { q[i] *= scale; } } float u1 = 0.f; auto thread_off = col / warp_size; if (lane_id == col % warp_size) { q[thread_off] += (q[thread_off] >= 0) ? 1.f : -1.f; u1 = q[thread_off]; R[col + ldr * col] = (u1 >= 0) ? -norm : norm; } u1 = __shfl_sync(0xffffffff, u1, col % warp_size); scale = __frsqrt_rn(fabs(u1)); #pragma unroll for (auto i = 0; i < tsqr_n32_data_num_per_thread; ++i) { auto row_idx = lane_id + i * warp_size; if (row_idx >= col && row_idx < block_size) { shared_A[row_idx + col * tsqr_n32_block_size] = q[i] * scale; } } } else { if (lane_id == col) { R[col + ldr * col] = 0.f; } #pragma unroll for (auto i = 0; i < tsqr_n32_data_num_per_thread; ++i) { auto row_idx = lane_id + i * warp_size; if (row_idx >= col && row_idx < block_size) { shared_A[row_idx + col * tsqr_n32_block_size] = 0.f; } } } } __syncthreads(); if (col < warp_id) { float nu = 0.f; #pragma unroll for (auto i = 0; i < tsqr_n32_data_num_per_thread; ++i) { auto row_idx = lane_id + i * warp_size; if (row_idx >= col && row_idx < block_size) { q[i] = shared_A[row_idx + col * tsqr_n32_block_size]; nu += q[i] * shared_A[row_idx + warp_id * tsqr_n32_block_size]; } } float utx = warp_all_reduce_sum(nu); #pragma unroll for (auto i = 0; i < tsqr_n32_data_num_per_thread; ++i) { auto row_idx = lane_id + i * warp_size; if (row_idx >= col && row_idx < block_size) { shared_A[row_idx + warp_id * tsqr_n32_block_size] -= utx * q[i]; } } } } __syncthreads(); if (lane_id < warp_id) { R[lane_id + ldr * warp_id] = shared_A[lane_id + warp_id * tsqr_n32_block_size]; shared_A[lane_id + warp_id * tsqr_n32_block_size] = 0.f; } if (lane_id > warp_id) { R[lane_id + ldr * warp_id] = 0.f; } #pragma unroll for (auto i = 0; i < tsqr_n32_data_num_per_thread; ++i) { auto row_idx = lane_id + i * warp_size; q[i] = (row_idx == warp_id) ? 1.f : 0.f; } __syncwarp(); for (auto col = tsqr_n32_n - 1; col >= 0; --col) { if (warp_id >= col) { float nu = 0.f; #pragma unroll for (auto i = 0; i < tsqr_n32_data_num_per_thread; ++i) { auto row_idx = lane_id + i * warp_size; if (row_idx < block_size) { nu += q[i] * shared_A[row_idx + col * tsqr_n32_block_size]; } } float utq = warp_all_reduce_sum(nu); #pragma unroll for (auto i = 0; i < tsqr_n32_data_num_per_thread; ++i) { auto row_idx = lane_id + i * warp_size; if (row_idx < block_size) { q[i] -= utq * shared_A[row_idx + col * tsqr_n32_block_size]; } } __syncwarp(); } } #pragma unroll for (auto i = 0; i < tsqr_n32_data_num_per_thread; ++i) { auto row_idx = lane_id + i * warp_size; if (row_idx < block_size) { A[row_idx + warp_id * lda] = q[i]; } } } __global__ void tsqr_n32_float_simple(int m, float* A, int lda, float* R, int ldr) { constexpr int tsqr_n32_block_size = 256; constexpr int n = 32; constexpr float eps = 1e-12f; const int bx = blockIdx.x; if (threadIdx.x != 0 || threadIdx.y != 0) { return; } A += bx * tsqr_n32_block_size; R += bx * (n * n); const int block_size = min(tsqr_n32_block_size, m - bx * tsqr_n32_block_size); if (block_size <= 0) return; for (int j = 0; j < n; ++j) { for (int i = 0; i < n; ++i) { R[i + j * ldr] = 0.0f; } } float v[tsqr_n32_block_size]; for (int j = 0; j < n; ++j) { for (int i = 0; i < block_size; ++i) { v[i] = A[i + j * lda]; } for (int k = 0; k < j; ++k) { float dot = 0.0f; for (int i = 0; i < block_size; ++i) { dot += A[i + k * lda] * v[i]; } R[k + j * ldr] = dot; for (int i = 0; i < block_size; ++i) { v[i] -= dot * A[i + k * lda]; } } float norm2 = 0.0f; for (int i = 0; i < block_size; ++i) { norm2 += v[i] * v[i]; } float norm = sqrtf(norm2); R[j + j * ldr] = norm; if (norm > eps) { float inv = 1.0f / norm; for (int i = 0; i < block_size; ++i) { A[i + j * lda] = v[i] * inv; } } else { for (int i = 0; i < block_size; ++i) { A[i + j * lda] = 0.0f; } } } } void verify_qr_decomposition(const std::vector<float>& A_orig, const std::vector<float>& Q, const std::vector<float>& R, int m, int n) { float max_err_qtq = 0.0f; float max_err_qr = 0.0f; for (int i = 0; i < n; ++i) { for (int j = 0; j < n; ++j) { float sum = 0.0f; for (int k = 0; k < m; ++k) { sum += Q[k + i * m] * Q[k + j * m]; } float target = (i == j) ? 1.0f : 0.0f; max_err_qtq = std::max(max_err_qtq, std::abs(sum - target)); } } for (int i = 0; i < m; ++i) { for (int j = 0; j < n; ++j) { float sum = 0.0f; for (int k = 0; k <= j; ++k) { sum += Q[i + k * m] * R[k + j * n]; } max_err_qr = std::max(max_err_qr, std::abs(sum - A_orig[i + j * m])); } } std::cout << "-------------------------------------------\n"; std::cout << "验证结果 (Verification Results):\n"; std::cout << "-------------------------------------------\n"; std::cout << "Q^T * Q 误差 (Orthogonality Error) : " << max_err_qtq << "\n"; std::cout << "Q * R 误差 (Reconstruction Error): " << max_err_qr << "\n"; if (max_err_qtq < 1e-4 && max_err_qr < 1e-4) { std::cout << "=> 测试通过!(TEST PASSED!)\n"; } else { std::cout << "=> 测试失败!(TEST FAILED!)\n"; } } int main() { const int m_per_block = 256; // 每个 Block 处理的行数 const int n = 32; // 列数 const int num_blocks = 128; // 并发任务数 const int total_m = m_per_block * num_blocks; // 总行数 (32768) const int lda = total_m; std::cout << "===========================================" << std::endl; std::cout << "TSQR Batched Performance Test (RTX 4060)" << std::endl; std::cout << "Total Matrix: " << total_m << " x " << n << std::endl; std::cout << "Parallel Blocks: " << num_blocks << " (Each 256x32)" << std::endl; std::cout << "===========================================" << std::endl; size_t a_size = total_m * n * sizeof(float); size_t r_batch_size = num_blocks * n * n * sizeof(float); std::vector<float> h_A(total_m * n); std::mt19937 rng(1234); std::uniform_real_distribution<float> dist(-1.0f, 1.0f); for (int i = 0; i < h_A.size(); ++i) h_A[i] = dist(rng); std::vector<float> h_A_orig_first(m_per_block * n); for (int j = 0; j < n; ++j) { for (int i = 0; i < m_per_block; ++i) { h_A_orig_first[i + j * m_per_block] = h_A[i + j * total_m]; } } float *d_A, *d_R; CHECK_CUDA(cudaMalloc(&d_A, a_size)); CHECK_CUDA(cudaMalloc(&d_R, r_batch_size)); CHECK_CUDA(cudaMemcpy(d_A, h_A.data(), a_size, cudaMemcpyHostToDevice)); cudaEvent_t start, stop; CHECK_CUDA(cudaEventCreate(&start)); CHECK_CUDA(cudaEventCreate(&stop)); dim3 block(32, 32); dim3 grid(num_blocks); std::cout << "Scaling up: Launching " << num_blocks << " blocks..." << std::endl; CHECK_CUDA(cudaEventRecord(start)); // tsqr_n32_float_simple<<<grid, block>>>(total_m, d_A, lda, d_R, n); tsqr_n32_float<<<grid, block>>>(total_m, d_A, lda, d_R, n); CHECK_CUDA(cudaEventRecord(stop)); CHECK_CUDA(cudaEventSynchronize(stop)); float ms = 0; CHECK_CUDA(cudaEventElapsedTime(&ms, start, stop)); double single_block_flops = 2.0 * m_per_block * n * n - (2.0/3.0) * n * n * n; double total_flops = single_block_flops * num_blocks; double gflops = (total_flops * 1e-9) / (ms * 1e-3); printf("-------------------------------------------\n"); printf("统计结果 (Performance Statistics):\n"); printf("-------------------------------------------\n"); printf("Kernel 耗时 : %.4f ms\n", ms); printf("总计算量 : %.2f MFLOPs\n", total_flops / 1e6); printf("吞吐性能 : %.2f GFLOPS\n", gflops); std::vector<float> h_Q_first(m_per_block * n); std::vector<float> h_R_first(n * n); std::vector<float> h_Q_all(total_m * n); CHECK_CUDA(cudaMemcpy(h_Q_all.data(), d_A, a_size, cudaMemcpyDeviceToHost)); CHECK_CUDA(cudaMemcpy(h_R_first.data(), d_R, n * n * sizeof(float), cudaMemcpyDeviceToHost)); for (int j = 0; j < n; ++j) { for (int i = 0; i < m_per_block; ++i) { h_Q_first[i + j * m_per_block] = h_Q_all[i + j * total_m]; } } verify_qr_decomposition(h_A_orig_first, h_Q_first, h_R_first, m_per_block, n); CHECK_CUDA(cudaEventDestroy(start)); CHECK_CUDA(cudaEventDestroy(stop)); cudaFree(d_A); cudaFree(d_R); return 0; }