contributed by: <Beethovenjoker
> (章元豪) Github
原本在eBPF的環境下編譯檔案邊玩Terreria編譯檔案很卡,但是換成scx_lavd之後遊戲體驗變得非常順暢。
can_migrate_task()
會評估 Task 是否可以遷移,ML 可以幫助提高這個判斷的準確性。fio
進行 Disk I/O 測試。排程器 | Task Migration 次數 | Kernel Compilation Time |
---|---|---|
EEVDF | 51,719 | 6m20s |
scx_rusty | 265,801 | 7m10s |
scx_rusty + ML | 61,649 | 5m41s |
在進行大型語言模型(LLM)推理時,矩陣乘法往往是主要的計算瓶頸之一。以 LLaMA 為例,其運作原理在底層仍然必須進行大量的矩陣運算,因此如何加速矩陣乘法就成了提升 LLaMA 推理速度的關鍵。
補充:近代研究已使矩陣乘法複雜度逐漸逼近
; 然而,這些演算法通常只有在非常大的矩陣規模下才會展現出優勢,且程式的常數項較大。實際應用仍需權衡。
在硬體支援的前提下,最直接也最有效的方式之一就是「並行化」:無論是 CPU 多核心、SIMD 指令集(如 SSE、AVX),或是 GPU 等專用硬體,都能加速大量的乘加運算。為了探討這些方法,可以比較以下兩個專案:
float sdot_8(int n, const float *x, const float *y)
{
int i, n8 = n>>3<<3;
float s, t[8];
t[0] = t[1] = t[2] = t[3] = t[4] = t[5] = t[6] = t[7] = 0.0f;
for (i = 0; i < n8; i += 8) {
t[0] += x[i+0] * y[i+0];
t[1] += x[i+1] * y[i+1];
t[2] += x[i+2] * y[i+2];
t[3] += x[i+3] * y[i+3];
t[4] += x[i+4] * y[i+4];
t[5] += x[i+5] * y[i+5];
t[6] += x[i+6] * y[i+6];
t[7] += x[i+7] * y[i+7];
}
for (s = 0.0f; i < n; ++i) s += x[i] * y[i];
s += t[0] + t[1] + t[2] + t[3] + t[4] + t[5] + t[6] + t[7];
return s;
}
Loop unrolling:將迴圈攤平後,編譯器或組譯器能更容易預測記憶體存取位置,也降低迴圈控制的開銷。不過若迴圈過大,程式碼膨脹嚴重時,編譯器就不一定會自動展開。
float sdot_sse(int n, const float *x, const float *y)
{
int i, n8 = n>>3<<3;
__m128 vs1, vs2;
float s, t[4];
vs1 = _mm_setzero_ps(); // {0.f, 0.f, 0.f, 0.f}
vs2 = _mm_setzero_ps(); // {0.f, 0.f, 0.f, 0.f}
for (i = 0; i < n8; i += 8) {
__m128 vx1, vx2, vy1, vy2;
vx1 = _mm_loadu_ps(&x[i]); // load {x[0], x[1], x[2], x[3]}
vx2 = _mm_loadu_ps(&x[i+4]); // load {x[4], x[5], x[6], x[7]}
vy1 = _mm_loadu_ps(&y[i]); // load {y[0], y[1], y[2], y[3]}
vy2 = _mm_loadu_ps(&y[i+4]); // load {y[4], y[5], y[6], y[7]}
vs1 = _mm_add_ps(vs1, _mm_mul_ps(vx1, vy1)); // vs1 += vx1*vy1
vs2 = _mm_add_ps(vs2, _mm_mul_ps(vx2, vy2));
}
for (s = 0.0f; i < n; ++i) s += x[i] * y[i]; // Complete the remaining (insufficient 8) operations.
_mm_storeu_ps(t, vs1);
s += t[0] + t[1] + t[2] + t[3]; // {t[0], t[1], t[2], t[3]}
_mm_storeu_ps(t, vs2);
s += t[0] + t[1] + t[2] + t[3]; // {t[4], t[5], t[6], t[7]}
return s;
}
_mm_setzero_ps()
:載入四個 0 浮點數。_mm_loadu_ps()
:從記憶體載入 4 筆浮點數至暫存器。_mm_add_ps()
和 _mm_mul_ps()
:分別進行同時乘法或加法運算。int i, j, ii, jj, x = 16, n_b_rows = n_a_cols;
float **m, **bT;
m = mat_init(n_a_rows, n_b_cols);
bT = mat_transpose(n_b_rows, n_b_cols, b);
for (i = 0; i < n_a_rows; i += x) {
for (j = 0; j < n_b_cols; j += x) {
// loop tiling
int je = n_b_cols < j + x? n_b_cols : j + x;
int ie = n_a_rows < i + x? n_a_rows : i + x;
for (ii = i; ii < ie; ++ii)
for (jj = j; jj < je; ++jj)
m[ii][jj] += sdot_sse(n_a_cols, a[ii], bT[jj]);
}
}
在「matmul_bench」專案中,進一步利用多執行緒加速:
以如下程式碼為例,thread function
接收各執行緒需負責處理的起始索引 i_start
與結束索引 i_end
,然後各自做部分相乘。
for (unsigned long i=i_start; i<i_end; i++) {
for (unsigned long j=0; j<n; j++) {
float v = 0;
for (unsigned long k=0; k<n; k++) {
v += inL[i*n+k] * inR[k*n+j];
}
out[i*n+j] = v;
}
}
以「outer」寫法可讓三重迴圈的存取更有序,提升資料區域性與快取效率。
for (unsigned long i=0; i<n; i++) {
for (int k=0; k<n; k++) {
float lik = inL[i*n+k];
for (int j=0; j<n; j++) {
out[i*n+j] += lik * inR[k*n + j];
}
}
}
與一般的轉置做法類似,但不需真的轉置即可達成「對 out 的連續存取」。
GCC 的向量擴展(Vector Extensions) 提供了一種高階語法使用 SIMD 指令。
typedef float v4sf __attribute__((vector_size (16))); // 128-bit
typedef float v8sf __attribute__((vector_size (32))); // 256-bit
編譯器會將這些向量型態轉成目標架構的 SIMD 指令。
typedef float v4sf __attribute__((vector_size (16)));
for (unsigned long i=i_start; i<i_end; i++) {
for (int k=0; k<n; k++) {
float lik = inL[i*n+k];
v4sf vlik = {lik,lik,lik,lik};
for (int j=0; j<n; j+=4) {
*(v4sf*)&out[i*n+j] += vlik * *(v4sf*)&inR[k*n + j];
}
}
}
typedef float v8sf __attribute__((vector_size (32)));
v8sf vlik = {lik,lik,lik,lik,
lik,lik,lik,lik};
一次處理 8 個 float 數值,如果硬體支援 AVX,就能發揮更好的並行運算效率。
與 loop tiling 類似,但此處是將整個大矩陣拆分成多個小矩陣(block),再進行相乘;同樣是為了增強記憶體的 locality。
for (i0=i_start; i0<i_end; i0+=block_size) {
for (int j0=0; j0<n; j0+=block_size) {
for (int k0=0; k0<n; k0+=block_size) {
for (int bi=0; bi<block_size; bi++) {
int i = i0+bi;
for (int bk=0; bk<block_size; bk++) {
int k = k0+bk;
float lik = inL[i*n + k];
for (int bj=0; bj<block_size; bj++) {
int j = j0+bj;
out[i*n + j] += lik * inR[k*n + j]; // outer function
}
}
}
}
}
}
透過調整 block_size
,能在不同硬體(CPU、快取大小等)上找到最佳效能點。