執行人: LIAO-JIAN-PENG
專題解說錄影
Petakuo
能否解釋為何逐字處理輸入序列會容易遇到梯度消失和爆炸,以及控制門 (Gate) 具體解決此問題的運作方式?
LIAO-JIAN-PENG
已附上說明
marvin0102
transformer 在字串序列增加時,矩陣運算也會增加,運算效率與CNN RNN 相比,優勢在哪裡,可否舉例說明?
LIAO-JIAN-PENG
已附上說明
vestata
為什麼在 matmul 的 loop tiling 選擇 x=16? loop tiling 大小是如何選擇的呢?
主流的 CPU Cache 的 Cache Line 大小多為 64 Bytes
一個 float 是 4 bytes
因此一個 cache line 可以存放 16 個 float 數字 (64/4 = 16)
設定 x = 16 可以最大化 cache line 的利用率,減少 cache miss
參考:現代處理器設計: Cache 原理和實際影響
jserv
將 matmul.c 納入考量,這是針對現代 x86 處理器的 AVX 和 FMA 指令集擴展開發的快速矩陣乘法實作,程式碼精簡又性能表現優異。參照該專案的介紹文章 Beating NumPy's matrix multiplication in 150 lines of C code。
探討 llama.cpp 的效能表現,分析其效能瓶頸並嘗試改進
修正圖片的存取權限。
注意用語:
Strassen 矩陣乘法是一種高效的矩陣乘法演算法,它比傳統的矩陣乘法具有更低的時間複雜度
傳統的矩陣乘法對於兩個 矩陣 A 和 B,需要 的時間。Strassen 算法則透過將矩陣劃分成較小的子矩陣,並用一種特殊的計算方式,將時間複雜度降至約。
在 矩陣相乘中,傳統矩陣相乘使用 8 個乘法,4 個加法,在 Strassen 算法中,使用到 7 個乘法 18 加法,因為乘法跟加法的運算複雜度有差,當運算矩陣大型矩陣乘法時,乘法的運算成本也就會體現出來。
雖然說一般矩陣會是剛好 的矩陣,但是我們可以使用 Block matrix 的方式將矩陣從 變成 的矩陣。
進行效能比較和實作分析,從而歸納出提升矩陣乘法的手法
Implementation | Long description |
---|---|
Naive | Most obvious implementation |
Transposed | Transposing the second matrix for cache efficiency |
sdot w/o hints | Replacing the inner loop with BLAS sdot() |
sdot with hints | sdot() with a bit unrolled loop |
SSE sdot | vectorized sdot() with explicit SSE instructions |
SSE+tiling | sdot SSE sdot() with loop tiling |
OpenBLAS sdot | sdot() provided by OpenBLAS |
OpenBLAS sgemm | sgemm() provided by OpenBLAS |
在一些數值運算庫(如 BLAS,Basic Linear Algebra Subprograms),sdot
函數用於計算兩個向量的內積。例如,給定兩個向量 和 ,sdot
會返回以下結果:
透過 Loop unrolling 的技巧,編譯器或組譯器可以預先計算出迴圈中每次反覆執行所需要的記憶體位置,並直接參照到矩陣的變數位置。這樣可以減少執行時的位址計算開銷,產生較高效能的機器碼。不過,Loop unrolling 也可能會導致程式碼大小增加,因此編譯器會根據情況決定是否應用這個技術。
避免濫用「優化」ㄧ詞,務必使用課程教材規範的術語。參見:
編譯器在最佳化階段會自動嘗試各種最佳化手法,包括 Loop unrolling,來提升程式的執行效能。然而,編譯器是否真正採用 Loop unrolling,端視迴圈的大小、複雜度、硬體特性等多種因素而定。對於一些簡單的迴圈,編譯器較有可能進行展開,但對於大型迴圈,展開可能會導致程式碼膨脹,因此編譯器可能不會採用此最佳化手法。
注意用語:
SSE(Streaming SIMD Extensions)是 Intel 處理器的一組指令集擴展,該指令集對於處理大量資料在相同操作(例如多媒體處理、科學計算)下特別有效。
SSE(Streaming SIMD Extensions)最初新增 8 個新的 128 位元暫存器,稱為 XMM0 到 XMM7。
XMM0 through 7 from Streaming SIMD Extensions wiki
Loop tiling 將大迴圈分成多個小塊 (tile) 迴圈,有效的利用記憶體快取,提升資料在快取中的命中率。
相較於 matmul 專案的只有單執行緒比較,在 matmul_bench 實作多執行緒的處理
使用更多的執行緒可以讓運算時間大幅縮短,以下為 6 個執行緒的結果
thread function: 矩陣 inL
分成數等份,從 i_start
到 i_end
的區間資料做平行處理。
將向量總和操作放到迴圈外面,讓 out
,inR
,inL
皆能存取連續空間
使用 perf 分析 cache-misses, caceh-references, instructions, cycles 的指標,也觀察其中執行時間 sudo perf stat --repeat 5 -e cache-misses,cache-references,instructions,cycles ./matmul -n 2048 -a 1
,在矩陣相乘的部份使用的 outer function 的方式,測量後的 outer function 比 transposed 更快
GCC 提供了向量擴展(Vector Extensions),允許以相對較高階的方式使用向量指令,可以使用類似 C 語言的語法來操作向量,而編譯器會將這些操作轉換為目標架構的特定 SIMD 指令。
透過 __attribute__((vector_size(n)))
可以建立自訂大小的向量型態,例如 128-bit 和 256-bit 的向量,然後直接對這些向量進行運算,GCC 會自動將其轉換成 SIMD 指令。在以下範例中,展示 v4sf
(128-bit 單精度浮點數向量)和 v8sf
(256-bit 單精度浮點數向量)進行 SIMD 計算。
v4sf
(128-bit 向量)這段程式碼使用 128-bit 的向量型態 v4sf
來加速矩陣運算。每次迴圈會同時處理 4 個浮點數,減少處理次數。
v8sf
(256-bit 向量)v8sf
允許處理更大的向量,進一步增加處理的浮點數數量。在支援 AVX 的硬體上,可以用 256-bit 向量一次處理 8 個浮點數。
與 loop tiling 類似,loop tiling 是針對迴圈的操作,而 block matrix 是將大矩陣劃分成小矩陣(block matrix),並對這些小矩陣分別進行運算,兩者皆是改善記憶體的局部性(locality),減少了記憶體讀取的延遲,提高了計算效率,在實際應用中,可以根據具體的硬體配置選擇合適的分割大小 k,以達到最佳效能。
注意書寫規範:
Transformer 跟過去的模型差在哪裡,解決什麼問題
Transformer(Attention is all you need)模型在 2017 年引入,為了解決機器翻譯中的長期依賴性問題,透過避免遞迴操作並使用平行化計算的方式減少訓練時間,提升訓練效率。其主要創新點如下:
以下說明 Transformer 如何透過自注意力機制一步到位地取得全局關聯性。對於每個詞嵌入向量 ,計算 Query ()、Key () 和 Value () 向量:
其中 、 和 是學習的權重矩陣。
使用 Query 向量和 Key 向量計算 Attention scores:
計算得到的 Attention score 矩陣是一個 的矩陣,其中 是序列長度。這個矩陣的每個元素表示序列中兩個詞之間的關聯性。這些分數應用於 Value 向量,得到加權平均後的輸出向量:
在一次自注意力操作中,所有詞之間的關聯性都被計算出來。這意味著每個詞都能夠看到整個序列中的其他詞,從而捕捉到全局的關聯性。這與 CNN 逐層擴展視野的方法不同,Transformer 能在一步操作中完成全局關聯性的捕捉,且其中的計算可以被平行化。
RNN(遞迴神經網路)和 LSTM(長短期記憶網路)主要特性如下:
已知 RNN 模型主要是以序列處理,下一個輸入會是上一個輸出的結果,因此 RNN 的計算受限於時間,無法進行平行化。雖然透過局部平行化可以改善單個 RNN 節點的速度,但無法提升整體訓練速度。即使後續的 LSTM 和 GRU 透過門控機制解決長期依賴問題,它們依然是逐步序列處理,無法在根本上達到平行化的效益。
在 RNN 中,模型透過反向傳播更新參數。此過程中,梯度會反覆疊加並反向傳遞。當序列較長時,反向傳播的過程涉及將梯度多次相乘,尤其當 RNN 的隱藏狀態更新公式為:
這個更新公式中的權重矩陣 反覆相乘,使得梯度在傳播過程中呈現指數增長或縮減。當權重絕對值大於 1 時,會引發梯度爆炸;相反,當權重小於 1 時,梯度會指數衰減而導致梯度消失。這些問題特別會影響較長序列的學習,使得 RNN 無法有效捕捉長期依賴。
LSTM (Long Short-Term Memory) 和 GRU (Gated Recurrent Unit) 等架構透過控制門 (Gate) 的設計來解決梯度問題。具體來說,控制門透過引入額外的機制,讓模型能夠選擇性地保留或丟棄資訊,控制梯度的傳遞並保護長期記憶。以下是控制門的關鍵作用:
輸入門 (Input Gate):控制哪些新資訊需要被加入到記憶單元。輸入門會根據當前的輸入和先前的隱藏狀態生成一個範圍在 0 到 1 的控制信號,決定要寫入記憶的程度。
遺忘門 (Forget Gate):控制哪些資訊需要從記憶單元中移除。遺忘門會根據過去的狀態與當前輸入判斷應該遺忘的記憶比例。這樣可以避免無用資訊的累積,減少梯度爆炸的風險。
輸出門 (Output Gate):控制記憶單元中哪些資訊需要影響當前輸出的隱藏狀態。輸出門根據過去的記憶單元和當前的輸入決定應輸出的資訊,使得模型保留重要資訊。
CNN(卷積神經網路)在 NLP 中也被廣泛應用,尤其適合短文本處理。其主要特性如下:
相較之下,CNN 的計算更容易平行化,因為卷積操作本質上可以平行處理。然而,CNN 也有其局限性。CNN 模型中兩個重要元件:卷積 (Convolution) 和池化 (Pooling),都可以獨立操作,因而可以平行化。但隨著序列長度的增加,CNN 的運算成本也隨之倍增,主要有以下幾個原因:
局部視野:卷積核一次只能看到固定大小的窗口(例如 3 個或 5 個詞),這意味著需要多層卷積層來擴大視野以捕捉更長距離的依賴性。
層數增加:隨著序列長度增加,需要更多的卷積層來捕捉長距依賴性,這會導致模型變得更深,從而增加計算量和訓練時間。以下是層數增多的例子:
要捕捉長度為 100 的序列的長距離依賴性,需要多層卷積:
參考:Why does the transformer do better than RNN and LSTM in long-range context dependencies?
找出其中效能瓶頸的地方,可能是矩陣運算,或是記憶體 (特別是 data model)
llama.cpp 在導入 llamafile sgemm (commit 8cc91dc) 之前,使用 perf 去觀察 cpu-cycles
,faults
,cache-references
,cache-misses
llamafile sgemm 之前,瓶頸出現在 ggml_vec_dot
Matrix vector multiplication is an operation where latency (not throughput) is the bottleneck, and the bloat of fancy libraries has a measurable impact.
LLaMA Now Goes Faster on CPUs
llamafile sgemmg 之後
LLM通常使用全精度(float32)或半精度(float16)浮點數進行訓練。因此,量化過程尋找一種方法將 FP32 權重值的範圍(資料類型為 [min, max])表示為較低精確度值,例如 FP16 甚至 INT4(整數 4 位元)資料類型。典型的情況是從 FP32 到 INT8。除了降低模型載入及儲存的空間,同時也因為精度下降,進而讓計算速度有所提升
然而精度的下降意味著模型預測的準確度會有所影響,可以藉由 Perplexity 指標確認量化模型後的品質
這表示模型在給定序列 的條件下預測下個詞 的能力,而我們是希望在 的機率越大越好,條件機率的算法是 ,加上 之後就可以方便計算其總和並取平均,得到一個數值。Perplexity 越低,表示模型預測越準確;值越高,表示模型預測越困難。
在 llama.cpp 中有探討不同大小的語言模型量化後的評估 k-quants #1684
將浮點數 映射到整數
計算極值的映射關係
藉由極值 計算 ,
最後 zero point z
表示原點,且 scale s
表示縮放數值
透過極值 取得 zero point
及 scale
實際使用中 , 而量化極值可以是 , 通常這個數值範圍可以自訂
量化矩陣相乘過程,先知道矩陣相乘 ,
矩陣相乘數學表示式
依照上面量化公式 代入
從量化後的 , , 計算出量化結果
以下數值在模型推算時是常數,可以事先計算好
而其中 就是整數的矩陣相乘,可以使用硬體加速或是演算法處理
經常用於深度模型的 ReLU
用於表示量化 ReLU 且更通用的定義
數學公式推導量化 ReLU
input tensor
和 output tensor
。input tensor
x,即 ,輸出為浮點數的 output tensor
y,並計算 及 。input tensor
,並提供 output tensor
的參數 ,輸出為量化的 output tensor
,並計算 。output tensor
y。output tensor
,並計算 。動態量化是在神經網路推理過程中使用,主要目的是使用整數操作來代替浮點數操作,以提高計算效率和降低資源消耗。在動態量化中,網路的權重在推理運行前被量化為整數,但網路並不知道 output tensor
, 及 activation tensors
。而在執行階段中,只要一旦取得浮點數,即可以動態地計算出,進而算出中間層的 scale 及 zero point。
動態量化過程會直接算出 透過 ,前面有計算 的方式
優點:動態量化不需要任何預先校準資料,且是使用整數運算,動態量化仍能通過減少浮點運算來提升性能。
缺點:動態量化在計算 scale 和 zero points 時需要更多的運算,這可能在某些情況下增加計算負擔。
靜態量化與動態量化的不同之處在於,靜態量化在推理前已經預先計算好所有 activation tensor 的 scales 和 zero points。因此,在推理期間不需要再計算這些參數。activation tensor 可以以整數格式儲存在記憶體中,而無需從浮點數格式轉換。
注意用語
計算所有 activation tensor 的 scales 和 zero points 的方法非常簡單。給定一個浮點數神經網路,我們只需使用一些代表性的無標籤資料運行該神經網路,收集所有 activation layer 的分布統計數據。然後,我們可以使用這些分布統計資料,按照前面的數學公式計算 scales 和 zero points。
在推理期間,由於所有計算都是使用整數運算,因此推理最快。唯一的缺點是我們必須準備代表性的無標籤資料。如果資料不具有代表性,計算出的 scales 和 zero points 可能無法反映到真實情況,從而影響推理準確性。
量化感知訓練在模型訓練期間進行模擬量化操作,使得模型在訓練時就能適應量化後的精度變化。
是一個未知的小數值,如果 則表示量化模型的推理準確性將與浮點數模型完全一致,而這也是一個方式可以讓神經網路去學習之間的關係,並讓 的數值越小越好,並將之間的誤差降到最低。
這種量化技術不僅提高了模型的推理速度,還使得儲存需求更低,同時確保精度上有較小的損失。
探討其代價。