当 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. 公司的博客网站 。也欢迎扫码关注我们的微信公众号:

我们的微信公众号

翻译

我们提供了英文版原文和中译版(本文)。我们也欢迎读者提供其他语言的翻译版本,只要是全文翻译不带省略,我们都将会考虑采用,非常感谢!