contributed by <twzjwang
>,<rayleigh0407
>
GitHub: matrix_oo
Task: 比照 B10: matrix 要求,完整實作多種加速矩陣乘法的演算法,伴隨相關的 SIMD 改善機制
input matrix A(n*m), B(m*p)
creat matrix C(n*p)
for i = 1 : n
for j = 1 : p
sum = 0
for k = 1 : m
sum = sum + A[i][k] * B[k][j]
C[i][j] = sum
return C
Naive | Strassen algo |
---|---|
想驗證
是否成立,直觀的想法為先算出 ,再判斷
時間複雜度為~
Freivalds' algorithm 可加速至
:
( ) r( )
=>
:
( ) (Br)( )
=>
e.g.:
case:
, ,
隨機產生可能為 , , ,
運算結果皆為
=> 錯誤機率為 0case
:
, ,
隨機產生可能為 , , ,
運算
其中, 算出
, 算出
=> 錯誤機率為 0.5
(有一半機會當卻算出 )
https://en.wikipedia.org/wiki/Matrix_multiplication_algorithm#Parallel_and_distributed_algorithms
驗證方法
algo = matrix_providers[0];
if (!algo->mul(&L_Br, &B, &r)) {
printf("B x r error!\n");
continue;
}
if (!algo->mul(&L_ABr, &A, &L_Br)) {
printf("A x Br error!\n");
continue;
}
if (!algo->mul(&R_Cr, &C, &r)) {
printf("C x r error!\n");
continue;
}
if (!algo->equal(&L_ABr, &R_Cr)) {
printf("ABr and Cr are not equal!\n");
break;
}
改善
int flag = 1;
algo = matrix_providers[0];
for (int i = 0; i < 10 && flag; i++) {
if (!algo->mul(&L_Br, &B, &r)) {
printf("B x r error!\n");
flag = 0;
continue;
}
if (!algo->mul(&L_ABr, &A, &L_Br)) {
printf("A x Br error!\n");
flag = 0;
continue;
}
if (!algo->mul(&R_Cr, &C, &r)) {
printf("C x r error!\n");
flag = 0;
continue;
}
if (!algo->equal(&L_ABr, &R_Cr)) {
printf("ABr and Cr are not equal!\n");
flag = 0;
break;
}
}
for (int i = 0; i < l->row; i++)
for (int j = 0; j < r->col; j++) {
PRIV(dst)->values[i][j] = 0;
for (int k = 0; k < l->col; k++)
PRIV(dst)->values[i][j] += PRIV(l)->values[i][k] *
PRIV(r)->values[k][j];
}
algo : Naive
matrix : 8 x 8
exe time : 0.000001 ms
matrix : 16 x 16
exe time : 0.000004 ms
matrix : 32 x 32
exe time : 0.000029 ms
matrix : 64 x 64
exe time : 0.000237 ms
matrix : 128 x 128
exe time : 0.001771 ms
matrix : 256 x 256
exe time : 0.014509 ms
matrix : 512 x 512
exe time : 0.144029 ms
matrix : 1024 x 1024
exe time : 1.456696 ms
類似 naive , 一次做 4 個 int (128 bits)
指令
__m128i _mm_setzero_si128()
__m128i _mm_load_si128(__m128i const*p)
__m128i _mm_set_epi32(int i3, int i2, int i1, int i0)
__m128i _mm_mullo_epi32( __m128i a, __m128i b)
__m128i _mm_unpacklo_epi32(__m128i a, __m128i b)
__m128i _mm_unpackhi_epi32(__m128i a, __m128i b)
__m128i _mm_unpacklo_epi64(__m128i a, __m128i b)
__m128i _mm_unpackhi_epi64(__m128i a, __m128i b)
__m128i _mm_add_epi32(__m128i a, __m128i b)
void _mm_store_si128(__m128i *p, __m128i b)
實驗結果
algo : SSE
matrix : 8 x 8
exe time : 0.000000 ms
matrix : 16 x 16
exe time : 0.000002 ms
matrix : 32 x 32
exe time : 0.000009 ms
matrix : 64 x 64
exe time : 0.000061 ms
matrix : 128 x 128
exe time : 0.000471 ms
matrix : 256 x 256
exe time : 0.003963 ms
matrix : 512 x 512
exe time : 0.035724 ms
matrix : 1024 x 1024
exe time : 0.327177 ms
類似 naive , 一次做 8 個 int (256 bits)
指令
__m256i _mm256_setzero_si256(void)
__m256i _mm256_load_si256(__m256i const *a)
__m256i _mm256_set1_epi32(int)
__m256i _mm256_mullo_epi32(__m256i s1, __m256i s2)
_mm256_add_epi32
void _mm256_store_si256(__m128i *a, __m128i b)
實驗結果
algo : AVX
matrix : 8 x 8
exe time : 0.000001 ms
matrix : 16 x 16
exe time : 0.000002 ms
matrix : 32 x 32
exe time : 0.000006 ms
matrix : 64 x 64
exe time : 0.000026 ms
matrix : 128 x 128
exe time : 0.000175 ms
matrix : 256 x 256
exe time : 0.001366 ms
matrix : 512 x 512
exe time : 0.011233 ms
matrix : 1024 x 1024
exe time : 0.103360 ms
將以下迴圈展開
for (int i = 0; i < 8; i++) {
// broadcast each elements from source 1
ymm8 = _mm256_set1_epi32(PRIV(l)->values[(x + i)][(k + 0)]);
ymm9 = _mm256_set1_epi32(PRIV(l)->values[(x + i)][(k + 1)]);
ymm10 = _mm256_set1_epi32(PRIV(l)->values[(x + i)][(k + 2)]);
ymm11 = _mm256_set1_epi32(PRIV(l)->values[(x + i)][(k + 3)]);
ymm12 = _mm256_set1_epi32(PRIV(l)->values[(x + i)][(k + 4)]);
ymm13 = _mm256_set1_epi32(PRIV(l)->values[(x + i)][(k + 5)]);
ymm14 = _mm256_set1_epi32(PRIV(l)->values[(x + i)][(k + 6)]);
ymm15 = _mm256_set1_epi32(PRIV(l)->values[(x + i)][(k + 7)]);
// multiply
ymm8 = _mm256_mullo_epi32(ymm8, ymm0); // row 1, 2
ymm9 = _mm256_mullo_epi32(ymm9, ymm1);
ymm8 = _mm256_add_epi32(ymm8, ymm9);
ymm10 = _mm256_mullo_epi32(ymm10, ymm2); // row 3, 4
ymm11 = _mm256_mullo_epi32(ymm11, ymm3);
ymm10 = _mm256_add_epi32(ymm10, ymm11);
ymm12 = _mm256_mullo_epi32(ymm12, ymm4); // row 5, 6
ymm13 = _mm256_mullo_epi32(ymm13, ymm5);
ymm12 = _mm256_add_epi32(ymm12, ymm13);
ymm14 = _mm256_mullo_epi32(ymm14, ymm6); // row 7, 8
ymm15 = _mm256_mullo_epi32(ymm15, ymm7);
ymm14 = _mm256_add_epi32(ymm14, ymm15);
ymm8 = _mm256_add_epi32(ymm8, ymm10); // sum
ymm12 = _mm256_add_epi32(ymm12, ymm14);
ymm8 = _mm256_add_epi32(ymm8, ymm12);
switch(i) {
case 0:
ymm16 = _mm256_add_epi32(ymm16, ymm8);
break;
case 1:
ymm17 = _mm256_add_epi32(ymm17, ymm8);
break;
case 2:
ymm18 = _mm256_add_epi32(ymm18, ymm8);
break;
case 3:
ymm19 = _mm256_add_epi32(ymm19, ymm8);
break;
case 4:
ymm20 = _mm256_add_epi32(ymm20, ymm8);
break;
case 5:
ymm21 = _mm256_add_epi32(ymm21, ymm8);
break;
case 6:
ymm22 = _mm256_add_epi32(ymm22, ymm8);
break;
case 7:
ymm23 = _mm256_add_epi32(ymm23, ymm8);
break;
}
}
實驗結果
algo : AVX
matrix : 8 x 8
exe time : 0.000001 ms
matrix : 16 x 16
exe time : 0.000002 ms
matrix : 32 x 32
exe time : 0.000006 ms
matrix : 64 x 64
exe time : 0.000026 ms
matrix : 128 x 128
exe time : 0.000177 ms
matrix : 256 x 256
exe time : 0.001359 ms
matrix : 512 x 512
exe time : 0.011220 ms
matrix : 1024 x 1024
exe time : 0.101038 ms
algo : AVX
matrix : 8 x 8
exe time : 0.000001 ms
matrix : 16 x 16
exe time : 0.000002 ms
matrix : 32 x 32
exe time : 0.000006 ms
matrix : 64 x 64
exe time : 0.000026 ms
matrix : 128 x 128
exe time : 0.000165 ms
matrix : 256 x 256
exe time : 0.001294 ms
matrix : 512 x 512
exe time : 0.010437 ms
matrix : 1024 x 1024
exe time : 0.096296 ms
n | 8 | 16 | 32 | 64 | 128 | 256 | 512 | 1024 |
---|---|---|---|---|---|---|---|---|
before(ms) | 0.000001 | 0.000002 | 0.000006 | 0.000026 | 0.000177 | 0.001359 | 0.011220 | 0.101038 |
after(ms) | 0.000001 | 0.000002 | 0.000006 | 0.000026 | 0.000165 | 0.001294 | 0.010437 | 0.096296 |
speedup | 1 | 1 | 1 | 1 | 1.07 | 1.05 | 1.07 | 1.05 |
n | 8 | 16 | 32 | 64 | 128 | 256 | 512 | 1024 |
---|---|---|---|---|---|---|---|---|
naive(ms) | 0.000001 | 0.000004 | 0.000029 | 0.000237 | 0.001771 | 0.014509 | 0.144029 | 1.456696 |
sse(ms) | 0.000000 | 0.000002 | 0.000009 | 0.000061 | 0.000471 | 0.003963 | 0.035724 | 0.327177 |
avx(ms) | 0.000001 | 0.000002 | 0.000005 | 0.000025 | 0.000168 | 0.001288 | 0.010347 | 0.095896 |
更新 AVX loop unrolling
seperate()
, combine()
, add_sub_part()
, mul_part()
等函式以便實作.algo : Naive
matrix : 8 x 8
exe time : 0.000001 ms
matrix : 16 x 16
exe time : 0.000004 ms
matrix : 32 x 32
exe time : 0.000032 ms
matrix : 64 x 64
exe time : 0.000220 ms
matrix : 128 x 128
exe time : 0.001543 ms
matrix : 256 x 256
exe time : 0.012644 ms
matrix : 512 x 512
exe time : 0.137741 ms
matrix : 1024 x 1024
exe time : 1.216822 ms
algo : Strassen
matrix : 8 x 8
exe time : 0.000004 ms
matrix : 16 x 16
exe time : 0.000010 ms
matrix : 32 x 32
exe time : 0.000038 ms
matrix : 64 x 64
exe time : 0.000244 ms
matrix : 128 x 128
exe time : 0.001432 ms
matrix : 256 x 256
exe time : 0.009937 ms
matrix : 512 x 512
exe time : 0.083023 ms
matrix : 1024 x 1024
exe time : 0.978982 ms
n | 8 | 16 | 32 | 64 | 128 | 256 | 512 | 1024 |
---|---|---|---|---|---|---|---|---|
naive(ms) | 0.000001 | 0.000004 | 0.000032 | 0.000220 | 0.001543 | 0.012644 | 0.137741 | 1.216822 |
Strassen(ms) | 0.000004 | 0.000010 | 0.000038 | 0.0000244 | 0.001432 | 0.009937 | 0.083023 | 0.978982 |
大約在 N > 100 後 , strassen algorithm 會快過 naive 版本
原本的 Strassen algorithm 是遞迴呼叫的, 也就是子矩陣相乘時呼叫自身, 才能使得時間複雜度為 O(n2.81), 但是這次在實作時執行時間會暴增, 所以只先執行一次矩陣分割, 目前推測問題在於分割及結合子矩陣花費的時間過長
algo : SSE
matrix : 8 x 8
exe time : 0.000000 ms
matrix : 16 x 16
exe time : 0.000001 ms
matrix : 32 x 32
exe time : 0.000008 ms
matrix : 64 x 64
exe time : 0.000056 ms
matrix : 128 x 128
exe time : 0.000430 ms
matrix : 256 x 256
exe time : 0.003593 ms
matrix : 512 x 512
exe time : 0.032536 ms
matrix : 1024 x 1024
exe time : 0.297687 ms
algo : Strassen SSE
matrix : 8 x 8
exe time : 0.000003 ms
matrix : 16 x 16
exe time : 0.000008 ms
matrix : 32 x 32
exe time : 0.000023 ms
matrix : 64 x 64
exe time : 0.000098 ms
matrix : 128 x 128
exe time : 0.000529 ms
matrix : 256 x 256
exe time : 0.003462 ms
matrix : 512 x 512
exe time : 0.026929 ms
matrix : 1024 x 1024
exe time : 0.240095 ms
n | 8 | 16 | 32 | 64 | 128 | 256 | 512 | 1024 |
---|---|---|---|---|---|---|---|---|
Strassen(ms) | 0.000004 | 0.000010 | 0.000038 | 0.0000244 | 0.001432 | 0.009937 | 0.083023 | 0.978982 |
SSE(ms) | 0.000000 | 0.000001 | 0.000008 | 0.000056 | 0.000430 | 0.003593 | 0.032536 | 0.297687 |
Strassen SSE(ms) | 0.000003 | 0.000008 | 0.000023 | 0.00098 | 0.000529 | 0.003462 | 0.026929 | 0.240095 |
在矩陣大於 256 x 256 時, Strassen SSE 會略快於 SSE |
邊界 \ n | 8 | 16 | 32 | 64 | 128 | 256 | 512 | 1024 |
---|---|---|---|---|---|---|---|---|
8 | 0.000005 | 0.000011 | 0.000089 | 0.000669 | 0.004888 | 0.033680 | 0.236442 | 1.664202 |
16 | 0.000005 | 0.000011 | 0.000040 | 0.000326 | 0.002450 | 0.017589 | 0.123404 | 0.874777 |
32 | 0.000005 | 0.000011 | 0.000040 | 0.000247 | 0.001883 | 0.013629 | 0.095878 | 0.681866 |
64 | 0.000004 | 0.000011 | 0.000040 | 0.00245 | 0.001461 | 0.010796 | 0.075746 | 0.539311 |
128 | 0.000004 | 0.000011 | 0.000040 | 0.00247 | 0.001471 | 0.010397 | 0.072306 | 0.510256 |
256 | 0.000005 | 0.000011 | 0.000040 | 0.00251 | 0.001485 | 0.010220 | 0.082975 | 0.589007 |
更新 AVX loop unrolling