前言
上篇文章简要介绍了Arm Topdown工具的概要,本文将深入讲解其具体实现。本次实践将不再使用简单的 ls命令,而是通过 llama.cpp在ARM CPU上部署llama3 8B模型来进行实战演练, 测试环境为高通骁龙8650手机,采用1+3+2+2结构,超大核为X4 CPU,我们将模型绑定在超大核X4 CPU上执行,由于仅有一个核心参与运算,导致运行Llama3 8B模型时为算力受限。
本文针对每个测试项进行了详细说明,文章篇幅较长,略显枯燥,读者可选择跳过详细说明,直接阅读总结。
Arm的TopDown分析方法包含两个核心阶段:
- TopDown分析:通过检测与流水线延迟相关的性能指标,识别CPU性能瓶颈;
- 微架构探索:进一步分析瓶颈资源的利用率。
接下来将对这两个阶段进行详细拆解说
TopDown分析
笔者基于TopDown论文框架,结合Arm TopDown工具重新绘制了适用于Arm CPU的性能分析模型。
Top-Down分析方法的第一层分为以下四个关键步骤:
- 前端瓶颈(Frontend Bound) :指CPU前端无法及时从内存获取和解码指令,导致流水线空闲。
- 后端瓶颈(Backend Bound) :指CPU后端在执行阶段因资源不足或延迟而无法处理指令。
- 错误推测(Bad Speculation) :指CPU因错误预测分支或speculative操作而浪费资源。
- 退休(Retiring) :指指令成功完成并提交到执行结果队列的过程。
理论讲解难免枯燥,我们直接通过运行Llama3 8B模型的实测结果来直观展示分析效果。
taskset 80 ./topdown-tool --perf-path ~/bin/perf --cpu cortex-x4 -m Topdown_L1 /data/local/tmp/llama_cpp/bin/llama-cli -t 1 -no-cnv -m ${model} -n 10 -c 512 -p "the answer to the ultimate question of life, the universe, and everything is "
...
Stage 1 (Topdown metrics)
=========================
[Topdown Level 1]
Frontend Bound... 19.15% slots
Backend Bound.... 47.06% slots
Retiring......... 32.41% slots
Bad Speculation.. 1.39% slots
输出省略了llama.cpp本身的输出,以下为运行命令说明:
- 使用
taskset 80命令将CPU绑定至X4核心; - 通过
--perf-path ~/bin/perf指定perf工具路径(可替换为Android平台的simpleperf); - 通过
--cpu cortex-x4配置目标CPU为cortex-x4; - 通过
-m Topdown_L1启用Topdown分析的Level 1配置组; 其余参数为llama.cpp的常规运行参数,如输出10个token,此处不再赘述。
Top-Down分析结果显示,Backend Bound和Retiring的占比最高。Frontend Bound和Bad Speculation的占比相对较低。若针对llama.cpp的性能分析,需进一步分解Backend Bound,但本文重点在于介绍Top-Down方法本身,因此对占比较低的项目也将纳入后续分析。
Frontend Bound
CPU流水线的前端包含分支预测、取指单元、译码等模块。其中,分支预测对整个流水线的效率至关重要,因此Top-Down分析专门在Bad Speculation模块中进行深入分析。
在当前 Llama.cpp 的测试场景下,Frontend Core Bound 并非主要瓶颈点。
计算公式:
(STALL_SLOT_FRONTEND / (10 * CPU_CYCLES) - STALL_FRONTEND_FLUSH / CPU_CYCLES) * 100
- 公式中的STALL_SLOT_FRONTEND表示每个周期中由于前端原因未能发送到后端的操作数。
- (10 * CPU_CYCLES) 中的10与CPU架构相关,代表每个周期可以发送的指令数量。由于我们测试的是X4 CPU,因此该值为10,具体数值可通过查阅CPU手册获取。将STALL_SLOT_FRONTEND除以该数值,即可计算出前端停滞的百分比。
- STALL_FRONTEND_FLUSH / CPU_CYCLES 表示需要从每个周期中减去因指令预取未命中、异常或微架构刷新等原因导致的flush指令数量。这部分会被统计在后续的“Bad Speculation”中,因此在计算时应予以扣除。
从公式中可以看出,Topdown分析方法与硬件CPU的特性密切相关,不同CPU架构下的性能瓶颈和优化策略可能存在显著差异。ARM的Topdown工具为每个CPU类型单独定义了JSON配置文件,运行时需要根据具体的CPU型号指定相应的配置文件。这种设计反映了Topdown分析方法对硬件微架构的依赖性,因为不同CPU的PMU(Performance Monitoring Unit)事件、指令集架构(ISA)以及内部流水线结构都会影响性能指标的计算方式。
下面对Frontend Bound进行详细分析
taskset 80 ./topdown-tool --perf-path ~/bin/perf --cpu cortex-x4 -m Topdown_Frontend /data/local/tmp/perry/llama_cpp/bin/llama-cli -t 1 -no-cnv -m ${model} -n 10 -c 512 -p "the answer to the ultimate question of life, the universe, and everything is "
...
[Topdown Frontend]
Frontend Core Bound...... 66.38% cycles
Frontend Core Flush Bound 14.57% cycles
Frontend Core Flow Bound. 24.78% cycles
Frontend Memory Bound.... 33.62% cycles
Frontend Mem Cache Bound. 58.42% cycles
Frontend Cache L1I Bound. 50.81% cycles
Frontend Cache L2I Bound. 49.19% cycles
Frontend Mem TLB Bound... 0.86% cycles
测试命令的主体保持不变,仅修改了“-m Topdown_Frontend”参数,用于指定分析Topdown_Frontend指标。 可以看到,Frontend被细分为Core Bound和Memory Bound两部分
Frontend Core Bound
Frontend Core Bound...... 66.38% cycles
Frontend Core Flush Bound 14.57% cycles
Frontend Core Flow Bound. 24.78% cycles
Frontend Bound中主要瓶颈是Frontend Core Bound 为66.38%。
计算公式:
STALL_FRONTEND_CPUBOUND / STALL_FRONTEND * 100
- STALL_FRONTEND_CPUBOUND:当前端因CPU资源约束而无法将任何微操作发送到重命名阶段时,计数的周期
- STALL_FRONTEND:当前端因获取内存延迟或分支预测流程停滞而无法将任何微操作发送到重命名阶段时,计数的周期
Frontend Core bound下面又细分了Frontend Core Flush Bound和Frontend Core Flow Bound。 Flush Bound是因为pipeline被刷新(主要是因为指令预取失败)而引起的bound,Flow Bound是因为前端资源限制而引起的bound。细心的读者可能会发现,这两项之和并不等于100%,这是因为CPU前端是一个非常复杂的系统,还存在许多其他影响性能的因素。这里的Flush Bound和Core Flow Bound只是其中两个主要因素。
Core Flush Bound
"Flush"意味着整个流程中的内容都会被重新加载,正在执行的指令将失效,之前所花费的时间也将被浪费。上面提到Frontend Bound在统计时,是剔除了Flush bound的。ARM TopDown工具可能出于与其他前端边界因素对比的考量,将相关细节分析归入前端范畴。
计算公式:
STALL_FRONTEND_FLUSH / STALL_FRONTEND_CPUBOUND * 100
- STALL_FRONTEND_FLUSH: 前端停滞周期,当前端因机器刷新或恢复(resteer)而无法将任何微操作发送到重命名阶段时,计数的周期。导致刷新的示例场景包括分支预测错误、取指令异常和微架构刷新等
- STALL_FRONTEND_CPUBOUND:当前端因CPU资源约束而无法将任何微操作发送到重命名阶段时,计数的周期
### Frontend Core Flow Bound
计算公式:
STALL_FRONTEND_FLOW / STALL_FRONTEND_CPUBOUND * 100
- STALL_FRONTEND_FLOW:当前端因分支预测单元资源限制而无法将任何微操作发送到重命名阶段时,计数的周期。
- STALL_FRONTEND_CPUBOUND:当前端因CPU资源限制而无法将任何微操作发送到重命名阶段时,计数的周期。
Frontend Memory Bound
Frontend Memory Bound.... 33.62% cycles
Frontend Mem Cache Bound. 58.42% cycles
Frontend Cache L1I Bound. 50.81% cycles
Frontend Cache L2I Bound. 49.19% cycles
Frontend Mem TLB Bound... 0.86% cycles
Memory bound是Front bound的次要瓶颈。前端的Memory都是和指令相关的,包括:L1 Icache, L2 Icache,TLB。
- L1 Icache:(一级指令缓存)是CPU中用于存储最近访问过的指令数据的高速缓存。它通常位于CPU核心内部,具有非常快的访问速度。
- L2 Icache:二级指令缓存)是位于CPU核心和主内存之间的一个缓存层,用于进一步减少CPU访问主内存的频率。L2 Icache通常比L1 Icache大,但访问速度较慢。
- TLB:(地址转换旁路缓冲器)是用于加速虚拟地址到物理地址转换的硬件组件。当CPU需要访问内存时,它首先检查TLB是否已经缓存了所需的地址映射。如果TLB中存在该映射,则可以直接使用;否则,需要通过页表进行查找,并将结果缓存到TLB中。
计算公式:
STALL_FRONTEND_MEMBOUND / STALL_FRONTEND * 100
- STALL_FRONTEND_MEMBOUND:该指标表示由于前端内存访问组件导致的指令获取延迟而使前端停滞的总周期
- STALL_FRONTEND:当前端因获取内存延迟或分支预测流程停滞而无法将任何微操作发送到重命名阶段时,计数的周期
Frontend Memory Cache Bound
Memory Cache Bound包括Cache L1I Bound和Cache L2I Bound。
计算公式:
(STALL_FRONTEND_L1I + STALL_FRONTEND_MEM) / STALL_FRONTEND_MEMBOUND * 100
- STALL_FRONTEND_L1I:该指标表示一级指令缓存中未命中的指令获取请求而导致的前端停滞周期数。
- STALL_FRONTEND_MEM:该指标表示最后一级指令缓存(这里指L2 Icache)中未命中的指令获取请求而导致的前端停滞周期数。
- STALL_FRONTEND_MEMBOUND:该指标表示由于前端内存访问组件导致的指令获取延迟而使前端停滞的总周期
Frontend Memory Cache L1I Bound
计算公式:
STALL_FRONTEND_L1I / (STALL_FRONTEND_L1I + STALL_FRONTEND_MEM) * 100
- STALL_FRONTEND_L1I:该指标表示一级指令缓存中未命中的指令获取请求而导致的前端停滞周期数。
- STALL_FRONTEND_MEM:该指标表示最后一级指令缓存中未命中的指令获取请求而导致的前端停滞周期数。
Frontend Memory Cache L2I Bound
计算公式:
STALL_FRONTEND_MEM / (STALL_FRONTEND_L1I + STALL_FRONTEND_MEM) * 100
- STALL_FRONTEND_L1I:该指标表示一级指令缓存中未命中的指令获取请求而导致的前端停滞周期数。
- STALL_FRONTEND_MEM:该指标表示最后一级指令缓存中未命中的指令获取请求而导致的前端停滞周期数。
Frontend Memory TLB Bound
计算公式:
STALL_FRONTEND_TLB / STALL_FRONTEND_MEMBOUND * 100
- STALL_FRONTEND_TLB:该指标表示当TLB发生未命中时而使前端停滞的总周期
- STALL_FRONTEND_MEMBOUND:该指标表示由于前端内存访问组件导致的指令获取延迟而使前端停滞的总周期
前端总结
了解了每一个指标的含义和计算公式,我们可以知道重新来看llama.cpp 8B模型场景的前端分析数据:
[Topdown Frontend]
Frontend Core Bound...... 66.38% cycles
Frontend Core Flush Bound 14.57% cycles
Frontend Core Flow Bound. 24.78% cycles
Frontend Memory Bound.... 33.62% cycles
Frontend Mem Cache Bound. 58.42% cycles
Frontend Cache L1I Bound. 50.81% cycles
Frontend Cache L2I Bound. 49.19% cycles
Frontend Mem TLB Bound... 0.86% cycles
- 前端的主要瓶颈是Core Bound,但Flush和Flow Bound的比例较低,说明分支预测资源不足或错误预测并非主要瓶颈。
- 前端的Memory Bound中,Cache Bound占比为58.42%,其中L1I和L2I各占一半,表明代码地址跨度较大,各级I Cache未能有效覆盖。
- 前端的TLB Bound比例较低,说明TLB资源并非瓶颈。
Backend Bound
下面我们开始后端的分解:
taskset 80 ./topdown-tool --perf-path ~/bin/perf --cpu cortex-x4 -m Topdown_Backend /data/local/tmp/perry/llama_cpp/bin/llama-cli -t 1 -no-cnv -m ${model} -n 10 -c 512 -p "the answer to the ultimate question of life, the universe, and everything is "
...
[Topdown Backend]
Backend Core Bound........ 78.94% cycles
Backend Core Rename Bound. 55.78% cycles
Backend Busy Bound........ 41.52% cycles
Backend Memory Bound...... 21.02% cycles
Backend Memory Cache Bound 82.65% cycles
Backend Cache L1D Bound... 16.08% cycles
Backend Cache L2D Bound... 83.91% cycles
Backend Memory TLB Bound.. 18.63% cycles
Backend Memory Store Bound 12.87% cycles
与前端类似,仅修改了“-m Topdown_Backend”参数,用于指定分析Topdown_Backend指标。 Backtend也被细分为Core Bound和Memory Bound两部分
Backend Core Bound
Backend Core Bound........ 78.94% cycles
Backend Core Rename Bound. 55.78% cycles
Backend Busy Bound........ 41.52% cycles
后端的主要瓶颈是Core Bound,占比78.94%。
计算公式:
STALL_BACKEND_CPUBOUND / STALL_BACKEND * 100
- STALL_BACKEND_CPUBOUND:表示由于后端除内存资源外的任何资源限制而导致的停滞周期数。
- STALL_BACKEND:表示由于后端无法接受任何操作而导致的停滞周期数。
Backend Core Rename Bound
传统的处理器架构中,每个逻辑寄存器只能被一个指令使用,这限制了处理器的并行执行能力。例如,在x86架构中,只有8个通用寄存器,而在RISC架构中,虽然寄存器数量更多,但仍然存在寄存器资源不足的问题。为了解决这一问题,寄存器重命名技术应运而生。
寄存器重命名通过引入大量的物理寄存器,并在指令解码阶段将逻辑寄存器映射到这些物理寄存器,使得多个指令可以同时使用不同的寄存器,从而避免了因寄存器冲突而导致的流水线阻塞。
此项指标用于衡量因无可用重命名寄存器而导致的停滞周期数。
计算公式:
STALL_BACKEND_RENAME / STALL_BACKEND_CPUBOUND * 100
- STALL_BACKEND_RENAME:表示由于没有可用的重命名寄存器,操作无法被发送到后端而导致的停滞周期数
- STALL_BACKEND_CPUBOUND:表示由于后端除内存资源外的任何资源限制而导致的停滞周期数。
Backend Busy Bound
指令队列(Instruction Queue)是现代处理器设计中的一个重要组成部分,它在指令流水线中扮演着关键角色。指令队列的主要功能是存储从指令缓存中取出的指令,以便在后续阶段进行解码、分配和执行。通过这种方式,指令队列能够确保指令在流水线中高效流动,从而提高处理器的整体性能
此项指标用于衡量由于指令队列已满而导致的停滞周期数。
计算公式:
STALL_BACKEND_BUSY / STALL_BACKEND * 100
- STALL_BACKEND_BUSY:表示由于指令队列已满,无法执行操作而导致的停滞周期数。
- STALL_BACKEND:表示由于后端无法接受任何操作而导致的停滞周期数。
Backend Memory Bound
Backend Memory Bound...... 21.02% cycles
Backend Memory Cache Bound 82.65% cycles
Backend Cache L1D Bound... 16.08% cycles
Backend Cache L2D Bound... 83.91% cycles
Backend Memory TLB Bound.. 18.63% cycles
Backend Memory Store Bound 12.87% cycles
与前端类似,后端的Memory bound包括:L1 Dcache, L2 Dcache,TLB。另外还包括Store指令。
计算公式:
STALL_BACKEND_MEMBOUND / STALL_BACKEND * 100
- STALL_BACKEND_MEMBOUND:当后端无法接受任何微操作时,由于内存资源限制导致的停滞周期数。
- STALL_BACKEND:表示由于后端无法接受任何操作而导致的停滞周期数。
Backend Memory Cache Bound
计算公式:
(STALL_BACKEND_L1D + STALL_BACKEND_MEM) / STALL_BACKEND_MEMBOUND * 100
- STALL_BACKEND_L1D:当一级数据缓存中存在未完成的请求时,后端停滞的周期数。该事件统计的是由于一级数据缓存未命中导致的后端停滞周期数。
- STALL_BACKEND_MEM:当最后一级核心缓存(本CPU中为二级缓存)中存在未完成的请求时,后端停滞的周期数。由于本CPU的最后一级缓存是二级缓存,因此该事件与STALL_BACKEND_L2D的计数相同
- STALL_BACKEND_MEMBOUND:当后端无法接受任何微操作时,由于内存资源限制导致的停滞周期数。
Backend Cache L1D Bound
计算公式:
STALL_BACKEND_L1D / (STALL_BACKEND_L1D + STALL_BACKEND_MEM) * 100
- STALL_BACKEND_L1D:当一级数据缓存中存在未完成的请求时,后端停滞的周期数。该事件统计的是由于一级数据缓存未命中导致的后端停滞周期数。
- STALL_BACKEND_MEM:当最后一级核心缓存(本CPU中为二级缓存)中存在未完成的请求时,后端停滞的周期数。由于本CPU的最后一级缓存是二级缓存,因此该事件与STALL_BACKEND_L2D的计数相同
Backend Cache L2D Bound
计算公式:
STALL_BACKEND_MEM / (STALL_BACKEND_L1D + STALL_BACKEND_MEM) * 100
- STALL_BACKEND_L1D:当一级数据缓存中存在未完成的请求时,后端停滞的周期数。该事件统计的是由于一级数据缓存未命中导致的后端停滞周期数。
- STALL_BACKEND_MEM:当最后一级核心缓存(本CPU中为二级缓存)中存在未完成的请求时,后端停滞的周期数。由于本CPU的最后一级缓存是二级缓存,因此该事件与STALL_BACKEND_L2D的计数相同
Backend Memory TLB Bound
STALL_BACKEND_TLB / STALL_BACKEND_MEMBOUND * 100
- STALL_BACKEND_TLB:后端因处理任何需求TLB未命中而停滞的周期数。
- STALL_BACKEND_MEMBOUND:当后端无法接受任何微操作时,由于内存资源限制导致的停滞周期数。
Backend Memory Store Bound
STALL_BACKEND_ST / STALL_BACKEND_MEMBOUND * 100
- STALL_BACKEND_ST:指后端停滞时,尚未达到预提交阶段的存储操作所占用的周期数。
- STALL_BACKEND_MEMBOUND:当后端无法接受任何微操作时,由于内存资源限制导致的停滞周期数。
后端总结
同样,我们可以知道重新来看llama.cpp 8B模型场景的后端分析数据:
Backend Core Bound........ 78.94% cycles
Backend Core Rename Bound. 55.78% cycles
Backend Busy Bound........ 41.52% cycles
Backend Memory Bound...... 21.02% cycles
Backend Memory Cache Bound 82.65% cycles
Backend Cache L1D Bound... 16.08% cycles
Backend Cache L2D Bound... 83.91% cycles
Backend Memory TLB Bound.. 18.63% cycles
Backend Memory Store Bound 12.87% cycles
- 主要瓶颈在Backend Core Bound,占比78.94%。
- Rename寄存器资源耗尽占比55.78%,可以尝试编译阶段优化代码生成,减少高开销操作(如频繁的寄存器分配),并优先使用局部变量或栈空间替代寄存器
- 其次是Backend Busy Bound,占比41.52%。说明测试需要进行大量运算将指令队列耗尽,这个涉及LLama模型的运算逻辑,也侧面验证了当前测试场景(8B模型)是算力bound
- 其次是Backend Memory Cache Bound,占比21.02%。不是主要瓶颈
Retiring
运行结果:
taskset 80 ./topdown-tool --perf-path ~/bin/perf --cpu cortex-x4 -n Retiring /data/local/tmp/perry/llama_cpp/bin/llama-cli -t 1 -no-cnv -m ${model} -n 10 -c 512 -p "the answer to the ultimate question of life, the universe, and everything is "
...
[Topdown Level 1]
Retiring.......................................... 32.41% slots
Stage 2 (uarch metrics)
=======================
[Speculative Operation Mix]
Load Operations Percentage...................... 17.79% operations
Store Operations Percentage..................... 8.34% operations
Integer Operations Percentage................... 43.31% operations
Advanced SIMD Operations Percentage............. 14.92% operations
Floating Point Operations Percentage............ 0.08% operations
Barrier Operations Percentage................... 0.07% operations
Branch Operations Percentage.................... 15.35% operations
Crypto Operations Percentage.................... 0.00% operations
SVE Operations (Load/Store Inclusive) Percentage 0.00% operations
计算公式:
(1 - STALL_SLOT / (CPU_CYCLES * 10)) * (OP_RETIRED / OP_SPEC) * 100
- STALL_SLOT:表示在每个周期中,由于前端或后端未发送操作到重命名单元(rename unit),导致某些槽位(slot)未被使用的情况。具体来说,当没有操作从前端发送到重命名单元,或者重命名单元无法将操作发送到后端时,就会出现STALL_SLOT。STALL_SLOT是STALL_SLOT_FRONTEND和STALL_SLOT_BACKEND的总和
- STALL_SLOT / (CPU_CYCLES * 10): 表示后端没有发送操作的周期数占总周期数的比例。
- OP_RETIRED:表示在架构上成功执行的微操作(micro-operations)的数量。它统计的是在单个周期内,从提交队列中退休的微操作数量
- OP_SPEC:表示在推测性执行(speculative execution)中执行的微操作数量。它统计的是在一个周期内被分发(dispatched)的微操作总数
- (OP_RETIRED / OP_SPEC): 表示已完成的操作数占总指定操作数的比例。
在公式中,首先计算 (1 - STALL_SLOT / (CPU_CYCLES * 5)),表示发送操作的周期数占总周期数的比例;然后计算 (OP_RETIRED / OP_SPEC),表示已完成的操作数占总指定操作数的比例;最后将这两个值相乘,再乘以 100,即可得到从提交队列中退休的微操作的百分比
Load Operations Percentage
LD_SPEC / INST_SPEC * 100
- LD_SPEC:被推测执行的load操作计数,包括单指令多数据(SIMD)。
- INST_SPEC:已完成的推测执行操作计数。
Store Operations Percentage
ST_SPEC / INST_SPEC * 100
- ST_SPEC:被推测执行的store操作计数,包括单指令多数据(SIMD)。
- INST_SPEC:已完成的推测执行操作计数。
Integer Operations Percentage
DP_SPEC / INST_SPEC * 100
- DP_SPEC:操作指令中,整数数据处理会执行推测性执行的逻辑或算术指令,例如 MOV/MVN 操作。
- INST_SPEC:已完成的推测执行操作计数。
Advanced SIMD Operations Percentage
ASE_SPEC / INST_SPEC * 100
- ASE_SPEC:高级SIMD指令集操作计数,不包括加载、存储和移动微操作的SIMD指令。
- INST_SPEC:已完成的推测执行操作计数。
Floating Point Operations Percentage
VFP_SPEC / INST_SPEC * 100
- VFP_SPEC:浮点运算操作计数,不包括对浮点寄存器读入写出的操作。
- INST_SPEC:已完成的推测执行操作计数。
Barrier Operations Percentage
(ISB_SPEC + DSB_SPEC + DMB_SPEC) / INST_SPEC * 100
- ISB_SPEC: 屏障操作ISB操作的执行次数,ISB(Instruction Synchronization Barrier)指令的主要作用是刷新处理器的流水线,确保在ISB指令执行完成后,所有后续指令都从缓存或内存中重新获取,并且能够看到ISB指令之前执行的上下文更改操作的效果
- DSB_SPEC: 屏障操作DSB操作的执行次数,DSB(Data Synchronization Barrier)仅确保在 DMB 指令之前的所有内存访问操作完成,之后的指令可以立即执行,不影响其他指令的执行顺序。
- DMB_SPEC: 屏障操作DMB操作的执行次数,DMB(Data Memory Barrier)不仅确保内存访问完成,还要求所有缓存、TLB 和分支预测器维护操作完成,并且后续指令必须等待 DSB 完成后才能执行
- INST_SPEC: 已完成的推测执行操作计数。
Branch Operations Percentage
(BR_IMMED_SPEC + BR_INDIRECT_SPEC) / INST_SPEC * 100
- BR_IMMED_SPEC:立即分支推测执行计数,包括直接分支操作。
- BR_INDIRECT_SPEC: 间接分支推测执行计数,包括间接分支操作及过程返回(如RET),这些操作会强制软件改变程序计数器(PC),但不包括引发异常的操作或直接分支指令。该事件涵盖的指令示例包括BR Xn、RET等。
- INST_SPEC:已完成的推测执行操作计数。
Crypto Operations Percentage
CRYPTO_SPEC / INST_SPEC * 100
- CRYPTO_SPEC:推测执行的加密操作,统计了除 PMULL 和 VMULL 操作外的所有推测执行的加密操作。
- INST_SPEC:已完成的推测执行操作计数。
SVE Operations (Load/Store Inclusive) Percentage
SVE Operations (Load/Store Inclusive) Percentage 0.00% operations
ARM的SVE(Scalable Vector Extension,可扩展向量扩展)指令集是ARM架构中用于高性能计算(HPC)和机器学习领域的重要技术,旨在提升处理器在并行计算任务中的效率。作为ARMv8-A架构的可选扩展,SVE不仅支持固定长度的向量操作,还引入了可变长度向量(VLA)的概念,使得软件能够根据硬件能力动态调整向量长度,从而实现更高效的计算。
当前版本的llama.cpp未使用SVE指令集(测试平台也不支持SVE指令),因此SVE操作的百分比为0.00%。关于llama.cpp对SVE指令的优化,后续将有专门文章进行介绍
计算公式:
SVE_INST_SPEC / INST_SPEC * 100
- SVE_INST_SPEC:所有推测执行的SVE操作,包括SVE中的加载和存储操作。
- INST_SPEC:已完成的推测执行操作计数。
在微架构探查部分,也有关于SVE的细化分析:
SVE Empty Predicate Percentage
SVE指令支持使用谓词寄存器进行条件执行,这使得SVE指令能够根据谓词寄存器的状态来决定是否执行某些操作。在SVE指令中,谓词寄存器可以用于条件执行,例如在加载或存储操作中,根据谓词寄存器的状态来决定是否加载或存储数据。当谓词寄存器没有活跃条件时,则当前指令操作也没有进行实际操作。
计算公式:
SVE_PRED_EMPTY_SPEC / SVE_PRED_SPEC * 100
- SVE_PRED_EMPTY_SPEC:以无活跃谓词为条件,推测执行的SVE操作计数。
- SVE_PRED_SPEC:以谓词为条件,推测执行的SVE操作计数。
SVE Full Predicate Percentage
谓词寄存器全部为活跃条件时,对应的操作数全部进行操作。
计算公式:
SVE_PRED_FULL_SPEC / SVE_PRED_SPEC * 100
- SVE_PRED_FULL_SPEC:以全部活跃谓词为条件,推测执行的SVE操作计数。
- SVE_PRED_SPEC:以谓词为条件,推测执行的SVE操作计数。
SVE Partial Predicate Percentage
计算公式:
SVE_PRED_PARTIAL_SPEC / SVE_PRED_SPEC * 100
- SVE_PRED_PARTIAL_SPEC:以部分活跃谓词为条件,推测执行的SVE操作计数。
- SVE_PRED_SPEC:以谓词为条件,推测执行的SVE操作计数。
SVE Predicate Percentage
计算公式:
SVE_PRED_SPEC / INST_SPEC * 100
- SVE_PRED_SPEC:以谓词为条件,推测执行的SVE操作计数。
- INST_SPEC: 已完成的推测执行操作计数。
Bad Speculation
运行结果:
taskset 80 ./topdown-tool --perf-path ~/bin/perf --cpu cortex-x4 -n bad_speculation /data/local/tmp/perry/llama_cpp/bin/llama-cli -t 1 -no-cnv -m ${model} -n 10 -c 512 -p "the answer to the ultimate question of life, the universe, and everything is "
...
[Topdown Level 1]
Bad Speculation............. 1.36% slots
Stage 2 (uarch metrics)
=======================
[Branch Effectiveness]
Branch MPKI............... 0.094 misses per 1,000 instructions
Branch Direct Ratio....... 0.777 per branch
Branch Indirect Ratio..... 0.223 per branch
Branch Return Ratio....... 0.204 per branch
Branch Misprediction Ratio 0.001 per branch
Branch MPKI
BR_MIS_PRED_RETIRED / INST_RETIRED * 1000
- BR_MIS_PRED_RETIRED:被错误预测的分支指令计数,
- INST_RETIRED: 已成功完成执行的指令数量
Branch Direct Ratio
BR_IMMED_RETIRED / BR_RETIRED
- BR_IMMED_RETIRED:CPU微架构上执行的立即分支指令,包括所有立即跳转指令,如B、CBNZ等。
- BR_RETIRED:CPU微架构上执行的分支指令,无论是否被实际执行,均计入统计。此外,明确写入PC的指令也计入统计。但需要注意的是,异常生成指令、异常返回指令和上下文同步指令不计入统计。
Branch Indirect Ratio
BR_IND_RETIRED / BR_RETIRED
- BR_IND_RETIRED:CPU微架构上执行的间接分支计数,包括架构上执行的间接分支和过程返回。
- BR_RETIRED:CPU微架构上执行的分支指令,无论是否被实际执行,均计入统计。此外,明确写入PC的指令也计入统计。但需要注意的是,异常生成指令、异常返回指令和上下文同步指令不计入统计。
Branch Return Ratio
BR_RETURN_RETIRED / BR_RETIRED
- BR_RETURN_RETIRED:CPU微架构上执行的分支指令,统计返回操作的次数。
- BR_RETIRED:CPU微架构上执行的分支指令,无论是否被实际执行,均计入统计。此外,明确写入PC的指令也计入统计。但需要注意的是,异常生成指令、异常返回指令和上下文同步指令不计入统计。
Branch Misprediction Ratio
BR_MIS_PRED_RETIRED / BR_RETIRED
- BR_MIS_PRED_RETIRED:执行但被错误预测的分支总数。
- BR_RETIRED:CPU微架构上执行的分支指令,无论是否被实际执行,均计入统计。此外,明确写入PC的指令也计入统计。但需要注意的是,异常生成指令、异常返回指令和上下文同步指令不计入统计。
总结
本文以llama.cpp在ARM X4 CPU上部署llama3 8B模型来说明TopDown工具的使用, 因为我们限定了一个X4 CPU 运行,所以程序变现为算力bound。主要瓶颈在Backend Core Bound,占比78.94%。
Backend Core Bound中:
- Rename寄存器资源耗尽占比55.78%,可以尝试编译阶段优化代码生成,减少高开销操作(如频繁的寄存器分配),并优先使用局部变量或栈空间替代寄存器。
- 其次是Backend Busy Bound,占比41.52%。说明测试需要进行大量运算将指令队列耗尽。
参考
- A Top-Down method for performance analysis and counters architecture
- Arm Topdown 性能分析工具
- Part2 Use Perf with PMU feature Enabled on Armv8 A CPUs
本文章同步在微信公众号和知乎发布