當 llama.cpp 在 CPU 上執行 LLaMA 2 模型時,它可以輕鬆消耗掉 400% 的 CPU 核心資源。這些 CPU 時間究竟花在了哪裡?我們使用 OpenResty XRay 對一個正在執行量化 7B 模型的、未經修改的 llama.cpp 程序進行了實時剖析,並將 CPU 佔用追蹤到兩條最主要的程式碼路徑:ggml_compute_forward_mul_mat(通用矩陣乘法)和 ggml_vec_dot_q4_K_q8_Kk_quants.c 中的量化向量點積)。

本文將展示完整的剖析過程——從觀察到高 CPU 佔用開始,一直定位到具體的原始檔和行號。

執行 llama.cpp 並觀察到 400% 的 CPU 使用率

首先,編譯 llama.cpp 時開啟除錯符號,以便剖析工具能夠解析函式名和原始碼位置:

cd llama.cpp
make -j4 OPT="-g -O3"

然後使用量化後的 LLaMA 2 7B 模型執行 main 程式。LLaMA 2 是 Meta 公司開源的大語言模型。這裡我們使用 Q4_K_M 量化版本,它將 7B 模型壓縮到約 4 GB——小到足以透過 llama.cpp 完全在 CPU 上執行:

./main -m models/llama-2-7b-chat.ggmlv3.q4_K_M.bin -n 4096 -p "Linux"

其中 -n 4096 選項指定生成的 token 數,-p "Linux" 則是用來生成內容的提示詞。

程序載入模型後開始生成文字。請注意輸出中的 system_info 一行——它顯示 n_threads = 4AVX = 1AVX2 = 1 以及 BLAS = 0,表示當前構建使用了 AVX2 SIMD 指令,但沒有連結外部 BLAS 庫:

llama.cpp 正在生成文字,顯示模型載入資訊和系統能力(AVX2 已啟用、BLAS 未啟用、4 個執行緒)

在另一個終端中,top 命令顯示 main 程序消耗了接近 400% 的 CPU——4 個核心全部被打滿:

top 顯示 llama.cpp 的 main 程序 CPU 佔用達 396%

問題來了:llama.cpp 內部究竟是哪些 C++ 函式造成了這麼高的 CPU 佔用?

使用 OpenResty XRay 剖析 llama.cpp 的 CPU 佔用

我們使用 OpenResty XRay 來回答這個問題。OpenResty XRay 可以透過動態追蹤技術直接附加到正在執行的、未經修改的程序上並生成 CPU 火焰圖——無需重新編譯,也無需插樁。

在 OpenResty XRay 的 Web 控制檯中,進入 Guided Analysis(引導式分析)頁面,選擇 High CPU usage(高 CPU 使用率)作為問題型別,然後從程序列表中選擇 llama.cpp 的 main 程序。控制檯顯示該程序的 CPU 佔用為 320.4%,並且完整的命令列清晰可見:

OpenResty XRay 程序列表顯示 llama.cpp 的 main 程序 CPU 佔用 320.4%,可以看到完整命令列和模型路徑

將應用程式型別設定為 C/C++,最大分析時間保持預設的 300 秒不變。點選 Start analyzing(開始分析)後,系統會持續執行多輪取樣分析。對這個案例來說,兩輪分析就足夠了。停止分析後,系統會自動生成分析報告。

分析結果:最熱的 C++ 程式碼路徑

自動生成的報告識別出三條熱點 C++ 程式碼路徑,每條路徑都標註了佔總 CPU 時間的百分比和完整的呼叫鏈:

OpenResty XRay 分析報告顯示 llama.cpp 的三條 CPU 熱點路徑:#1 佔 21.1%,#2 佔 17.4%,#3 佔 13.4%

第一名:ggml_compute_forward_mul_mat——佔 CPU 時間的 21.1%

最熱的程式碼路徑指向 ggml.c 中的 ggml_compute_forward_mul_mat。這是 ggml 庫中的通用矩陣乘法(GEMM)核心——它是 Transformer 推理中注意力機制和前饋層背後的核心運算。而其上一級呼叫它的函式 ggml_graph_compute_thread 負責進行單個執行緒的計算:

高亮顯示的第一熱點 C++ 程式碼路徑:ggml_compute_forward_mul_mat 佔 21.1%,由 ggml_graph_compute_thread 呼叫

點選 More(更多)展開詳情,可以看到這條程式碼路徑所依據的 CPU 火焰圖。火焰圖以視覺化的方式呈現了完整的呼叫棧,越寬的框表示消耗的 CPU 時間越多:

llama.cpp 的 CPU 火焰圖,呼叫棧底部最寬的框是 ggml_compute_forward_mul_mat

OpenResty XRay 還會為這條程式碼路徑自動生成解釋說明和最佳化建議。它指出 ggml_compute_forward_mul_mat 計算的是兩個張量的矩陣乘法,並給出了包括並行化、記憶體訪問最佳化,以及使用 BLAS 或 LAPACK 等最佳化庫在內的多種最佳化策略:

自動生成的解釋和最佳化建議:並行化、記憶體訪問最佳化、演算法最佳化,以及使用 BLAS/LAPACK 庫

其中 BLAS 這條建議在這裡尤其切中要害——回想一下前面 system_info 輸出中的 BLAS = 0,這說明 llama.cpp 用的是自己實現的 GEMM,而不是經過最佳化的 BLAS 庫。在編譯時啟用 OpenBLAS 或 Intel MKL,有望顯著降低這條熱點路徑的 CPU 佔比。

第二名:ggml_vec_dot_q4_K_q8_K——佔 CPU 時間的 17.4%

第二熱的程式碼路徑指向 k_quants.c 中的 ggml_vec_dot_q4_K_q8_K

