ASPLOS'26 最佳论文 | 让 0.5B 小模型读汇编,给 CPU 预取器当军师
ASPLOS’26 最佳论文 | 让 0.5B 小模型读汇编,给 CPU 预取器当军师
1. 前言:这篇论文把两个八竿子打不着的东西接上了
先说这篇论文让我眼前一亮的地方:它让一个 0.5B 的语言模型去读汇编代码,然后指挥 CPU 里的硬件预取器该怎么工作。
我第一反应是”这不可能”,因为两边的时间尺度差得太远了。但看完发现思路很漂亮。这篇 ASPLOS’26 最佳论文来自港科大 + Duke,今天想把它的完整逻辑讲清楚。
不过要讲明白这个工作,得先讲清楚CPU 预取器到底是干什么的、为什么它是个难题。这块属于 CPU 微架构,和大部分做 AI 的同学日常接触的东西距离比较远,所以下面两节我会从零开始铺,已经熟悉的可以直接跳到第 4 节。
2. 背景:CPU 预取器要解决什么问题
2.1 一切的起点:内存墙
先看一个可能反直觉的事实:现代 CPU 里,一次 DRAM(主存)访问要花几百个周期。
对比一下同一颗 CPU 里的其他操作有多快(这篇论文模拟的配置,后面会详细列):
| 操作 | 延迟 |
|---|---|
| 一次加法/乘法 | 1 个周期左右 |
| L1 缓存命中 | 2 个周期 |
| L2 缓存命中 | 9 个周期 |
| L3 缓存命中 | 20 个周期 |
| DRAM 访问 | 几百个周期 |
差了两个数量级。CPU 算得再快,数据没到就只能干等——这个”处理器速度和内存延迟之间的鸿沟”就是计算机体系结构里著名的 memory wall(内存墙)。
为什么这件事在今天还重要?论文开头的论证是:虽然现在到处是多核、GPU、各种加速器,但Amdahl 定律说明单线程速度对降低应用延迟依然关键——程序里那些无法并行的部分,快慢完全取决于单核性能。而单核性能的传统来源(指令级并行 ILP)已经挖得差不多了,内存墙成了最主要的障碍。
所以怎么办?既然消除不了这几百个周期的延迟,那就把它藏起来。
2.2 预取:在 CPU 要用之前先把数据搬过来
Data prefetching(数据预取) 的想法很朴素:在 CPU 真正需要某个数据之前,投机性地(speculatively)提前把它从慢的地方搬到快的地方。等 CPU 真的要用了,数据已经在 L1 缓存里躺着了,2 个周期就拿到,那几百个周期的延迟就被”藏”在了 CPU 干别的事的时间里。

对着这张图走一遍:
- 左边没有预取:第 0 周期 CPU 执行一条 load 指令要读地址 A → L1D/L2/L3 层层未命中 → 请求发到 DRAM → 接下来将近 300 个周期,CPU 就卡在这条指令上干等(后面依赖这个数据的指令都动不了)→ 数据到了才能继续。
- 右边有预取:第 0 周期,预取器从之前的访存规律里预测到”接下来会用 A” → 第 1 周期就发起搬运,而 CPU 此时在正常执行别的指令,一点没停 → 300 个周期后 A 已经在 L1D 里了 → 第 320 周期 CPU 才真正执行那条 load,直接 L1D 命中,只花 2 个周期。
延迟并没有消失(DRAM 该花的 300 周期照花),但它和 CPU 的正常执行重叠了,所以对程序来说是免费的。
注意这里的关键前提:预取器必须提前猜对要读哪个地址。 这就是整个领域的难点所在。而且猜错的代价不是”白忙一场”这么轻:
- 浪费内存带宽——搬了没人要的数据
- 缓存污染(cache pollution)——把有用的数据从缓存里挤出去了,导致本来会命中的访问变成了未命中
所以预取器猜错是会让程序变慢的,不是不赚而已。后面会看到,有些编排方法在某些负载上收益几乎归零,就是这个原因。
再明确一点:预取器纯粹是硬件里的一个预测器,它看不到源代码。它能观察到的只有运行时经过它的地址流——”刚才读了 0x1000、0x1040、0x1080……”,然后从这串数字里找规律。这个信息局限是后面所有故事的根源。
2.3 各种”专才”预取器:按访问模式分工
预取器怎么从地址流里找规律?答案是针对不同的内存访问模式设计专门的算法。学术界几十年里做出了一大堆,各管一类模式:
- stream(流式):顺序往后读,比如遍历一个字符串或数组。规律最简单:读了 A,那就预取 A+1、A+2……
- stride(定步长):固定间隔跳跃。比如遍历一个结构体数组时只取其中一个字段,每次地址前进的距离等于结构体大小。
- spatial(空间局部性):访问了某个结构体的一个字段,紧接着会访问同一个结构体的其他字段——地址在一小块区域内,但不是连续也不是等距的。这类预取器学的是”在一个内存区域里,哪些偏移量常常一起被访问”这种模板。
- irregular(不规则):链表、图遍历这种,地址之间没有算术关系,只能靠记住历史上的访问序列。
具体的预取器名字后面会反复出现,先有个印象就行:Pythia、Bingo、SMS、DSPatch、Sandbox、AMPM、MLOP、Power7、PPF、Stride、Stream、Next Line。这些就是这篇论文 ensemble 里的候选零件,各自擅长的模式论文列了一张表,后面会看到。
2.4 从”专才”到”组合”:ensemble 和它的编排难题
Prefetcher ensemble(预取器组合) 的动机来自一个现实问题:真实程序不是单一模式的。
论文的说法是:真实应用很少是”铁板一块”的,而是由多个动态切换的执行阶段组成,每个阶段有各自主导的内存行为。一个程序可能这一段在顺序扫数组(适合 stream),下一段在遍历链表(stream 完全没用),再下一段在做矩阵分块访问(适合 spatial)。所以任何单个专才预取器天生就覆盖不了整个负载。
于是近年的设计思路是把多个专才组合成一个 ensemble。但这么一来,核心难题就从”怎么设计单个预取器”转移到了”怎么在运行时动态调度这一组预取器”。
这个调度层叫 orchestration layer(编排层):

