我有一個大張量的浮點資料,其尺寸35k(rows) x 45(cols) x 150(slices)存盤在犰狳立方體容器中。我需要在 35 毫秒內將所有 150 個切片線性組合在一起(我的應用程式必須這樣做)。線性組合浮點權重也存盤在犰狳容器中。到目前為止,我最快的實作需要 70 毫秒,平均超過 30 幀的視窗,我似乎無法擊敗它。請注意,我允許CPU并行計算,但不允許 GPU。
我已經嘗試了多種不同的方法來執行這種線性組合,但下面的代碼似乎是我能得到的最快的(70 毫秒),因為我相信我通過在每次迭代中獲取最大可能的連續記憶體塊來最大化快取命中機會。
請注意,犰狳以列主要格式存盤資料。所以在張量中,它首先存盤第一個通道的列,然后是第二個通道的列,然后是第三個,依此類推。
typedef std::chrono::system_clock Timer;
typedef std::chrono::duration<double> Duration;
int rows = 35000;
int cols = 45;
int slices = 150;
arma::fcube tensor(rows, cols, slices, arma::fill::randu);
arma::fvec w(slices, arma::fill::randu);
double overallTime = 0;
int window = 30;
for (int n = 0; n < window; n ) {
Timer::time_point start = Timer::now();
arma::fmat result(rows, cols, arma::fill::zeros);
for (int i = 0; i < slices; i )
result = tensor.slice(i) * w(i);
Timer::time_point end = Timer::now();
Duration span = end - start;
double t = span.count();
overallTime = t;
cout << "n = " << n << " --> t = " << t * 1000.0 << " ms" << endl;
}
cout << endl << "average time = " << overallTime * 1000.0 / window << " ms" << endl;
我需要將此代碼優化至少 2 倍,我將非常感謝任何建議。
uj5u.com熱心網友回復:
首先我需要承認,我不熟悉arma框架或記憶體布局;如果語法result = slice(i) * weight延遲計算,則最少。
無論如何,兩個主要問題及其解決方案在于記憶體布局和記憶體與算術計算的比率。
說a =b*c是有問題的,因為它需要讀取band a,寫入a并使用最多兩個算術運算(兩個,如果架構不結合乘法和累加)。
如果記憶體布局是 form float tensor[rows][columns][channels],則問題會轉化為制作rows * columns長度的點積,channels并且應該這樣表示。
如果是float tensor[c][h][w],最好將回圈展開到result = slice(i) slice(i 1) ...。一次讀取四個切片可將記憶體傳輸減少 50%。
處理 N<16 的 4*N 結果塊(從所有 150 個通道/切片中讀取)的結果可能會更好,以便編譯器可以將累加器顯式或隱式分配給 SIMD 暫存器。
通過將切片計數填充為 4 或 8 的倍數,通過編譯-ffast-math以啟用融合乘法累加(如果可用)和多執行緒,有可能進行微小的改進。
約束表明需要執行 13.5GFlops,這在算術方面是一個合理的數字(對于許多現代架構),但也意味著至少 54 Gb/s 的記憶體帶寬,可以通過 fp16 或 16 位定點放寬算術。
編輯
知道記憶體順序是float tensor[150][45][35000]或float tensor[kSlices][kRows * kCols == kCols * kRows]建議我首先嘗試將外部回圈展開 4(甚至可能是 5,因為 150 不能被 4 整除,需要特殊情況來處理多余的)流。
void blend(int kCols, int kRows, float const *tensor, float *result, float const *w) {
// ensure that the cols*rows is a multiple of 4 (pad if necessary)
// - allows the auto vectorizer to skip handling the 'excess' code where the data
// length mod simd width != 0
// one could try even SIMD width of 16*4, as clang 14
// can further unroll the inner loop to 4 ymm registers
auto const stride = (kCols * kRows 3) & ~3;
// try also s =6, s =3, or s =4, which would require a dedicated inner loop (for s =2)
for (int s = 0; s < 150; s =5) {
auto src0 = tensor s * stride;
auto src1 = src0 stride;
auto src2 = src1 stride;
auto src3 = src2 stride;
auto src4 = src3 stride;
auto dst = result;
for (int x = 0; x < stride; x ) {
// clang should be able to optimize caching the weights
// to registers outside the innerloop
auto add = src0[x] * w[s]
src1[x] * w[s 1]
src2[x] * w[s 2]
src3[x] * w[s 3]
src4[x] * w[s 4];
// clang should be able to optimize this comparison
// out of the loop, generating two inner kernels
if (s == 0) {
dst[x] = add;
} else {
dst[x] = add;
}
}
}
}
編輯 2
另一個起點(在添加多執行緒之前)將考慮將布局更改為
float tensor[kCols][kRows][kSlices kPadding]; // padding is optional
現在的缺點是kSlices = 150不能再適應暫存器中的所有權重(其次,kSlices 不是 4 或 8 的倍數)。此外,最終的減少需要是水平的。
好處是減少不再需要通過記憶體,這是增加多執行緒的一件大事。
void blendHWC(float const *tensor, float const *w, float *dst, int n, int c) {
// each thread will read from 4 positions in order
// to share the weights -- finding the best distance
// might need some iterations
auto src0 = tensor;
auto src1 = src0 c;
auto src2 = src1 c;
auto src3 = src2 c;
for (int i = 0; i < n/4; i ) {
vec8 acc0(0.0f), acc1(0.0f), acc2(0.0f), acc3(0.0f);
// #pragma unroll?
for (auto j = 0; j < c / 8; c ) {
vec8 w(w j);
acc0 = w * vec8(src0 j);
acc1 = w * vec8(src1 j);
acc2 = w * vec8(src2 j);
acc3 = w * vec8(src3 j);
}
vec4 sum = horizontal_reduct(acc0,acc1,acc2,acc3);
sum.store(dst); dst =4;
}
}
這些是一些自定義 SIMD 類vec4,vec8它們通過內部函式映射到 SIMD 指令,或者通過編譯器能夠編譯using vec4 = float __attribute__ __attribute__((vector_size(16)));為高效的 SIMD 代碼。
uj5u.com熱心網友回復:
正如@hbrerkere 在評論部分所建議的那樣,通過使用該-O3標志并進行以下更改,性能提高了近 65%。代碼現在以45 毫秒運行,而不是最初的 70 毫秒。
int lastStep = (slices / 4 - 1) * 4;
int i = 0;
while (i <= lastStep) {
result = tensor.slice(i) * w_id(i) tensor.slice(i 1) * w_id(i 1) tensor.slice(i 2) * w_id(i 2) tensor.slice(i 3) * w_id(i 3);
i = 4;
}
while (i < slices) {
result = tensor.slice(i) * w_id(i);
i ;
}
uj5u.com熱心網友回復:
沒有實際代碼,我猜
= tensor.slice(i) * w_id(i)
創建一個臨時物件,然后將其添加到 lhs。是的,多載的運算子看起來不錯,但我會寫一個函式
addto(lhs, slice1, w1, slice2, w2, ....展開到 4...)
這轉化為元素上的純回圈:
for (i=....)
for (j=...)
lhs[i][j] = slice1[i][j]*w1[j] slice2[i][j] &c
如果這不給你帶來額外的因素,我會感到驚訝。
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/466548.html