高亮顯示的第二熱點 C++ 程式碼路徑:ggml_vec_dot_q4_K_q8_K 佔 17.4%,下方還可以看到佔 13.4% 的第三條路徑

這個函式計算兩個量化向量的點積——K 採用 4 位元量化(q4_K),Q 採用 8 位元量化(q8_K)。量化點積正是 llama.cpp 能夠在 CPU 上跑動 7B 模型的關鍵所在——它帶來了速度和記憶體上的巨大節省:

第二條程式碼路徑的解釋:ggml_vec_dot_q4_K_q8_K 計算量化向量 K 和 Q 的點積,定義在 k_quants.c 中

從剖析報告定位到原始碼

OpenResty XRay 會把每個熱點函式對映回它的原始檔和行號。把滑鼠懸停在報告中 ggml_vec_dot_q4_K_q8_K 的函式框上,提示框會顯示確切位置:llama.cpp/k_quants.c 檔案的第 2608 行:

提示框顯示原始碼位置:檔案 llama.cpp/k_quants.c,行號 2608

開啟 k_quants.c,按照報告提示框中的指引跳轉到第 2608 行:

k_quants.c 第 2608 行的原始碼,位於 #elif defined AVX2 分支內,熱點行已標出

我們可以看到這行程式碼使用了 C 語言的位運算來對陣列的元素進行一些操作——從打包成 12 位元組的 scales 表示中解出量化的縮放因子:

高亮顯示的 k_quants.c 第 2608 行熱點程式碼:對 utmp 陣列元素的位運算

在狀態列中可以看到這行程式碼也確實在之前報告裡提到的 ggml_vec_dot_q4_K_q8_K 函式中:

編輯器狀態區域高亮顯示所在函式 ggml_vec_dot_q4_K_q8_K()

這種精確度——從一個正在執行的程序一路定位到消耗 CPU 的具體原始碼行——正是動態追蹤型剖析工具在分析 LLM 推理這類計算密集型應用的 CPU 去向時的價值所在。

常見問題

llama.cpp 為甚麼消耗這麼多 CPU?

llama.cpp 對大語言模型做推理時,主要的計算就是矩陣乘法和量化向量點積。在我們對 LLaMA 2 7B 執行過程的剖析中,CPU 佔用第一名是 ggml_compute_forward_mul_mat——ggml 庫中的通用矩陣乘法核心;第二名是 ggml_vec_dot_q4_K_q8_K,它計算兩個量化向量的點積。這些運算本身就是計算密集型的,在 CPU 上做推理很容易跑滿多個核心、達到 400% 的佔用。

如何剖析 llama.cpp 的 CPU 佔用?

一種方法是使用像 OpenResty XRay 這樣基於動態追蹤的剖析工具。它可以直接附加到正在執行的、未經修改的 llama.cpp 程序上並生成 CPU 火焰圖。火焰圖能精確揭示哪些 C++ 函式消耗的 CPU 時間最多——一直細化到原始檔和行號——而無需修改或插樁正在執行的應用程式。

llama.cpp 中最熱的 C++ 函式是哪些?

在我們對執行量化 LLaMA 2 7B 模型的 llama.cpp 的分析中,第一熱點程式碼路徑是 ggml_compute_forward_mul_mat(佔 CPU 時間的 21.1%),由 ggml_graph_compute_thread 呼叫;第二熱點路徑是 k_quants.c(第 2608 行)中的 ggml_vec_dot_q4_K_q8_K(佔 17.4%),它對量化陣列元素做位運算。加上第三條路徑(佔 13.4%),這三條程式碼路徑合計佔據了一半以上的 CPU 時間。

關於 OpenResty XRay

OpenResty XRay 是一個動態追蹤產品,它可以自動分析執行中的應用程式,以解決效能問題、行為問題和安全漏洞,並提供可行的建議。在底層實現上,OpenResty XRay 由我們的 Y 語言驅動,可以在不同環境下支援多種不同的執行時,如 Stap+、eBPF+、GDB 和 ODB。

關於本文和關聯影片

本文和相關聯的影片都是完全由我們的 OpenResty Showman 產品從一個簡單的劇本檔案自動生成的。

關於作者

章亦春是開源 OpenResty® 專案創始人兼 OpenResty Inc. 公司 CEO 和創始人。

章亦春(Github ID: agentzh),生於中國江蘇,現定居美國灣區。他是中國早期開源技術和文化的倡導者和領軍人物,曾供職於多家國際知名的高科技企業,如 Cloudflare、雅虎、阿里巴巴, 是 “邊緣計算“、”動態追蹤 “和 “機器程式設計 “的先驅,擁有超過 22 年的程式設計及 16 年的開源經驗。作為擁有超過 4000 萬全球域名使用者的開源專案的領導者。他基於其 OpenResty® 開源專案打造的高科技企業 OpenResty Inc. 位於美國矽谷中心。其主打的兩個產品 OpenResty XRay(利用動態追蹤技術的非侵入式的故障剖析和排除工具)和 OpenResty Edge(最適合微服務和分散式流量的全能型閘道器軟體),廣受全球眾多上市及大型企業青睞。在 OpenResty 以外,章亦春為多個開源專案貢獻了累計超過百萬行程式碼,其中包括,Linux 核心、Nginx、LuaJITGDBSystemTapLLVM、Perl 等,並編寫過 60 多個開源軟體庫。

關注我們

如果您喜歡本文,歡迎關注我們 OpenResty Inc. 公司的部落格網站 。也歡迎掃碼關注我們的微信公眾號:

我們的微信公眾號

翻譯

我們提供了英文版原文和中譯版(本文)。我們也歡迎讀者提供其他語言的翻譯版本,只要是全文翻譯不帶省略,我們都將會考慮採用,非常感謝!