图里的数据通路是这样:Core/LSU 发出访存请求 → 先查 L1D,未命中就往 L2/L3/DRAM 走(上面那条链)。同时这个请求分出一路进 Router,Router 决定把它送给哪些子预取器;各子预取器产出的预取请求汇到 Mux,Mux 决定放哪个真正发出去。(c) Orchestrator 就是同时控制 Router 和 Mux 的那个东西(图里两个红箭头),它要做两个决策:
-
(a) Demand Request Training——一个真实的访存请求(demand request)来了,该送给哪些子预取器?
这里要解释一下为什么”送给谁”是个决策。预取器是靠观察访存历史来”训练”自己内部状态的——比如 stride 预取器需要看到连续几次等距访问才能确认步长。所以喂给它什么请求,直接决定了它内部状态的质量。喂了不相关的请求,等于往它的训练数据里灌噪声。
-
(b) Prefetch Request Issue——多个子预取器同时发出了预取请求,该选哪个(或哪些)发到下一级内存?带宽有限,不能都发。
论文提到已有工作有的只做前者、有的只做后者、有的两者都做。
2.5 编排层的两个死结
有了 ensemble,整个系统的性能就取决于这个选择策略——必须把动态变化的程序阶段和最优的子预取器正确配对。配错了,就是前面说的挤掉缓存数据、浪费带宽,性能反而下降。
现有方法靠在线学习(online learning)在运行时推导策略:试着用某个预取器,观察效果,再调整。这条路有两个绕不过的死结:
第一,试错收敛期躲不掉。 在线学习本质是 trial-and-error,得试错一段时间才能学到有效策略。而程序的阶段切换可能比这个收敛期还快——等它学明白该用哪个预取器,程序已经进入下一个阶段了。永远在追,永远追不上。
第二,硬件预算把它锁死在简单启发式上。 这里是我觉得最能体现”硬件约束有多残酷”的地方:现代 CPU 主频高达 5GHz,一个周期 0.2ns,一次预取决策的时间预算常常在亚纳秒级别。再加上核内的面积和功耗限制,在线机制只能用非常便宜的逻辑——几个计数器、一张小表,就这么多。它根本不可能去分析更长的程序上下文、做更有信息量的判断。
论文还发现了一个更尴尬的现象:现有编排算法对付不了复杂的现代预取器。像 Pythia 这类 sophisticated 的预取器,内部状态是通过跟踪整个内存区域的模式建立起来的,而 Alecto、DOL 这些编排算法是 PC-centric 的——按指令(PC)粒度做选择和过滤。用指令粒度的逻辑去管区域粒度的状态,会破坏它的内部状态、干扰它的模式检测,导致显著的性能下降。后面实验会看到这个现象有多严重(严重到 ensemble 打不过单个预取器)。
2.6 那全搬到离线呢?两条老路各有硬伤
既然在线的算力预算这么紧,为什么不把分析搬到离线?离线有大得多的计算预算,可以做深得多的分析。这个思路早就有人走了,两条老路:
编译器方案:靠人工设计的启发式,在编译期决定往哪里插预取指令。比 baseline 有提升,但问题是依赖人类专家去设计规则;而且这些规则通常是被特定代码模式触发的,没法在程序级别做推理,对付不了复杂的代码结构。
PGO(Profile-Guided Optimization,性能剖析引导优化):把程序编译并实际跑若干次,收集”如果这样预取会怎样”的 what-if 信息,直到找到有效策略。PGO 在很多程序上效果不错,但它必须有程序的输入数据才能收集到正确的 profile。而输入数据不总是有——程序的输入未知、或者不同运行之间变化很大的时候,PGO 就用不了。
而且这两条路还有一个共同的硬伤:都要重新编译程序(去注入预取指令或 hint)。如果应用是闭源的、手上只有一个二进制文件,这两种离线分析全都失效。
3. 需要先记住的几个术语
上面铺完了背景,这里把后面高频出现的几个缩写集中列一下,免得后文空降:
Prefetch degree(预取激进度):预取器一次往前预取多少个 cache line。值越大越激进——猜对了收益大,猜错了浪费也大。不同预取器支持的取值范围差别很大(有的 1-8,有的 0-64,有的干脆不支持调)。
Demand request(需求请求):CPU 真实发出的访存请求,区别于预取器投机发出的 prefetch request。前面说过,它同时承担”喂给预取器做训练”的作用。
AMAT(Average Memory Access Time,平均内存访问时间):以周期为单位。这篇论文用它衡量”某个预取策略对某条 load 指令好不好”,AMAT 越低越好。这是论文构造训练标签的依据。
IPC(Instructions Per Cycle):每周期执行的指令数,衡量 CPU 整体性能的最终指标。IPC 提升多少,就是这篇论文最终的成绩单。
PC(Program Counter,程序计数器):这里用来唯一标识一条指令。后面反复出现的”per-PC 的 hint”就是”每条指令配一份 hint”。
L1D / L2 / L3:三级数据缓存,越靠前越小越快。这篇论文优化的是 L1D 的预取。
4. PF-LLM 的破局思路:把决策搬到离线
回过头看,第 2 节的困境可以概括成一个矛盾:
- 在线:能看到运行时的真实访存行为,但算力预算极小(亚纳秒、几个计数器),看不到代码语义
- 离线:算力预算充足,但已有方法要么依赖人工规则、要么依赖输入数据、还都要重新编译
PF-LLM 的答案是:把”什么时候预取、怎么预取、多激进地预取”(when, how, and how aggressively to prefetch)这三个难决策全部从运行时硬件搬到离线的 LLM 分析里,运行时的硬件只负责按 PC 查表。
这样时间尺度的矛盾就消解了。回到开头我的第一反应——LLM 推理要几毫秒,预取决策只有亚纳秒,差六七个数量级——但如果 LLM 的活是在程序运行之前就干完的,运行时只剩一次查表(单周期),预算就完全够了。
论文把这个结果描述为:让片上预取器变成一个”零延迟、oracle 级”的系统,对每一条 load 指令都遵循最优的预取策略。”零延迟”指的就是没有在线试错的收敛期。
而且相比前面两条老路,用 LLM 读汇编还顺手解决了另外两个硬伤:不需要输入数据(纯静态分析),不需要源码和重新编译(闭源二进制反汇编就能用)。
5. 为什么是 LLM
上一节说的是”搬到离线”,但离线也可以用别的方法(前面那两条老路就是)。为什么偏偏是 LLM? 论文专门用一节论证这个,我觉得这几条论证是这篇工作真正的思想内核,值得完整过一遍。
核心洞察:静态代码上下文里已经包含了足够的信息来预测内存访问模式。
这个洞察怎么来的?作者的观察是:有经验的开发者看一眼代码就知道该怎么预取。看这个银行账户管理的 C 代码例子:

图分两半:左边是 C 代码,右边是对应的内存布局。四种模式在同一个 for 循环里同时出现,而且都肉眼可辨:
- (a) 无需预取:
mutex_lock(&accounts[i].lock)访问的是锁字段,预取没意义 - (b) 定步长:
total += accounts[i].amount遍历Account结构体数组。看右上角的内存布局,acc[0]、acc[1]依次排开,每次跳一个结构体大小——图里标的是 stride 9(is_valid1 字节 +owner4 字节 +lock2 字节,加上对齐) - (c) 空间局部性:
accounts[i].owner->age、->country_code、->name通过同一个Person*指针连续访问该结构体的多个字段。右图里*person那个框内的三个绿色箭头就是它 - (d) 流式:
printf("%s", accounts[i].owner->name)顺序读一个字符串。右图最下面name[0][1][2][3]...那条直箭头
这个例子说明两件事:即使很简单的程序也需要混合多种预取策略;而合适的策略常常能直接从静态代码结构推断出来。
于是核心问题变成:既然有经验的开发者能从代码里识别这些模式,现代 LLM 能不能自动做同样的事?
论文给了四条论证:
深度代码理解。硬件预取器只能从地址流里找模式,因此局限于固定步长这类时序规律性。编译器启发式通常也只做局部循环分析或模式匹配。而 LLM 能从代码上下文推断底层语义——它能区分”遍历简单数组”、”遍历结构体数组”和”遍历链表”,这三者意味着完全不同的最优预取策略,但在地址流层面可能长得很像。另外现代 LLM 的大上下文窗口支持更高层次的程序结构分析,这在编译器或硬件里都做不到。
自动发现启发式。人工设计规则费力、需要领域专家、结果往往脆弱且难泛化。学出来的启发式还能适应新的编程习惯、新硬件特性、新负载特征——重新 fine-tune 就行。而人工规则在新一代硬件出来之后可能就过时了。
从集体经验中泛化。这条是对 PGO 的直接改进。PGO 的知识只针对被 profile 的那个应用,而且对 profiling 时用的具体输入敏感。PF-LLM 在一个多样化的语料上训练,学的是静态代码模式和动态内存行为之间的关系,而不是单个程序的行为。所以它能为没见过的新程序推断有效策略,不需要该应用的 profile,摆脱了对输入数据的依赖。
关键的一步:PF-LLM 直接分析汇编代码。这让它同时独立于源码和程序输入,能用在预编译的闭源二进制上——PGO 和很多编译器方案在这里都失效。
复用预训练基础模型。这个方法可行的根基是:现在能 fine-tune 已经在数十亿行代码上训练过的编码模型,天然带着对编程语言和程序结构的理解。只需要专门化,不需要从零训练。
我把新旧两条路的对比画了一张图:

6. PF-LLM 的完整设计
整个框架分两部分:离线的 PF-LLM 模型 + 运行时的 LMHint Prefetcher 硬件。

这张图分三块,对着看一遍:
- 左边 (a) 训练:一段汇编上下文(红色那行是目标 load 指令)走两条路——上路经 prompt template 变成训练输入,下路进模拟器跑出 ground-truth 策略(
PF Sel / PF Degree / Filter)当训练标签,两者一起 fine-tune PF-LLM - 中间 (b) 离线生成 hint:拿到一个二进制 → 反汇编 → 找出所有 load 指令 + 各自的上下文 → 每条 load 跑一次 PF-LLM 推理 → 结果汇总成 PHT(按 PC 索引)
- 右边 (c) 运行时消费:Core/LSU 发出访存 → L1D → 未命中往 L2/L3/DRAM。PHT 通过”TLB-like loading mechanism”把 hint 送进来,分三路作用在 ensemble 上:(1) Demand Request Filtering 作用在 Router(控制哪些子预取器能收到这条 demand request)、(2) Prefetch Degree 作用在各子预取器、(3) Prefetcher Selection 作用在 Mux(决定谁的预取请求真正发出去)。注意候选里还有一项 “No Prefetch”——模型可以显式判断这条 load 不该预取
先说清楚适用范围:这篇工作聚焦于现代超标量处理器核内的 L1D 缓存数据预取,实验限定在单核、x86-64 ISA。作者的说法是先在微架构层面证明有效性,多核留给未来工作。
6.1 模型:为什么选 0.5B
模型选型:Qwen-2.5-Coder-0.5B-Instruct。理由是预取是个高度专门化的任务,小模型的能力已经够用,同时训练更快、显存占用更小。没有改动模型架构,就是在预取数据集上 fine-tune。
我觉得这个选择很关键。后面会看到,离线推理成本是这套方案能否落地的决定性因素——每条 load 指令都要跑一次推理,模型大一点整个方案的成本就爆了。0.5B 是个经过考量的工程决策,不是随便选的。
输入:目标 load 指令周围的汇编代码上下文,具体是前 128 行 + 后 128 行,共 257 行。
为什么用汇编而不是源码或 LLVM IR?论文在 discussion 里给了三个理由:
- 通用性:源码和 IR 不一定有(闭源二进制),但静态二进制总能反汇编
- 上下文长度可控:直接从二进制扒代码看似会破坏跨函数调用关系,但编译器通常会把调用亲和度高的函数放在相邻内存区域来改善指令缓存性能,所以不管函数边界直接扒上下文,反而能捕捉到跨函数信息。反过来,如果用源码,函数内联或循环展开会让上下文长度指数级增长,固定上下文窗口内反而抓不到有意义的语义模式
- 和运行时上下文最贴近:多一层抽象就多一层间接,LLM 更难学到”输入抽象”到”输出 hint”的映射
输出:三种 hint
- Prefetcher Selection Hint:一个整数索引,指定用 ensemble 里哪个子预取器来为这条 load 发预取请求
- Prefetch Degree Hint(可选):控制激进度的整数,值越大越激进。选中的预取器不支持 degree 控制时这个 hint 不用
- Demand Request Filtering Hint(可选):指定不该接收这条 load 产生的 demand request 的子预取器。过滤掉不相关的 demand request 能减少某些子预取器的训练噪声
第三个 hint 值得多说一句,因为它对应的正是前面提到的”PC-centric 编排会破坏高级预取器状态”那个问题。如果某个预取器根本不是为这种访问模式设计的,喂给它这条 load 的访存记录只会污染它的内部状态,那就干脆别喂。
6.2 训练数据怎么造
这是我认为整篇论文里最巧的一环。ground truth 从哪来? 答案是用架构模拟器暴力枚举。
流程是这样:对每个 benchmark,为”子预取器 × 预取激进度”的每一种组合跑一次独立的 ChampSim 模拟。作者改了模拟器,让它记录每条 load 指令(按 PC 唯一标识)的 AMAT。
举个具体例子:ensemble 里有 stream 和 stride 两个预取器,各支持 degree 1 和 2,那每个 benchmark 就要跑 2×2 = 4 次模拟。跑完得到一张 per-PC 的表:

表的读法:PC 0x1234 这条 load,用 stride + degree 1 时 AMAT 是 25.2 周期,stride + degree 2 是 28.3,stream + degree 1 是 101.0,stream + degree 2 是 103.9。
然后用一个确定性启发式从这张表推出 ground truth:
- 最优策略 = AMAT 最小的那个组合 → 上例中是
PF Sel: stride, PF Degree: 1 - 要过滤的预取器 = 表现最差的那个策略对应的预取器 → 上例中是
Filter: stream
第二条的逻辑是:如果某个预取器在这条 load 上表现极差,说明它不是为这种访问模式设计的,它的内部状态很可能正被这条 load 的 demand request 干扰,那它就是个理想的过滤候选。
但这里有个例外,很重要:如果表现最差的预取器属于”高级组件”,就不生成 filtering hint。因为这些预取器内部状态复杂,对 demand request 过滤本身就很脆弱。具体来说 Pythia、Bingo、DSPatch、Sandbox、SMS 这五个被排除在过滤之外:

这张表带星号(*)的就是论文正文里排除在过滤之外的那几个(表里标星的是 Pythia、Bingo、DSPatch,另外 Sandbox 和 SMS 在正文 5.1 节里一并列出)。还能看到各预取器的 degree 支持情况差异很大——Pythia、Bingo、Next Line、PPF 完全不支持 degree 控制,支持的那些原生范围从 0-6、1-8 到 0-64 各不相同。
顺便说下候选池的选取原则:ensemble 里没有放 Triage、Triangel 这类 temporal prefetcher,因为它们资源开销大、而且是专为 LLC 预取设计的,和这篇工作聚焦的 L1D 预取不匹配。
怎么统一这个异构的 degree 范围? 论文的做法是归一化成三态:1(保守)、2(中等)、3(激进),分别映射到每个子预取器原生范围的 Q1、中位数、Q3。举个例子:LMHint degree 3(激进)对 DSPatch(原生范围 0-64)来说,映射到 round(0.75 × 64) = 48。
这个归一化同时简化了两件事:PHT 的实现(只需要 2 bit 存 degree)和 LLM 的学习任务(不用学 12 种不同的数值范围)。
6.3 Prompt 格式
直接把裸汇编喂给 instruction-tuned 模型是次优的,Qwen 这类模型期望特定的 system/user/assistant 格式。论文用了基于 Qwen 官方模板的 prompt:
<|im_start|>system
You are a helpful assistant that generates prefetch hints for a given load
instruction in a assembly code snippet.
The load instruction is marked with <load> and </load>.
Your output should be a JSON object with "PFSel", "PF Degree" and "Filter"
as fields.
<|im_end|>
<|im_start|>user
movzbl 0x3adef0(%rip), %eax
xorl %r8d, %r8d
leaq 0x6dc8d(%rip), %rsi
<load>movq 0x10(%rsp), %rcx</load>
movl 0x1c(%rsp), %edx
movq 0x3ab11d(%rip), %rdi
orl $2, %eax
<|im_end|>
<|im_start|>assistant {
"PF Sel": "stride",
"PF Degree": 2,
"Filter": "stream",
}<|im_end|>
对齐指令格式有两个好处:加速 fine-tune 收敛(system/user prompt 让基础模型能用零样本能力理解任务,把学习精力集中在预取的细微差别上而不是输入格式);便于结构化输出(配合 vLLM 这类框架的 structured output 功能,可以强制输出严格符合 JSON schema)。
两个实现细节:
-
<load>和</load>被加成了特殊 token(进 embedding table),让模型更平滑地学会识别 load 指令的位置 - loss 只在输出部分计算——只算 JSON 输出那几个 token 的 loss,不算输入汇编和 system prompt 的。这让训练聚焦在生成 hint 这个主任务上,不会因为”复现输入汇编”的差异被惩罚
训练配置:LLaMA-Factory + DeepSpeed,8× NVIDIA H20,BF16,学习率 1e-5,有效 batch size 64,AdamW,2 个 epoch。
6.4 硬件侧:hint 怎么送进 CPU
这是另一个关键工程问题:离线生成的 hint 怎么在运行时被处理器用上?
论文的方案是套用 TLB 的思路:
- PHT(Prefetch Hint Table):完整的 hint 表,放在主存里,按每条 load 指令的虚拟 PC 索引。程序执行前加载进去
- PHB(Prefetch Hint Buffer):片上的小缓冲,缓存最近用过的 PHT 条目,类比 TLB 缓存页表项。用 256 项,在硬件开销和单周期访问之间取平衡
hint 的编码压得很紧:每个 PC 一个 8 bit 值,其中 4 bit 预取器选择 + 2 bit 预取激进度 + 2 bit demand request 过滤策略。论文提了一句为什么是 8 bit 而不是更少——行宽小于 8 bit 的 SRAM blackbox 不常见,所以就用 8 bit。这种细节挺能体现作者是真的在做硬件设计。
运行时流程:load 指令发出 → 用 PC 查 PHB → 命中就取出 hint 策略,分三路去控制 ensemble:
- 选择 hint 决定哪个子预取器被允许为这条 load 发预取请求
- degree hint 直接传给选中的子预取器控制激进度
- 过滤 hint 阻止这条 demand request 被送到指定的子预取器,保护它的内部状态不被无关训练信息污染
PHB miss 怎么办?发请求去主存的 PHT 填充条目,期间用一个默认策略——存在 PHT 和 PHB 的零地址保留条目里。
7. 效果
7.1 模型精度:95%
实验设置里有个细节值得表扬:训练用 SPEC 2006 的 memory-intensive benchmark,测试专门留 SPEC 2017,模型在评测前从没见过测试集。
另外因为 PF-LLM 需要静态汇编,不能用公开的 ChampSim trace(那些 trace 用 Pin 生成,只含指令执行顺序、访存历史这些动态信息,没有静态汇编代码),所以作者自己从源码编译了所有 SPEC benchmark:Ubuntu 20.04 + GCC 9.4.0,-O2 -fpermissive -march=x86-64,每个 job 模拟 2 亿条指令。
模拟器是 ChampSim 的 DPC-3 版本,核的配置对标 Arm Neoverse N2:5-wide fetch/decode、10-wide issue/commit、120 项 IQ、85/90 项 LQ/SQ、288 项 ROB;L1 I/D 各 64KB 4-way(2 周期命中,从 L2 预取),L2 512KB 8-way(9 周期),L3 2MB 16-way(20 周期),DRAM 是 LPDDR5-5500 单通道。注意 L1D 缓存只有 64KB、L3 只有 2MB——这个配置比服务器级 x86 小不少,缓存越小预取的相对收益越大,这是看那个 54.5% 时要记住的前提。

蓝线是 training loss(对数轴),橙线是 evaluation accuracy。loss 在最开始就断崖式下降,之后一路缓慢下行;accuracy 从 84% 左右起步——注意这个起点已经很高了,说明预训练模型带来的先验很强,fine-tune 主要是在做专门化,和论文”只需专门化、不需从零训练”的论证对得上。然后 accuracy 阶梯式往上爬,最后在留出测试集上达到 95.0%。
论文说是因为算力和时间限制停在这里,更大的训练集精度应该还能涨。看曲线末端确实还在上扬、没有走平,这个说法可信。
更细的分析在这里:

右边 (b) 图先看,这是理解左图的钥匙:最优预取器策略的分布极度不均匀。Sandbox 那一行的柱子长度远远压过其他所有预取器,而且颜色以保守/中等 degree 为主(论文正文的说法是 “Sandbox + moderate degree 远比其他组合更常成为最优”)。Streamer 排第二,其余的量级都很小。
左边 (a) 是混淆矩阵,行是 ground truth、列是预测。对角线整体成立,但强弱差别很大:
- Sandbox 0.98、Power7 1.00,几乎全对
- MLOP 0.87、Stride 0.85、Bingo 0.84、Pythia 0.77、SMS 0.76、DSPatch 0.75、AMPM 0.75,中上水平
- Streamer 只有 0.53——将近一半判错
而且误判的方向高度集中:几乎所有非对角线的重量都堆在 Sandbox 那一列(Streamer→Sandbox 0.34、DSPatch→Sandbox 0.25、AMPM→Sandbox 0.25、SMS→Sandbox 0.19、Pythia→Sandbox 0.14……)。
论文对误判的解释是:PF-LLM 猜错的时候输出不是随机的,而是高度倾向于第二好的选项,性能接近最优;这说明模型学到的不只是”单个最优策略”,而是”什么样的预取策略对这种代码上下文有效”这个更一般的特征。
这个解释我只部分认同。 从矩阵看,误判集中流向 Sandbox 更直接的解释是它是频率上的多数类——(b) 图里 Sandbox 的样本量压倒性地大,模型在不确定的时候倒向多数类是分类任务的常见行为,不一定是”学会了识别次优解”。当然这两种解释在结果上会重合:因为 Sandbox 本身就是最常见的最优解,倒向它确实往往不会错得太远,这也解释了为什么 IPC 上没有崩。
顺带说一句,对角线平均下来明显低于 95%,两个数字并不矛盾:95.0% 是在测试集上按样本算的准确率,而样本分布本身是倾斜的——Sandbox 这个占比最大的类准确率 0.98,把整体拉了上去。混淆矩阵是按行归一化的,等权重看每个类,所以显得低。看这类结果的时候,得分清”按样本”还是”按类别”。
不过论文这个观察方向仍然值得肯定:对系统优化来说,误判的代价分布比准确率数字更重要。如果剩下 5% 的误判是灾难性的,整体收益可能被吃光;论文至少去看了误判落在哪里,这一步很多 ML for Systems 的工作是省掉的。
7.2 端到端性能
消融实验设了四个配置:
- LMHint-S:只用预取器选择(Selection)hint
- LMHint-SD:加上预取激进度(Degree)控制
- LMHint-SDF:再加上 demand request 过滤(Filtering)
- LMHint-SDFR:降成本(Reduced)配置,三种 hint 都用,但硬件只实现 Figure 5(b) 里最常被选中的四个子预取器
先看总览:

图里的数字是相对无预取基线的几何平均 IPC 提升。四个 LMHint 配置分别是 54.5%(SDFR)、54.5%(SDF)、54.2%(SD)、53.9%(S),全部高于所有基线。基线里最好的单预取器是 Pythia 44.7%,其后 Bingo 43.2%、Sandbox 41.7%、PPF/Streamer 41.5%、AMPM 41.1%、Power7 38.6%、MLOP 36.0%;ensemble 方法里 DOL 35.6%、Conf. Based 29.8%、Alecto 21.9%;最差的 SW Prefetch 只有 0.2%。
论文声明的提升是 比最强单预取器高 9.8 个百分点,比最强已有 ensemble 方法高 18.9 个百分点。这两个数字对得上 54.5 − 44.7 = 9.8 和 54.5 − 35.6 = 18.9。不过论文正文把这两个对照对象分别写成了 Sandbox 和 Alecto,和图里的数值排序不一致(Sandbox 是 41.7、Alecto 是 21.9,都不是各自类别里最强的)。我倾向于认为是正文笔误、数字本身没问题,但读的时候值得留个心。
逐 benchmark 的细节(上半是对单预取器,下半是对 ensemble 方法):

先看这张图能读出的东西:LMHint 在几乎每个 benchmark 上都是最高的那根柱子,而且收益分布极不均匀——bwaves_s 和 fotonik3d_s 能到 150% 左右,gcc_s、roms_s 在 100% 上下,而 omnetpp_s 几乎没有提升(所有预取器都不行,说明它的访存模式本身就不可预取)。几何平均 54.5% 这个数字主要由几个高收益负载拉起来的,看总均值容易误判单个应用的实际收益。
下半图里 Alecto 和 Random 那两根在 mcf_s 上甚至掉到 0 附近,而 LMHint 是 30% 出头——编排策略选错的代价是实打实的负优化,不是”没优化好”而已。
再看总结论:
第一,消融实验证明每种 hint 都有价值,但贡献不均。从 LMHint-S 到 LMHint-SDF 是递增的:加 degree 控制(SD)带来 0.3% 平均 IPC 提升,再加 demand request 过滤(SDF)又贡献 0.3%。预取器选择提供了最大的收益,另外两个是精细调优。
第二,降成本配置几乎不掉性能,LMHint-SDFR 甚至比 SDF 高 0.01%(在噪声范围内)。这个结果的意义是:硬件只需实现 4 个子预取器而不是 11 个,面积和复杂度大幅下降,性能不变。而且不需要重训模型——只要在生成 hint 时约束 vLLM 推理引擎的 JSON schema,只允许模型从这四个里选。这个 trick 很漂亮,充分利用了结构化输出的能力。
第三,也是我觉得最有意思的一点:看上面那张总览图,DOL(35.6%)和 Alecto(21.9%)这两个在线 ensemble 方法,居然比几乎所有单预取器都差——Pythia 44.7%、Bingo 43.2%、Sandbox 41.7%,连排在中游的 MLOP(36.0%)都比 DOL 高。Alecto 的 21.9% 甚至只比 SMS(12.6%)和 Random(10.9%)好。
这印证了前面提到的问题。这些空间型子预取器通过跟踪整个内存区域的模式来构建复杂内部状态,而不是单条指令流。Alecto 这类方法的 PC-based 选择逻辑,要么用无关的 demand request 污染它们的训练,要么过滤掉了关键的 demand request,两头都会破坏它们的模式检测。
PF-LLM 靠静态的、上下文感知的 hint 绕过了这个问题。论文特别强调过滤 demand request 的能力尤为关键,因为它能屏蔽这些高级预取器脆弱的状态不受干扰输入影响。
7.3 为什么能赢:oracle 级的零延迟适应
这张图我觉得是全文最有说服力的一张:

这是 654.roms 这个负载的 heartbeat IPC 轨迹。(b) 是各个 baseline 预取器的轨迹,(a) 在它上面叠加了 LMHint 的轨迹。
LMHint 的曲线始终贴着所有子预取器的性能上包络线跑,而其他预取器在震荡和滞后。
为什么?单体预取器天生受限于它承诺了单一固定策略,不可能适应程序变化的访存模式。而 LMHint 是主动的——靠 PF-LLM 离线生成的 per-PC hint,它在一段代码被执行之前就知道最有效的策略是什么。程序控制流在不同阶段之间切换时,LMHint 瞬间切换策略,没有任何运行时试错。
看图里标的三个区域:
- 区域 ② 和 ③:LMHint 立刻匹配上了该阶段最优预取器(分别是 MLOP 和 Sandbox)的性能。这就是 oracle 级选择
- 区域 ①:更有意思——LMHint 超过了任何单个子预取器。这个阶段程序的访存模式太多样,没有单个预取器能有效处理。这时候细粒度控制的优势体现出来了:LMHint 用选择、degree、过滤三种 hint 在单条 load 指令的粒度上编排子预取器,让每个子预取器只专注于它擅长的访问流,最小化来自无关 demand request 的干扰。于是 ensemble 的性能变成可叠加的,超过任何单个组件的上限
7.4 真实 web 负载
SPEC 毕竟是合成 benchmark,论文还测了四个真实的 web serving 应用:Apache HTTP Server、MySQL、RocksDB、Xapian 搜索引擎。为了更贴近现代数据中心里拥挤的微服务共同部署环境,主存延迟调到 2 倍。

LMHint-SDFR 仍然优于 Pythia 和 DOL 这两个对照,但提升幅度明显小得多:Apache 约 3.7%、RocksDB 约 1.7%、MySQL 和 Xapian 都只有 0.5% 出头。论文老实分析了两个原因:
- 这些应用更偏 I/O-bound 而非 CPU/memory-bound,预取优化的天花板本身就低
- 这些应用已经被人工优化了很多年,很多自己实现了缓存和预取机制,留给自动预取的空间不大
这段自我批评我挺欣赏。3.7% 和 SPEC 上的 54.5% 差距巨大,如果论文避而不谈只报 SPEC,读者就会对实际收益产生错误预期。
另外注意这里只对了 Pythia 和 DOL 两个基线,没有像 SPEC 那样铺开全部 19 个。样本也是四个应用,说服力不如 SPEC 那部分。
7.5 开销:这才是落地的关键
离线推理开销。每条 load 指令跑一次 LLM 推理,听起来很贵。实测:

(a) 单张 H20 上用 vLLM 服务,吞吐最高 234.3 requests/秒(换成 L20 是 173.6)。可以看到吞吐随并发度显著上升,但在并发 8 之后基本走平——GPU 已经打满了,再加并发没用。
高吞吐来自三个因素:0.5B 参数量小;输入上下文短,而且生成长度极短(就一个小 JSON);分析二进制里所有 load 指令天然提供了大量独立请求,能充分利用 vLLM 的 batching。
(b) 是输入上下文长度的分布,论文的说法是”大部分汇编上下文在 3000 token 以下”。看直方图我觉得这个描述偏乐观——分布的峰就在 2900-3000 附近,整体跨度 2000 到 4000,3000 以上还有相当一块。不过这不影响结论:这个量级的上下文对 0.5B 模型来说都很轻。
(c) 是总推理时间 vs 二进制大小。论文说是线性增长,理由是推理次数由 load 指令数决定、而 load 指令数和代码规模成正比。这个逻辑没问题,但要注意这张图两个轴都是对数轴(横轴 100KB→100MB,纵轴 10⁰→10³ 秒),红色虚线是在 log-log 下拟合的。log-log 上的直线严格说是幂律关系,斜率约等于 1 时才是线性。图上的点离散度不小(同一量级的 binary 之间差了好几倍),所以当成”大致成正比”看就好,别当精确的线性模型。
最关键的数字:整个 SPEC 2017 套件生成 hint,在 8 GPU 系统上需要 38.5 分钟。而用 16 核机器编译这套 benchmark 需要 25.4 分钟。
同一个量级。 这个对比是这套方案能否落地的核心论据——它把 LLM 推理成本放在了”和编译差不多”这个开发者早已接受的尺度上。
运行时存储开销。PHT 每条目存 48 bit 虚拟 PC(x86-64)+ 8 bit hint = 56 bit(7 字节)。分析 SPEC 2017 二进制发现平均每 MB 可执行代码有 10.62K 条 load 指令,所以 PHT 开销是每 MB 二进制 74.34 KB,即静态程序体积增加 7.26%。
论文的论证是:为了 10-20% 的 IPC 提升,这点开销是划算的——想靠传统硬件手段(加宽核、加深流水线、加大缓存)拿到同等提升,面积、功耗和设计复杂度的代价会高得多。
8. 论文自己承认的局限
这部分论文写得很坦诚,我照实转述。
JIT 和字节码程序不支持。PF-LLM 依赖分析静态二进制,所以需要 JIT 编译的程序或跑在字节码上的程序(比如 Java)目前不支持。作者给的思路是扩展到分析运行前就有的静态 IR,训一个从 IR 生成 hint 的模型。
没处理 ASLR(地址空间布局随机化)。hint 是按静态 PC 索引的,运行时随机化会打断 hint 和指令的映射。解决思路是把 hinting 机制和 OS loader 集成——给 hint 表施加和代码段相同的随机化偏移,对应关系就保住了。
ISA 和机器配置依赖。当前只在 x86-64 上评测,但方法论本身是 ISA 无关的,换 ISA 重训就行。最优预取策略也会随缓存大小、带宽这些微架构参数变化,当前模型是为特定配置训的。论文的设想是:由 CPU 厂商提供 PF-LLM 模型——他们有详细的硬件知识和精确的模拟器来生成训练数据。或者把硬件参数作为额外的 prompt 输入,让模型生成配置感知的 hint。
顺便,论文对 PGO 的对比有个论证值得记:虽然 PF-LLM 给特定二进制生成的 hint 是静态的,但整体方案对运行时环境是自适应的——因为内存访问模式主要由程序代码决定,给定 load 的模式在不同运行间基本反映代码语义;足够大的上下文窗口让 PF-LLM 能区分不同的输入/分支相关情况;剩下的数据和输入相关的变化,由被选中的那个预取器继续在运行时学习来处理。
论文把这个总结为它的一个关键贡献:在离线 hint 生成和在线预取器选择之间定义了一个清晰、可泛化的接口,让静态的、优化过的、语义感知的 hint 去引导动态的、运行时自适应的硬件。
另外实验还顺手证明了一件事:软件预取(往指令流里直接插预取指令)表现很差(图里 SW Prefetch 只有 0.2%)。原因是它既对运行时访存模式缺乏可见性,又增加了指令数量,给核的前端带来额外开销。
9. 我的几点 take
第一,这篇工作真正的贡献是那个”接口”,不是 LLM。把”用 LLM 做预取”当成卖点的话就看小了。核心贡献是在离线深度分析和在线快速执行之间定义了一个 8 bit 宽的接口。8 bit 这个数字很关键——正是因为窄,它才能塞进 PHB 做到单周期访问;也正是因为语义上够表达(选哪个预取器 + 多激进 + 过滤谁),它才承载得住离线分析的价值。接口设计是这类 offline-online 协同工作的真正难点,不是模型训练。
第二,”离线做难决策、在线只查表”这个模式可以复制。论文在 implications 里也提到了:这套方法论能推广到其他动态硬件机制——LLM 分析复杂控制流给分支预测器提 hint,或者推断数据复用特性来指导更智能的缓存替换和插入策略。我觉得这个方向真的有搞头。分支预测器面临的困境和预取器几乎一模一样:决策预算极小、只能看到运行时历史、看不到代码语义。
第三,0.5B 这个选择比想象中重要。整套方案能不能落地,取决于”生成 hint 的时间”能不能和”编译时间”处在同一量级。38.5 分钟 vs 25.4 分钟这个对比,如果模型换成 7B,就完全不是一个故事了。这提醒我在做系统优化时,模型规模不是”越大越好”的自由变量,而是被端到端成本预算约束的设计参数。
第四,那个 95% 要拆开看。混淆矩阵按类别看,对角线从 Streamer 的 0.53 到 Power7 的 1.00 跨度很大,误判几乎全部流向 Sandbox 这个多数类。倾斜的标签分布让”按样本的准确率”和”按类别的准确率”差得很远,这是我读这篇论文时最需要自己动手核对的地方。但真正决定结论的不是这个数字——是 IPC 没崩这个事实,而它成立的原因恰好是 Sandbox 本身常常就是不错的选择。做 ML for Systems,别只报准确率,要报误判落在哪里、代价多大。
第五,在线 ensemble 反而不如单预取器这个发现很有教育意义。Alecto(21.9%)和 DOL(35.6%)都是发表过的方法,各自的论文里肯定都报了正向结果。但当子预取器换成 Pythia 这类复杂的现代预取器,它们就失效甚至变成负优化(mcf_s 上 Alecto 几乎归零)。这是系统研究里典型的”baseline 组合爆炸”问题——方法 A 在环境 X 有效,方法 B 在环境 X 有效,不代表 A 在 B 的环境里还有效。这也说明为什么这类工作必须做全面的 baseline 对比。
第六,真实负载只有 3.7% 这件事要正视。SPEC 上 54.5% 很漂亮,但真实 web 服务里最好的 Apache 才 3.7%,MySQL 和 Xapian 是 0.5% 出头。论文给的两个理由(I/O-bound、已被人工深度优化)都站得住,但这也意味着这套方案的真实受益场景是特定的:计算密集、访存密集、且没被人工深度优化过的负载。科学计算、编译器、数据库的部分内核可能符合,但典型的微服务未必。
10. 一点展望
这篇论文我读完最兴奋的不是那 18.9%,而是它示范了一种新的 hardware-software co-design 分工方式。
传统的分工是”编译器做静态优化、硬件做动态适应”,两边各自为战。PF-LLM 提出的分工是”离线的 AI 做语义理解、在线的硬件做快速执行”,中间用一个窄接口连接。这个分工里,LLM 承担的是”人类专家的代码直觉”这个角色——正好是它相对硬件和编译器启发式的比较优势。
还没被回答的问题不少。hint 的粒度是不是最优的? 现在是 per-PC 的,但同一条 load 指令在不同调用上下文里最优策略可能不同,per-PC 会取一个平均。跨 ISA 和跨微架构配置的泛化到底能做到什么程度? 论文的设想是厂商各训一个,但如果每代 CPU 都要重新收集训练数据、重训模型,这个成本谁承担?还有多核场景——共享 LLC 上的预取干扰是个完全不同的问题,per-PC 的静态 hint 在多核竞争下还成立吗?
欢迎评论区交流。搞 CPU 微架构的同学如果觉得我哪里理解偏了,欢迎指出来。
顺带一提,这篇工作的思路可以概括成一句话:把一个原本靠人工设计启发式的决策问题,交给模型从数据里自动学。ground truth 靠模拟器暴力枚举生成,模型学的是”代码上下文 → 最优配置”的映射。
这个套路 AutoML 圈子的人应该会觉得很熟悉——它本质上就是用学习替代人工启发式设计,只不过搜索的对象从神经网络架构换成了预取策略。我们把这方面的积累整理成了《动手学 AutoML:从 NAS 到大语言模型优化实战》,书里搜索策略那部分(进化、贝叶斯、强化学习、DARTS)和架构评估那部分(权重共享、免训练评估、benchmark 构建)讲的就是这类”怎么高效地搜、怎么便宜地评估”的方法。
不过要说清楚:这本书讲的是神经网络和 LLM 层面的自动化,CPU 微架构、预取器、ChampSim 这些内容书里完全没有。方向上算是共享方法论,但不是同一个领域,我不想扯得太硬。感兴趣的话可以当成另一个视角看看。
