sysprog
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Owners
        • Signed-in users
        • Everyone
        Owners Signed-in users Everyone
      • Write
        • Owners
        • Signed-in users
        • Everyone
        Owners Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights New
    • Engagement control
    • Transfer ownership
    • Delete this note
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Note Insights Versions and GitHub Sync Sharing URL Help
Menu
Options
Engagement control Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Owners
  • Owners
  • Signed-in users
  • Everyone
Owners Signed-in users Everyone
Write
Owners
  • Owners
  • Signed-in users
  • Everyone
Owners Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       Owned this note    Owned this note      
    Published Linked with GitHub
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    # Linux 核心專題: llama.cpp 效能分析 > 執行人: vestata > [專題解說影片](https://youtu.be/WM6qmB4tQJs) ### Reviewed by `yu-hsiennn` > 整個程式運作都的計算熱點會是矩陣乘法 指的是 整個程式運作的計算熱點都會是矩陣乘法? 還有 `熱點` 指的是什麼? > [Wikipedia: hot spot](https://en.wikipedia.org/wiki/Hot_spot_(computer_programming)) 我翻譯為「熱點」,解釋為程式執行過程中大部分時間消耗的區域,〈[LLaMA Now Goes Faster on CPUs](https://justine.lol/matmul/)〉中使用的描述是 [Wikipedia: Bottleneck](https://en.wikipedia.org/wiki/Bottleneck_(software)) 但是我覺得在這邊使用熱點比較符合。 ### Reviewed by `yenslife` 既然知道 `((1 << 3) - 1)` 表示 7,那為什麼一開始不要直接寫 7 就好?我剛剛在猜會不會與操作成本有關,但看到之後開發者修改成 -8 就更不懂了。 > 使用 `((1 << 3) - 1)` 表示為 7 而不是直接使用 7 的具體原因...我還不知道,從第一個版本 [commit 26c0846](https://github.com/ggerganov/llama.cpp/commit/26c084662903ddaca19bef982831bfb0856e8257#diff-6d9ce99fcb6f51ff76f59e479f6e6fc0bb62edef7442805d7a5bb15b23996b5d) 便是使用 `((1 << 3) - 1)` 延用到現在,我認為是為了增加程式可解讀性。若有發現我會再更新。 > 可以參考我之後的在 [Quantization](https://hackmd.io/8VsaB9JESziJyp5lfmSvRQ#Quantization) 下面的補充,其中 $\Delta$ 是其步長,一般會定義為 $\Delta = 2^{(b-1)}-1$ ,在 `q4_0` 也就是 `((1 << 3) - 1)` ,但是因為使用二補數,所以 4 位元表示範圍應該是 -8~7。我們現在令 `n = 30000, max = 65504(FP16 表示的最大值)` 作為舉例,分別進行 -8 與 7 的量化與解量化為 > 7:((30000 / (65504 / 7) + 8.5) - 8) * (65504 / 7) = 34678.85714285714 > -8:((30000 / (65504 / -8) + 8.5) - 8) * (65504 / -8) = 25905.999999999996 誤差分別為 -4678.857 與 4094,使用 -8 有較小的誤差。 ### Reviewed by `jserv` 將 [matmul.c](https://github.com/salykova/matmul.c) 納入考量,這是針對現代 x86 處理器的 AVX 和 FMA 指令集擴展開發的快速矩陣乘法實作,程式碼精簡又性能表現優異。參照該專案的介紹文章 [Beating NumPy's matrix multiplication in 150 lines of C code](https://salykova.github.io/matmul-cpu)。 ## 任務簡介 探討 llama.cpp 的效能表現,分析其效能瓶頸並嘗試改進 ## TODO: 讀懂〈[LLaMA Now Goes Faster on CPUs](https://justine.lol/matmul/)〉並重現實驗 > 不同 quantization 所產生的精度差異,如何實驗它對模型的影響? :::danger 改進你的漢語表達。 ::: [llamafile](https://github.com/Mozilla-Ocho/llamafile) 是為了簡化使用 LLM 所生的專案。結合了 [llama.cpp](https://github.com/ggerganov/llama.cpp) 與 [Cosmopolitan Libc](https://github.com/jart/cosmopolitan) 使配置與運行壓縮到單一可執行檔中。 :::danger 注意用語: * file 是「檔案」,而非「文件」(document) * thread 是「執行緒」,而非「線程」 > 好的,收到 ::: ### Quantization 〈[LLaMA Now Goes Faster on CPUs](https://justine.lol/matmul/)〉這篇文章是中使用不同的量化手法進行效能的比較,主要針對 prompt (tok/sec) 即讀取 token 的速度,與 eval (tok/sec) 輸出的 token 速度進行比較。一般訓練好並開放的大型語言模型權重 `.pth` 是使用 BF16 的資料型態,在 [llama.cpp](https://github.com/ggerganov/llama.cpp) 需要將 `.pth` 權重轉成 `.gguf` 的資料型態。 `.gguf` 是 [llama.cpp](https://github.com/ggerganov/llama.cpp) 的作者所開發的資料格式,可以使用在其相關的專案中,如以下圖例: ![image](https://hackmd.io/_uploads/r1bLTzDSR.png) > https://huggingface.co/docs/hub/gguf :::info TODO gguf/tensor 的差異 補 gguf 的 `mmap()` ::: 依據〈[Quantizing deep convolutional networks for efficient inference: A whitepaper](https://arxiv.org/pdf/1806.08342)〉 這篇論文,可以將量化的分為兩種 Affine Quantizer 與 Symmetric Quantizer 兩種。 ##### Symmetric quantize \begin{equation} x_{\text{int}} = \text{round}\left(\frac{x}{\Delta}\right) \end{equation} \begin{equation} x_Q = \text{clamp}\left(-\frac{N_{\text{levels}}}{2}, \frac{N_{\text{levels}}}{2} - 1, x_{\text{int}}\right) \quad \text{if signed} \end{equation} \begin{equation} x_Q = \text{clamp}(0, N_{\text{levels}} - 1, x_{\text{int}}) \quad \text{if un-signed} \end{equation} 其中 \begin{equation*} \text{clamp}(a, b, x) = \begin{cases} a & \text{if } x \leq a \\ x & \text{if } a \leq x \leq b \\ b & \text{if } x \geq b \end{cases} \end{equation*} s 為scale factor,z 為 zero point 此方法會對應到等等的 `Q8_0` 手法。 ##### Affine Quantizer 量化: \begin{equation} x_{\text{int}} = \text{round}\left(\frac{x}{\Delta}\right) + z \end{equation} \begin{equation} x_Q = \text{clamp}(0, N_{\text{levels}} - 1, x_{\text{int}}) \end{equation} 解量化: $x_{float}=(x_Q-z)\Delta$ 此方法會對應到等等的 `Q4_0`、`Q4_1` 手法。 #### 量化手法介紹 手法通常分為 `Qn_0`,`Qn_1`,`Qn_k`。 - [ ] `Qn_0` 將權重縮小到 n 位元表示,一次進行一個 block,大小為 32,選擇每一 block 局部最大值進行縮放,區間為 0 到 最大值之間。 - [ ] `Qn_1` 將權重縮小到 n 位元表示,一次進行一個 block,大小為 32,選擇每一 block 局部最大值與最小值進行縮放,區間為 最小值 到 最大值 之間。 - [ ] `Qn_k` :::info TODO ::: `Q4_0` 會儲存在一個 `block_q4_0` 結構體中,其大小會是 32,其中 `d` 是他的縮放比例(scalar),`qs` 是一個 `uint8_t` 陣列用來儲存兩個 4 位元的,在量化的過程中,原本 BF16 的模型權重以 `block_q4_0` 為單位去做儲存: ```c #define QK4_0 32 typedef struct { ggml_half d; // delta is uint16 uint8_t qs[QK4_0 / 2]; // nibbles / quants } block_q4_0; ``` 量化函式 `quantize_row_q4_0_reference()` 是負責量化的程式,其中發現很有趣的地方。以下是量化函式中的細節 `const float d = max / -8;` 是一個在 [commit dd0eabc](https://github.com/ggerganov/llama.cpp/commit/dd0eabc049fb1efc631cab8eb0a646808d704e18) 的變更,將 `((1 << 3) - 1)` 改為 -8 因為這邊是為了將原本 FP16 量化到 4 個位元,所以 4 位元的二的補數表示範圍為 -8 到 7。為了善用更大的分母所以使用 -8,其所導致的整個存放在 `.gguf` 的權重變成負數,在 perplexity 的表現上勝於 `((1 << 3) - 1)` 的方法,見 [#729](https://github.com/ggerganov/llama.cpp/pull/729)。 而在後面要將其放進 4 位元之中,將原本權重參數乘上縮放比例 `id`,但是後面的順序的擺放是將 `x[i*qk + 0 + j]` 與 `x[i*qk + qk/2 + j]` 擺在一起。這是在 [commit b9fd7ee](https://github.com/ggerganov/llama.cpp/commit/b9fd7eee57df101d4a3e3eabc9fd6c2cb13c9ca1) 的變更。 ```diff y[i].d = d; - for (int l = 0; l < QK4_0; l += 2) { - const float v0 = x[i*QK4_0 + l + 0]*id; - const float v1 = x[i*QK4_0 + l + 1]*id; + for (int j = 0; j < qk/2; ++j) { + const float x0 = x[i*qk + 0 + j]*id; + const float x1 = x[i*qk + qk/2 + j]*id; - const uint8_t vi0 = MIN(15, (int8_t)roundf(v0) + 8); - const uint8_t vi1 = MIN(15, (int8_t)roundf(v1) + 8); + const uint8_t xi0 = MIN(15, (int8_t)(x0 + 8.5f)); + const uint8_t xi1 = MIN(15, (int8_t)(x1 + 8.5f)); - assert(vi0 < 16); - assert(vi1 < 16); - - pp[l/2] = vi0 | (vi1 << 4); + y[i].qs[j] = xi0; + y[i].qs[j] |= xi1 << 4; } - - memcpy(y[i].qs, pp, sizeof(pp)); } ``` 這個變更讓我困惑很久,但是這必須提到 `llama.cpp` 的開發規則,在此專案之中的定義的矩陣相乘是 $C^T=AB^T \Leftrightarrow C = BA^T$ ,這與傳統的矩陣相乘記憶體儲存手法不同。 ![image](https://hackmd.io/_uploads/ry1NAY_UC.png) > [README.md](https://github.com/ggerganov/llama.cpp/blob/master/README.md#coding-guidelines) 在這邊補充一下,一般我們在做矩陣相乘的時候 $C = AB$ 會遇到的狀態會是 $A,B$ 在記憶體的排列是為 row-major。當處理 $A$ 時,可以利用其連續記憶體的特性,減少到主記憶體的成本,這樣能夠利用 L1 cache。然而,$B$ 的矩陣乘法需要每一 column 的資料,這與 row-major 記憶體排列剛好相反,所以 $C = AB$ 這個乘法最大的效能瓶頸會是 $B$ 對主記憶體成本。有很多矩陣乘法手段可以更好的利用 L1 cache,一個簡單的方法是對 $B$ 做轉置,詳細可以參考我對[矩陣相乘的筆記](https://hackmd.io/@tony268596/Hy4EWvXM0)。 所以對於 `quantize_row_q4_0_reference()` 的變更就是為了符合在 $C = BA^T$ 的運算規則之下。對其中一個矩陣雖然因為 L1 cache 的優勢,但是以演算法的角度尚需要 $O(n^3)$ 的成本。因此,llama.cpp 原本的作法是將量化後的 `uint` 以相鄰的方式排列,但之後需要額外的轉置成本。改動後,量化時便將其以矩陣轉置 $A^T$ 的排序放進記憶體中,以減少後續的轉置成本。 ### Perplexity 經過量化數值的精確度一定會喪失,只是不同的量化手段會有不同精確度喪失,但是這精確度喪失會大型語言模型會有什麼影響呢?回到模型的根本,是使用模型權重去選擇生成下一個最有可能出現的 token,我們可以針對生出的 token 進行評估。在 llama.cpp 專案之中的方法是使用 perplexity。其定義如下: $$\text{PPL}(X) = \exp \left\{ -\frac{1}{t} \sum_{i}^t \log p_\theta (x_i \mid x_{<i}) \right\} $$ :::danger 闡述其特性、考量因素,以及對應的限制。 ::: 給定特定的文章,這邊使用的是 [WikiText-2](https://paperswithcode.com/dataset/wikitext-2) 在給定的文章中,模型使用 $X_{<i}$ 的已知 token 去推斷 $X_i$ 的 token,$t$ 表示為 token 數,所以 perplexity 越低,表示模型的預測結果與實際 token 序列越接近。 ![image](https://hackmd.io/_uploads/H1xvZ8nL0.png =70%x) 文章中使用不同的 quantization 手法,有 `Q4_0`,`Q8_0`,`FP16`。以 `Q4_0` 為例,以下是主要實作的程式: ### 重現實驗 使用的測試工具是 llama-bench,這是在專案之中被廣泛的使用的測試工具,分別會計算輸入 token(prompt) 與輸出 token(eval) 的時間。主要的評估函式是 `test_promt()` 與 `test_gen()` 。其中 `test_promt()` 是以 `rand()` 去生成隨機數字,計算模型在消化 n_prompt 的隨機數字需要多少時間。 ```c while (n_processed < n_prompt) { int n_tokens = std::min(n_prompt - n_processed, n_batch); tokens[0] = n_processed == 0 && llama_add_bos_token(model) ? llama_token_bos(model) : std::rand() % n_vocab; for (int i = 1; i < n_tokens; i++) { tokens[i] = std::rand() % n_vocab; } llama_decode(ctx, llama_batch_get_one(tokens.data(), n_tokens, n_past + n_processed, 0)); n_processed += n_tokens; } ``` 在 `test_gen()` 則是計算模型每次處理一個 `rand()` 隨機數,進行 n_gen 次所花費的時間。 ```c for (int i = 0; i < n_gen; i++) { llama_decode(ctx, llama_batch_get_one(&token, 1, n_past + i, 0)); token = std::rand() % n_vocab; } ``` 測試設備: ```bash Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Address sizes: 46 bits physical, 48 bits virtual Byte Order: Little Endian CPU(s): 24 On-line CPU(s) list: 0-23 Vendor ID: GenuineIntel Model name: 13th Gen Intel(R) Core(TM) i7-13700 CPU family: 6 Model: 183 Thread(s) per core: 2 Core(s) per socket: 16 Socket(s): 1 Stepping: 1 CPU max MHz: 5200.0000 CPU min MHz: 800.0000 BogoMIPS: 4224.00 Virtualization features: Virtualization: VT-x Caches (sum of all): L1d: 640 KiB (16 instances) L1i: 768 KiB (16 instances) L2: 24 MiB (10 instances) L3: 30 MiB (1 instance) ``` 我這邊選擇兩個版本一個是 [commit dbceec8](https://github.com/ggerganov/llama.cpp/commit/dbceec87c0221ec952e69448df6a71f1372a7487) 使用舊版向量內積,與 [commit 74f33ad](https://github.com/ggerganov/llama.cpp/commit/74f33adf5f8b20b08fc5a6aa17ce081abe86ef2f),此版本是 2024-5-23 是我拿來做測試的版本,使用的是 sgemm 新的矩陣乘法模組(llamafile 的乘法模組)。我將以這兩個做實驗比較。在 x86 機器中使用 `make` 即可完成編譯,其中執行緒的數量皆是使用預設。 :::danger 補上對應的超連結。 > 已補上 ::: | prompt (tok/sec) | eval (tok/sec) | model | weights data type | hardware | software | | ---------------- | -------------- | -------------- | ----------------- | ----------- | ------------------- | | 255.90 ± 12.60 | 26.29 ± 0.56 | TinyLlama 1.1B | q8_0 | Raptor Lake | llama.cpp 2024-5-23 | | 129.67 ± 2.42 | 22.70 ± 0.61 | TinyLlama 1.1B | q8_0 | Raptor Lake | llama.cpp old | | 199.98 ± 33.37 | 14.96 ± 0.36 | TinyLlama 1.1B | FP16 | Raptor Lake | llama.cpp 2024-5-23 | | 48.99 ± 0.19 | 12.97 ± 0.27 | TinyLlama 1.1B | FP16 | Raptor Lake | llama.cpp old | 在 `TinyLlama 1.1B` 權重之下 `q8_0`,`FP16` 使用 llamafile 加速後,在 `prompt (tok/sec)` 上的性能分別提升了 197% 和 414%,有顯著的進步。,在 `eval (tok/sec)` 的表現上則相近。 | prompt (tok/sec) | eval (tok/sec) | model | weights data type | hardware | software | | ---------------- | -------------- | ---------- | ----------------- | ----------- | ------------------- | | 25.08 ± 1.10 | 2.05 ± 0.08 | mistral-7b | q8_0 | Raptor Lake | llama.cpp 2024-5-23 | | 19.40 ± 0.75 | 3.55 ± 0.02 | mistral-7b | q8_0 | Raptor Lake | llama.cpp old | | 41.51 ± 0.68 | 4.21 ± 0.05 | mistral-7b | FP16 | Raptor Lake | llama.cpp 2024-5-23 | | 6.86 ± 0.07 | 1.84 ± 0.05 | mistral-7b | FP16 | Raptor Lake | llama.cpp old | 在 `mistral-7b` 權重之下 `q4_0`,`q8_0`,`FP16` 使用 llamafile 加速後,在 `prompt (tok/sec)` 上的性能分別提升了 440%、%、413% 和 364%,有顯著的進步。,在 `eval (tok/sec)` 的表現上則相近。 :::danger 討論其精確度的犧牲狀況。 ::: <!-- #### llamafile | model_filename | model | size | params | backend | threads | test | t/s | | ---------------------------------------:| ------------- | ----------:| ------:| ------- | -------:| -----:| ------:| | TinyLlama-1.1B-Chat-v1.0.Q4_0.llamafile | llama 1B Q4_0 | 606.53 MiB | 1.10 B | CPU | 8 | pp512 | 397.62 | | TinyLlama-1.1B-Chat-v1.0.Q4_0.llamafile | llama 1B Q4_0 | 606.53 MiB | 1.10 B | CPU | 8 | tg16 | 53.83 | | TinyLlama-1.1B-Chat-v1.0.Q8_0.llamafile | llama 1B Q8_0 | 1.09 GiB | 1.10 B | CPU | 8 | pp512 | 327.43 | | TinyLlama-1.1B-Chat-v1.0.Q8_0.llamafile | llama 1B Q8_0 | 1.09 GiB | 1.10 B | CPU | 8 | tg16 | 30.37 | | TinyLlama-1.1B-Chat-v1.0.F16.llamafile | llama 1B F16 | 2.05 GiB | 1.10 B | CPU | 8 | pp512 | 317.98 | | TinyLlama-1.1B-Chat-v1.0.F16.llamafile | llama 1B F16 | 2.05 GiB | 1.10 B | CPU | 8 | tg16 | 16.65 | | model_filename | model | size | params | backend | threads | test | t/s | | ---------------------------------------:| ------------- | ---------:| ------:| ------- | -------:| -----:| -----:| | mistral-7b-instruct-v0.2.Q4_0.llamafile | llama 7B Q4_0 | 3.83 GiB | 7.24 B | CPU | 8 | pp512 | 44.02 | | mistral-7b-instruct-v0.2.Q4_0.llamafile | llama 7B Q4_0 | 3.83 GiB | 7.24 B | CPU | 8 | tg16 | 8.28 | | mistral-7b-instruct-v0.2.Q8_0.llamafile | llama 7B Q8_0 | 7.17 GiB | 7.24 B | CPU | 8 | pp512 | 42.52 | | mistral-7b-instruct-v0.2.Q8_0.llamafile | llama 7B Q8_0 | 7.17 GiB | 7.24 B | CPU | 8 | tg16 | 4.42 | | mistral-7b-instruct-v0.2.F16.llamafile | llama 7B F16 | 13.49 GiB | 7.24 B | CPU | 8 | pp512 | 33.71 | | mistral-7b-instruct-v0.2.F16.llamafile | llama 7B F16 | 13.49 GiB | 7.24 B | CPU | 8 | tg16 | 2.41 | --> <!-- 此外我還在 ARM 架構之下的 `Apple M1 Pro` 進行測試。 `Apple M1 Pro` | prompt (tok/sec) | eval (tok/sec) | model | weights data type | hardware | software | | ---------------- | -------------- | -------------- | ----------------- | ----------- | ------------------- | | 398 | 53 | TinyLlama 1.1B | q4_0 | M1 Pro | llamafile-0.8 | | 72 | 50 | TinyLlama 1.1B | q4_0 | M1 Pro | llama.cpp 2024-5-23 | | 327 | 30 | TinyLlama 1.1B | q8_0 | M1 Pro | llamafile-0.8 | | 72 | 30 | TinyLlama 1.1B | q8_0 | M1 Pro | llama.cpp 2024-5-23 | | 318 | 17 | TinyLlama 1.1B | FP16 | M1 Pro | llamafile-0.8 | | 72 | 16 | TinyLlama 1.1B | FP16 | M1 Pro | llama.cpp 2024-5-23 | #### llama.cpp | model | size | params | backend | ngl | test | t/s | | ------------- | ----------:| ------:| ------- | ---:| -----:| --------------:| | llama 1B Q4_0 | 606.53 MiB | 1.10 B | Metal | 99 | pp512 | 1678.39 ± 9.74 | | llama 1B Q4_0 | 606.53 MiB | 1.10 B | Metal | 99 | tg16 | 131.38 ± 12.51 | | llama 1B Q8_0 | 1.09 GiB | 1.10 B | Metal | 99 | pp512 | 1650.29 ± 6.80 | | llama 1B Q8_0 | 1.09 GiB | 1.10 B | Metal | 99 | tg16 | 102.91 ± 0.76 | | llama 1B F16 | 2.05 GiB | 1.10 B | Metal | 99 | pp512 | 1812.96 ± 4.95 | | llama 1B F16 | 2.05 GiB | 1.10 B | Metal | 99 | tg16 | 66.19 ± 0.22 | #### llamafile | model_filename | model | size | params | backend | threads | test | t/s | | ---------------------------------------:| ------------- | ----------:| ------:| ------- | -------:| -----:| ------:| | TinyLlama-1.1B-Chat-v1.0.Q4_0.llamafile | llama 1B Q4_0 | 606.53 MiB | 1.10 B | CPU | 8 | pp512 | 314.38 | | TinyLlama-1.1B-Chat-v1.0.Q4_0.llamafile | llama 1B Q4_0 | 606.53 MiB | 1.10 B | CPU | 8 | tg16 | 61.18 | | tinyLlama-1.1B-Chat-v1.0.Q8_0.llamafile | llama 1B Q8_0 | 1.09 GiB | 1.10 B | CPU | 8 | pp512 | 359.48 | | tinyLlama-1.1B-Chat-v1.0.Q8_0.llamafile | llama 1B Q8_0 | 1.09 GiB | 1.10 B | CPU | 8 | tg16 | 73.94 | | TinyLlama-1.1B-Chat-v1.0.F16.llamafile | llama 1B F16 | 2.05 GiB | 1.10 B | CPU | 8 | pp512 | 362.40 | | TinyLlama-1.1B-Chat-v1.0.F16.llamafile | llama 1B F16 | 2.05 GiB | 1.10 B | CPU | 8 | tg16 | 48.06 | --> ## TODO: 影響 LLaMA 推理速度的因素 > 找出其中效能瓶頸的地方,可能是矩陣運算,或是記憶體 (特別是 data model),予以量化,善用 perf 和 Ftrace 等工具 ### 矩陣相乘 使用 perf 依照 `cpu-cycles`,`faults`,`cache-references`,`cache-misses` 進行比較 ```bash $ sudo perf record -e cpu-cycles,faults,cache-references,cache-misses ./llama-bench -m models/tinyllama-1.1b-chat-v1.0.Q4_0.gguf $ sudo perf report ``` 在 llama.cpp 併入 llamafile 的 sgemm 之前,也就是 [commit dbceec8](https://github.com/ggerganov/llama.cpp/commit/dbceec87c0221ec952e69448df6a71f1372a7487) 的 backend cpu 實作手法,整個程式運作都的計算熱點會是矩陣乘法: ``` # Samples: 1M of event 'cpu_core/cpu-cycles/' 53.96% ,llama-bench,llama-bench ,[.] ggml_graph_compute_thread 37.68% ,llama-bench,llama-bench ,[.] ggml_vec_dot_q4_0_q8_0 1.86% ,llama-bench,llama-bench ,[.] ggml_vec_dot_q6_K_q8_K 1.60% ,llama-bench,llama-bench ,[.] ggml_vec_dot_f16 0.66% ,llama-bench,llama-bench ,[.] ggml_compute_forward_mul_mat # Samples: 699K of event 'cpu_atom/cpu-cycles/' 78.01% ,llama-bench,llama-bench ,[.] ggml_vec_dot_q4_0_q8_0 6.88% ,llama-bench,llama-bench ,[.] ggml_vec_dot_f16 6.21% ,llama-bench,llama-bench ,[.] ggml_graph_compute_thread 2.67% ,llama-bench,llama-bench ,[.] ggml_vec_dot_q6_K_q8_K 0.95% ,llama-bench,llama-bench ,[.] ggml_compute_forward_mul_mat 0.74% ,llama-bench,llama-bench ,[.] ggml_compute_forward_soft_max ``` ### 矩陣計算底層程式 在 llama.cpp 中有 gg 作者所撰寫的手法是簡單的將矩陣轉為內積去做計算,由上述提到的 $C=BA^T$ 並且在量化的時候便已經將 $A$ 向量化排列在連續記憶體之中。有很多開源的 BLAS libraries 有很純熟的矩陣相乘手法,但是放在已經向量化的場景反而會降低速度,因為這些 BLAS libraries 有很多複雜的手段會增加許多延遲,是對應各層面的矩陣相乘場景,而 gg 作者的向量內積是為了這個專案特別設計的。 其中 `ggml_vec_dot_q4_0_q8_0()` 是實作整個向量內積的底層程式。但是細看程式碼,先以矩陣相乘的演算法的角度(SIMD 等技巧先忽略),我們可以發現這是一個 q4_0 乘上 q8_0 的混合精度乘法: ```c void ggml_vec_dot_q4_0_q8_0(int n, float * restrict s, size_t bs, const void * restrict vx, size_t bx, const void * restrict vy, size_t by, int nrc) { const int qk = QK8_0; const int nb = n / qk; const block_q4_0 * restrict x = vx; const block_q8_0 * restrict y = vy; // scalar float sumf = 0.0; for (int i = 0; i < nb; i++) { int sumi = 0; for (int j = 0; j < qk/2; ++j) { const int v0 = (x[i].qs[j] & 0x0F) - 8; const int v1 = (x[i].qs[j] >> 4) - 8; sumi += (v0 * y[i].qs[j]) + (v1 * y[i].qs[j + qk/2]); } sumf += sumi*GGML_FP16_TO_FP32(x[i].d)*GGML_FP16_TO_FP32(y[i].d); // multipyy the scalars } *s = sumf; } ``` 此類函式是整個專案中負責最重要的矩陣計算,所以 gg 作者希望可以將這段程式最佳化。它負責計算 `z = x * y`,其中 `x` 為量化完的數值,`y` 是 FP32,`z` 為 FP32。 這邊就令人好奇為什麼是 q4_0 * q8_0 的混合精度乘法呢?為什麼不是直覺的 q4_0 * q4_0 呢? 在一開始的確是使用 `ggml_vec_dot_q4_0()` 這個函式。 ```c typedef struct { ggml_half d; // delta uint8_t qs[QK4_0 / 2]; // nibbles / quants } block_q4_0; ``` ```c const block_q4_0 * restrict x = vx; const block_q4_0 * restrict y = vy; float sumf = 0.0; ... for (int i = 0; i < nb; i++) { const float d0 = x[i].d; const float d1 = y[i].d; const uint8_t * restrict p0 = x[i].qs; const uint8_t * restrict p1 = y[i].qs; int sumi = 0; for (int j = 0; j < QK/2; j++) { const uint8_t v0 = p0[j]; const uint8_t v1 = p1[j]; const int8_t i0 = (int8_t) (v0 & 0xf) - 8; const int8_t i1 = (int8_t) (v0 >> 4) - 8; const int8_t i2 = (int8_t) (v1 & 0xf) - 8; const int8_t i3 = (int8_t) (v1 >> 4) - 8; sumi += i0*i2 + i1*i3; } sumf += d0 * d1 * sumi; ``` 在 caller `ggml_compute_forward_mul_mat` 發現原先在計算 z = x * y 時,會先將 y (FP32) 做一次 q4_0 的量化。 ```c if (params->type == GGML_TASK_INIT) { char * wdata = params->wdata; const size_t row_size = ne10*GGML_TYPE_SIZE[type]/GGML_BLCK_SIZE[type]; for (int64_t i13 = 0; i13 < ne13; ++i13) { for (int64_t i12 = 0; i12 < ne12; ++i12) { for (int64_t i11 = 0; i11 < ne11; ++i11) { quantize_row_q((float *)((char *) src1->data + i13*nb13 + i12*nb12 + i11*nb11), (void *) wdata, ne10); wdata += row_size; } } } ``` 在這邊可以看到原本在計算 `y` 的步驟是: <!-- ```graphviz digraph S{ rankdir=LR node[shape=box] x[label="x\nQ4_0"] y[label="y\nFP32"] z[label="z\nFP32"] tmp[label="=", color=white] z->tmp->x->y[style=invis] subgraph{ subgraph { rank=same; y; tmp2 [label="y\nQ4_0"]; tmp3 [label="y\nint8_t"] } y->tmp2->tmp3 subgraph { x tmp4 [style=invis] mul [label="*"] } x->tmp4->mul } } ``` --> ``` z(FP32) = x(Q4_0) * y(FP32) ^ | | | | |------這個 Q4_0 量化過程會導致精度的損失 | | | | x(int8_t) y(Q4_0) | | | | | |------內積時還是要將 4 bit 擴充成 int8_t 做計算 | | | --------------- * ---------- y(int8_t) ``` 改為 ``` z(FP32) = x(Q4_0) * y(FP32) ^ | | | | |------ 改為 Q8_0 的量化 | | | | x(int8_t) y(Q8_0) | | | | | | | | | --------------- * ---------- y(int8_t) ``` 這個步驟在每一個向量相乘都會發生,且因為他是一個底層運算函式,不斷的做量化會不斷放大誤差,導致模型的 perplexity 居高不下。 >But at the end, avoiding quantization of intermediate results will always be more important than any improvements to model weight quantization. 這是在此專案場景矩陣相乘遇到的困難點,所以才衍生出之後的 `ggml_vec_dot_q4_0_q8_0()`,降低了量化過程中所帶來的精度喪失。在之後底層運算的實作皆是使用這個向量乘法演算法。 :::danger 建立數學模型,探討量化過程的精確度損失和補償方案。 數學分析! ::: ```c void ggml_vec_dot_q4_0_q8_0(int n, float * restrict s, size_t bs, const void * restrict vx, size_t bx, const void * restrict vy, size_t by, int nrc) { const int qk = QK8_0; const int nb = n / qk; assert(n % qk == 0); #if defined(__ARM_FEATURE_MATMUL_INT8) assert((nrc == 2) || (nrc == 1)); #else assert(nrc == 1); #endif UNUSED(nrc); UNUSED(bx); UNUSED(by); UNUSED(bs); const block_q4_0 * restrict x = vx; const block_q8_0 * restrict y = vy; // scalar float sumf = 0.0; for (int i = 0; i < nb; i++) { int sumi = 0; for (int j = 0; j < qk/2; ++j) { const int v0 = (x[i].qs[j] & 0x0F) - 8; const int v1 = (x[i].qs[j] >> 4) - 8; sumi += (v0 * y[i].qs[j]) + (v1 * y[i].qs[j + qk/2]); } sumf += sumi*GGML_FP16_TO_FP32(x[i].d)*GGML_FP16_TO_FP32(y[i].d); } *s = sumf; } ``` ### 矩陣計算 thread 執行緒的工作分為三種,`GGML_TASK_TYPE_INIT`,`GGML_TASK_TYPE_COMPUTE`,`GGML_TASK_TYPE_FINALIZE`,分別處理工作的初始化、矩陣的計算以及不同執行緒結果的收尾。 在這邊使用的同步機制是 spinlock,執行緒會上鎖並分頭計算不同區塊的矩陣。這邊並行程式是為了處理底層計算的部份,因此沒有使用 futexes 或是 semaphores,因為 Linux 核心的排程會降低計算效能。 ```c static void ggml_graph_compute_thread_sync_node(int * node_n, struct ggml_compute_state * state, const bool do_yield) { // wait for other threads to finish const int last_node_n = * node_n; while (true) { if (do_yield) { sched_yield(); } * node_n = atomic_load(&state->shared->node_n); if (* node_n != last_node_n) break; } } ``` 首先紀錄 node_n 編號。這段程式會一直查看佇列中是否有新的工作,使用 atomic_load 查看若是 node_n 值有所不同,表示其他執行緒已更新了節點,可以開始處理其他節點了。 如果設定 `do_yeild` 為真它會放棄目前 CPU 然後移到排程佇列最末端,讓其他執行緒優先執行。 :::warning 搭配 taskset 觀察多執行緒程式的執行。 ::: #### `ggml_graph_compute_thread()` 此函式負責的處理並行矩陣計算所要的執行緒所使用的所有動作,逐一解釋: 翻閱 `atomic_fetch_sub()` man page 說明如下: >The atomic_fetch_sub() macro subtracts the value operand from atomic variable object and returns the original contents of the atomic variable. ```c if (atomic_fetch_sub(&state->shared->n_active, 1) == 1) ``` 所以如果 `atomic_fetch_sub()` 回傳是 1 的話,一方面執行 n_active 執行緒數減一,一方面表示這是矩陣乘法多執行緒中的最後一個執行緒,其他工作的執行緒已完成,最後一個執行緒會有一個收尾的動作。以下這一段是處理收尾的主要程式: ```c if (node_n != -1) { /* FINALIZE */ struct ggml_tensor * node = cgraph->nodes[node_n]; if (GGML_OP_HAS_FINALIZE[node->op]) { params.nth = ggml_get_n_tasks(node, n_threads, state->shared->n_threads); ggml_compute_forward(&params, node); } ggml_graph_compute_perf_stats_node(node, state->shared); } ``` `node_n` 初始化為 -1。調用 `ggml_get_n_tasks()` (存放所有動作會需要多少執行緒)來獲取現在的動作會需要多少執行緒。並使用 `ggml_compute_forward()` 完成最後的計算。 ## TODO: 探討 llamafile 相較於 llama.cpp 進行哪些調整,得以加速? > 重現實驗並分析 sgemm.cpp 是 llamafile 的矩陣計算模組,大幅提昇 prompt evaluation 的速度,這個矩陣計算模組也併入 llam.cpp 之中,是目前 llam.cpp 的預設計算模組。 在我使用原本 `vec_dot()` 版本進行效能測試時它預設開啟的執行緒數量是 12,但是當我到 [commit 8cc91dc](https://github.com/ggerganov/llama.cpp/commit/8cc91dc63c0df397d644a581b2cbeea74eb51ae0) 測試發現這邊預設的執行緒是 8。後來發現這邊也有緣由。在原本的版本使用 `get_num_physical_cores()` 去判斷現在的設備有多少邏輯核心,如果邏輯核心數超過 4 個便會使用 `邏輯核心數 / 2` 個執行緒。 以下是我的設備為例有 24 核,所以在原本向量矩陣相乘的方法將 12 配置給執行中的程式。 而 sgemm.cpp 改成 8 是因為 `13th Gen Intel(R) Core(TM) i7-13700` 其中 24 個核心包含 8 個雙執行緒的效能核心與 8 個單執行緒的效率核心,希望避免將大量的矩陣計算移到效率核心去做計算。 [執行緒選取的更動](https://github.com/ggerganov/llama.cpp/commit/8cc91dc63c0df397d644a581b2cbeea74eb51ae0#diff-201cbc8fd17750764ed4a0862232e81503550c201995e16dc2e2766754eaa57a) 這邊有對核心的選取,我對這邊核心相關的程式細節不了解(`cpuid()`, `is_hybrid_cpu()`, `is_running_on_efficiency_core()`),希望知道人可以補充。這邊使用 `pthread_setaffinity_np()` 將設定在效能核心上。 ```c int get_math_cpu_count() { #if defined(__x86_64__) && defined(__linux__) int cpu_count = sysconf(_SC_NPROCESSORS_ONLN); if (cpu_count < 1) { return get_num_physical_cores(); } if (is_hybrid_cpu()) { cpu_set_t affinity; if (!pthread_getaffinity_np(pthread_self(), sizeof(affinity), &affinity)) { int result = count_math_cpus(cpu_count); pthread_setaffinity_np(pthread_self(), sizeof(affinity), &affinity); if (result > 0) { return result; } } } #endif return get_num_physical_cores(); } ``` > On success, these functions return 0; on error, they return a nonzero error number. > [name=man page] ### 矩陣相乘 llama.cpp 支援許多種 backend,也就是多種矩陣相乘的計算模組,原有支援 `CPU`,`OpenCL`,`Vulkan`,`Kompute`,`Metal`,`GPU BLAS`,`BLAS`。在 [commit 8cc91dc](https://github.com/ggerganov/llama.cpp/commit/8cc91dc63c0df397d644a581b2cbeea74eb51ae0) 加入 sgemm.cpp 這個乘法模組。文章中有提到其在 `Q8` 與 `FP16` 有最好的效果,那可以從其計算熱點觀察。 ```c 66.22% llama-bench llama-bench [.] void (anonymous namespace)::tinyBLAS_Q0_AVX<block_q8_0, block_q8_0, float>::gemm<4, 1>(long, long, long, long) ◆ 20.80% llama-bench llama-bench [.] void (anonymous namespace)::tinyBLAS_Q0_AVX<block_q8_0, block_q8_0, float>::gemm<4, 2>(long, long, long, long) 6.46% llama-bench llama-bench [.] ggml_graph_compute_thread ▒ 1.43% llama-bench llama-bench [.] void (anonymous namespace)::tinyBLAS<8, float __vector(8), float __vector(8), unsigned short, float, float>::gemm<4, 3>(long, long, long, long) ▒ 1.29% llama-bench llama-bench [.] ggml_vec_dot_f16 ▒ 0.56% llama-bench llama-bench [.] ggml_compute_forward_soft_max ▒ 0.56% llama-bench llama-bench [.] ggml_compute_forward_mul_mat ``` gg 作者的 `vec_dot()` 向量化方法目前最佳化 token 生成的速度,但是在 llam.cpp 中還是遇到瓶頸,那便是消化輸入的 token 的速度。畢竟在要總結大量文字或是在 LLaVa 消化圖片時還是需要依靠一個完整的矩陣相乘。這也是為什麼現在依舊有許多外部 BLAS Library。 可以觀察到這邊運用到很多像是之前的 `vec_dot_q8_0_q8_0()` 的函式,其實兩者的實作方式有些許相似,以下解釋 sgemm 是怎麼運作的。 ### sgemm.cpp 這個矩陣模組的核心是 `mnpack()` 這一個函式,其作用是將矩陣分解成更小的塊。依據這些分解的小區塊再進入 `gemm` 去運算。 ```cpp class tinyBLAS { public: tinyBLAS(int k, const TA *A, int lda, const TB *B, int ldb, TC *C, int ldc, int ith, int nth) : A(A), B(B), C(C), k(k), lda(lda), ldb(ldb), ldc(ldc), ith(ith), nth(nth) { } void matmul(int m, int n, int task) { if (task == GGML_TASK_TYPE_COMPUTE) mnpack(0, m, 0, n); } private: NOINLINE void mnpack(int m0, int m, int n0, int n) { int mc, nc, mp, np; if (m - m0 <= 0 || n - n0 <= 0) return; if (VECTOR_REGISTERS >= 32 && n - n0 >= 5 && m - m0 >= 5) { mc = 5; nc = 5; gemm5x5(m0, m, n0, n); } else if (n - n0 >= 4 && m - m0 >= 3) { mc = 3; nc = 4; gemm3x4(m0, m, n0, n); } else if (n - n0 >= 4) { mc = 1; nc = 4; gemm1x4(m0, m, n0, n); } else if (m - m0 >= 4) { mc = 4; nc = 1; gemm4x1(m0, m, n0, n); } else { mc = 1; nc = 1; gemm1x1(m0, m, n0, n); } mp = m0 + (m - m0) / mc * mc; np = n0 + (n - n0) / nc * nc; mnpack(mp, m, n0, np); mnpack(m0, mp, np, n); mnpack(mp, m, np, n); } ``` 這邊 `m` 代表行數 `n` 代表列數。在 `mnpack()` 之中會使用遞迴方式分配每一個矩陣計算到它相對應 `llmmnxn` 去做計算。(以上的程式是第一次合併進 llama.cpp 的版本 [commit 8cc91dc](https://github.com/ggerganov/llama.cpp/commit/8cc91dc63c0df397d644a581b2cbeea74eb51ae0) 中的版本,後續不斷有新的 `llmmnxn` 不斷更新)。 那這樣的分配的確增加原本一個一個 `block_qn_0` 去做計算更快。它做的更動還有加入更多的 SIMD 指令,若是以演算法的角度去看 `vec_dot()` 也是基本的 $O(n^3)$ 做計算: ```c template <typename T> void LLMM(int m, int n, int k, const T *A, int lda, const T *B, int ldb, T *C, int ldc) { for (int i = 0; i < m; ++i) for (int j = 0; j < n; ++j) { T d = 0; for (int l = 0; l < k; ++l) d += A[lda * i + l] * B[ldb * j + l]; C[ldc * j + i] = d; } } ``` 在 llam.cpp 中使用的技巧是將最裡面的迴圈使用 SIMD 指令展開。 ```c for (int i = 0; i < m; ++i) for (int j = 0; j < n; ++j) { __m256 c0 = _mm256_setzero_ps(); __m256 c1 = _mm256_setzero_ps(); for (int l = 0; l < k; l += 16) { c0 = _mm256_fmadd_ps(_mm256_loadu_ps(A + lda * i + l + 0), _mm256_loadu_ps(B + ldb * j + l + 0), c0); c1 = _mm256_fmadd_ps(_mm256_loadu_ps(A + lda * i + l + 8), _mm256_loadu_ps(B + ldb * j + l + 8), c1); } C[ldc * j + i] = hsum(c0) + hsum(c1); } ``` 但實際上效能提升有限,因為編譯器在最佳化時,即使原本沒有 SIMD 的程式,也會做類似的處理。而在 sgemm.cpp 中,中間層的迴圈使用 SIMD 展開。以 `gemm1x4` 為範例,直接將三層迴圈中的中下兩層直接使用 SIMD 展開: ```c NOINLINE void gemm1x4(int m0, int m, int n0, int n) { BEGIN_KERNEL(1, 4) __m256 c0 = _mm256_setzero_ps(); __m256 c1 = _mm256_setzero_ps(); __m256 c2 = _mm256_setzero_ps(); __m256 c3 = _mm256_setzero_ps(); const TB *Bp0 = B + ldb * (j + 0); const TB *Bp1 = B + ldb * (j + 1); const TB *Bp2 = B + ldb * (j + 2); const TB *Bp3 = B + ldb * (j + 3); const TA *Ap = A + lda * i; for (int l = 0; l < k; ++l) { float da0 = unhalf(Ap[l].d); __m256i f = load(Ap + l); __m256i u = _mm256_sign_epi8(f, f); // make all the number >=0 __m256 d0 = _mm256_set1_ps(unhalf(Bp0[l].d) * da0); // b.scalar * a.scalar __m256 d1 = _mm256_set1_ps(unhalf(Bp1[l].d) * da0); // make it a vector __m256 d2 = _mm256_set1_ps(unhalf(Bp2[l].d) * da0); __m256 d3 = _mm256_set1_ps(unhalf(Bp3[l].d) * da0); __m256 g0 = updot(u, _mm256_sign_epi8(load(Bp0 + l), f)); __m256 g1 = updot(u, _mm256_sign_epi8(load(Bp1 + l), f)); __m256 g2 = updot(u, _mm256_sign_epi8(load(Bp2 + l), f)); __m256 g3 = updot(u, _mm256_sign_epi8(load(Bp3 + l), f)); c0 = madd(d0, g0, c0); c1 = madd(d1, g1, c1); c2 = madd(d2, g2, c2); c3 = madd(d3, g3, c3); } C[ldc * (j + 0) + i] = hsum(c0); C[ldc * (j + 1) + i] = hsum(c1); C[ldc * (j + 2) + i] = hsum(c2); C[ldc * (j + 3) + i] = hsum(c3); END_KERNEL() } ``` 假設在 `Q8_0` 中,以 usigned int8 儲存,一次計算 256 個位元,會處理 256 / 8 = 32 筆資料,而 gemm1x4 就會處理 32 * 128 大小的矩陣。以此類推,尚有 `gemm4x3()`,`gemm4x1()`,`gemm1x4()`,`gemm1x1()`。可以說 sgemm 便是將大矩陣不斷分割成小矩陣進行 SIMD 並行指令計算。這也是得益於大型語言模型穩定的矩陣大小,讓針對性的此專案的矩陣相乘得以發展。 ## TODO: 選定 data type 並聚焦在 matmul 實作 > w/ SIMD, OpenMP, oneMKL, OpenBLAS, …) 的效益 => 我們需要有個 wrapper/tester,得以涵蓋現有實作並整合進 llama.cpp,在 Linux 平台驗證,perf (搭配第六次作業提到的效能分析方式,排除系統干擾) ### 實驗暫存 `make LLAMA_NO_ACCELERATE=1` tiny-llama q4_0 | model | size | params | backend | threads | test | t/s | | ------------- | ----------:| ------:| ------- | -------:| -----:| --------------:| | llama 1B Q4_0 | 606.53 MiB | 1.10 B | CPU | 8 | pp512 | 235.26 ± 13.62 | | llama 1B Q4_0 | 606.53 MiB | 1.10 B | CPU | 8 | tg16 | 43.79 ± 4.99 | ### SIMD ### OpenMP OpenMP 在 llama.cpp 的實作在於矩陣相乘 backend 使用 [BLIS](https://github.com/flame/blis) 這一個專案中 在編譯 BLIS 的時候使用 `make LLAMA_BLIS=1` 可以順利編譯,但是使用 `./llama-bench` 出現 `./llama-bench: error while loading shared libraries: libblis.so.4: cannot open shared object file: No such file or directory` ### oneMKL ### OpenBLAS `make LLAMA_OPENBLAS=1` tiny-llama q4_0 | model | size | params | backend | threads | test | t/s | | ------------- | ----------:| ------:| ------- | -------:| -----:| ------------:| | tinyllama 1B Q4_0 | 606.53 MiB | 1.10 B | BLAS | 8 | pp512 | 72.94 ± 0.08 | | tinyllama 1B Q4_0 | 606.53 MiB | 1.10 B | BLAS | 8 | tg16 | 50.04 ± 0.89 | ### CUDA 這邊順便附上在 NVIDIA GeForce GTX 1070 的實驗,使用 `make LLAMA_CUDA=1` 去編譯,會出現 error,跟著他的步驟去 Nvidia 官往查詢顯示卡的計算能力 https://developer.nvidia.com/cuda-gpus ,這邊看到 GTX 1070 的計算能力為 6.1,便使用命令 `export CUDA_DOCKER_ARCH=compute_61`,即可完成編譯。 `ggml_cuda_init: GGML_CUDA_FORCE_MMQ: no ggml_cuda_init: CUDA_USE_TENSOR_CORES: yes ggml_cuda_init: found 1 CUDA devices: Device 0: NVIDIA GeForce GTX 1070, compute capability 6.1, VMM: yes` | model | size | params | backend | test | t/s | | ------------- | ----------:| ------:| ------- | -----:| --------------:| | tinyllama 1B Q4_0 | 606.53 MiB | 1.10 B | CUDA | pp512 | 505.89 ± 77.20 | | tinyllama 1B Q4_0 | 606.53 MiB | 1.10 B | CUDA | tg16 | 24.76 ± 0.33 | ## TODO: 提出更快的 matmul > 搭配作業系統核心層級的調整,讓運算能佔用更多 CPU 資源 ## 數學分析 探討量化過程的精確度損失 令 n 為原始數值,max 為該區塊的最大值 以`q4_0` 為例: 整個量化與解量化所產生的誤差為: $n-((n/\frac{max}{-8}+8.5)-8)*\frac{max}{-8}$ $=n-(\frac{-8}{max}+8.5-8)*\frac{max}{-8}$ $=n-(\frac{-8}{max}+0.5)*\frac{max}{-8}$ $=n-(n+\frac{max}{-8})$ $=\frac{max}{-8}$ 在 `q4_0 * q4_0` 時 令前項為 $n_1$、block 中的最大值為 $max_1$,後項為 $n_2$、最大值為 $max_2$ 其相乘所帶來的誤差為: $n_1*n_2-((n_1/\frac{max_1}{-8}+8.5)-8)*((n_2/\frac{max_2}{-8}+8.5)-8)*(\frac{max_1}{-8}*\frac{max_2}{-8})$ $=n_1*n_2-(\frac{-8n_1}{max_1}+0.5)*(\frac{-8n_2}{max_2}+0.5)*(\frac{max_1}{-8}*\frac{max_2}{-8})$ $=n_1*n_2-(\frac{64n_1n_2}{max1max2}+\frac{-4n_1}{max_1}+\frac{-4n_2}{max_2}+0.25)*(\frac{max_1}{-8}*\frac{max_2}{-8})$ $=n_1*n_2-n_1*n_2+\frac{max_2*n_1}{16}+\frac{max_1*n_2}{16}+\frac{max_1*max_2}{64}$ $=\frac{max_1*n_2}{16}+\frac{max_2*n_1}{16}+\frac{max_1*max_2}{64}$ 在 `q4_0 * q8_0` 時,誤差為: 令前項為 $n_1$、block 中的最大值為 $max_1$,後項為 $n_2$、最大值為 $max_2$ $n_1*n_2-((n_1/\frac{max_1}{-8}+8.5)-8)(round(n_2/\frac{max}{127}))*(\frac{max_1}{-8}*\frac{max_2}{127})$ $=n_1*n_2-(\frac{-8n_1}{max_1}+0.5)*(round(\frac{127n_2}{max_2}))*(\frac{max_1}{-8}*\frac{max_2}{127})$ $=n_1*n_2-(\frac{-8n_1}{max_1}+0.5)*(\lfloor\frac{127n_2}{max_2}+0.5\rfloor)*(\frac{max_1}{-8}*\frac{max_2}{127})$ $=n_1*n_2-(\frac{max_1}{-8}*(\frac{-8n_1}{max_1}+0.5))*(\frac{max_2}{127}*\lfloor\frac{127n_2}{max_2}+0.5\rfloor)$ $=n_1*n_2-(n_1+\frac{max_1}{-16})*(\frac{max_2}{127}*(\frac{127n_2}{max_2}\pm1))$ 使用 $\pm1$ 表示 $n_2>0,n_2<0$ 的最大修正 $=n_1*n_2-(n_1+\frac{max_1}{-16})*((n_2\pm\frac{max_2}{127}))$ $=n_1*n_2-n_1*n_2\pm\frac{max_2*n_1}{127}-\frac{max_1*n_2}{16}\pm\frac{max_1*max_2}{-2032}$ $=\pm\frac{max_2*n_1}{127}-\frac{max_1*n_2}{16}\pm\frac{max_1*max_2}{-2032}$ 使用代數 $k\leq\frac{127n_2}{max_2}+0.5<k+1$ ? 為何 `q8_0` 做量化使用 Symmetric quantize,而不是跟 `q4_0` 一樣使用 Affine Quantizer? ## 參考 [#909](https://github.com/ggerganov/llama.cpp/issues/909) [#896](https://github.com/ggerganov/llama.cpp/pull/896)

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully