跳到主要内容

深入 BPU:预测流水、覆盖与恢复

本章不再停留在“s1 快、s2/s3 更准”。目标是读懂昆明湖 V2 Predictor 的控制骨架,并能独立设计一条具有晚级覆盖能力的预测流水。

对应源码:src/main/scala/xiangshan/frontend/BPU.scala:235-1225,源码锚点 133d7dc4b。

学完本章应该能做到​

  1. 写出 s0~s3 的 valid、ready、fire 与 flush 方程;
  2. 解释为什么 FTQ 先收到 s1 结果,s2/s3 只在结论变化时发送覆盖;
  3. 区分 BPU 内部 override 与后端 redirect;
  4. 说明 PC、全局历史指针、折叠历史为何必须由同一个优先级网络选择;
  5. 根据波形判断气泡来自 FTQ full、预测组件不 ready、内部覆盖还是后端恢复;
  6. 为这条流水写出足以抓住状态错位的断言。

1. 先区分四个容易混淆的概念​

prediction​

某一级给出的控制流结论,包括 hit、taken、CFI 位置、target 和 fall-through。不同级可以对同一取指块给出不同结论。

override / BPU redirect​

s2 或 s3 发现自己的结论与前一级已经采用的结论不同,需要覆盖早期预测。它仍属于预测器内部纠错,还不知道程序执行的真实结果。

backend redirect​

分支执行、异常或内存顺序违规已经证明当前推测路径错误。它来自 FTQ/后端,语义优先级高于任何预测级结果。

update​

对应控制流已经到达训练时机,FTQ 把真实结果和预测时保存的 meta 送回各预测组件。update 修改预测器状态,但一般不等价于立即改变当前取指 PC。

这四者分不开,就无法解释为什么源码同时存在 s2_redirect、s3_redirect、ftq_to_bpu.redirect 和 ftq_to_bpu.update。


2. 组件组合:不是五个独立答案简单投票​

Parameters.scala 给出的默认组件顺序是:

val preds = Seq(uftb, tage, ftb, ittage, ras)

uftb <- resp_in
tage <- uftb.out
ftb <- tage.out
ittage <- ftb.out
ras <- ittage.out

组件内部查表可以跨相同流水阶段并行发生,但响应在 Composer 中逐层补充或覆盖。这个顺序表达了组合关系:

  • uFTB 提供很早的取指结构候选;
  • TAGE-SC 修正条件分支方向;
  • FTB 给出完整取指块结构与目标;
  • ITTAGE 修正间接跳转目标;
  • RAS 对 return 提供专门目标。

Composer 还做两件对正确性很重要的事:

  1. 把所有组件的 last_stage_meta 拼成一个整体,写进 FTQ;
  2. update 时按逆序把 meta 切回各组件。

因此 meta 是“预测现场”,不是可有可无的调试数据。没有它,训练时无法可靠知道当时哪个 provider 命中、计数器旧值是什么、是否采用 alternate。


3. 流水携带的不只有 PC​

Predictor 声明三段有效状态 s1_valid/s2_valid/s3_valid,但每级同步推进的状态至少包括:

状态用途
pc当前取指块起点
folded_gh各预测表索引/tag 所需折叠历史
ghist_ptr环形全局历史的逻辑起点
last_br_num_oh前一个块向历史加入了多少分支
ahead_fh_oldest_bits增量更新折叠历史时要移出的旧位

源码在 BPU.scala:289-351 为它们建立平行的 s0/s1/s2/s3 寄存器链。工程上必须把它们看成一个 transaction:如果 PC 前进而历史指针停住,查到的表项依然可能“像一个合理预测”,但属于错误上下文,是最难调试的静默错误。

为什么存在大量 _dup​

s0_pc_dup、s1_valid_dup 等不是多个逻辑事务,而是对高扇出控制和数据的物理复制。不同消费者使用副本以改善布线和时序。源码用 full_pred_diff 检查多个预测副本是否出现逻辑差异:只要某副本在 hit 时与相邻副本的 s1/s2/s3 full_pred 不同,就报错。

这体现一个重要原则:物理复制可以改变扇出树,不能改变体系结构行为。


4. ready、valid、fire:逐条推导​

4.1 s1 的 ready​

源码逻辑等价于:

s1_ready = s1_fire OR NOT s1_valid
s0_fire = predictors.s1_ready AND s1_ready

含义:s1 空时可以接收;s1 本拍成功向后推进时,也可同拍接收下一项,实现满流水吞吐。除此之外必须保持原内容。

注意 s0_fire 还受预测组件自身 s1_ready 限制。即使流水寄存器空,如果某预测 SRAM 或更新冲突导致组件不 ready,也不能启动新请求。

4.2 s2 的推进​

源码 BPU.scala:398-405:

s2_ready = s2_fire OR NOT s2_valid
s1_fire = s1_valid
AND predictors.s2_ready
AND s2_ready
AND ftq_resp.ready

最后一项非常关键:FTQ 不接收 s1 预测时,s1 不能前进。这会把 FTQ 满的背压送回 BPU,保持预测结果、PC 和历史不丢失。

4.3 s3 的推进​

s3_ready = s3_fire OR NOT s3_valid
s2_fire = s2_valid AND predictors.s3_ready AND s3_ready
s3_fire = s3_valid

s3 是末级,已有有效内容就会消费。若 s3 结论与 s2 一致,它不需要再向 FTQ 建立新表项;若不同,则作为对既有 FTQ 项的覆盖通知。

4.4 为什么 ready = fire || !valid​

这是单项弹性流水寄存器的标准方程:空寄存器可接收;满寄存器只有在旧内容本拍离开时才可同时接收。若写成简单 ready = !valid,每传一项就会插入一个空拍,吞吐减半。


5. valid 状态机与优先级​

只看组合 fire 方程不够,还必须看 valid 的更新优先级。

s1 valid​

BPU.scala:416-421 的优先级是:

  1. 外部 backend redirect:清零;
  2. s0_fire:装入新事务;
  3. s1_flush:清零;
  4. s1_fire:旧事务离开,清零;
  5. 否则保持。

为什么 s0_fire 高于内部 s1_flush?因为 s2/s3 override 会在同拍选择新的 next PC;s1 中旧路径事务需要清掉,但 s0 产生的新事务已经是纠正后的路径,应该被接收。外部 backend redirect 更早处理,因为它会整体重建恢复状态。

s2 valid​

优先级:s2_flush → s1_fire → s2_fire → 保持。装入时写 !s1_flush,避免把已判定作废的 s1 事务推进。

s3 valid​

优先级:s3_flush → s2_fire → s3_fire → 保持。装入时写 !s2_flush。

必须掌握的同拍案例​

事件正确结果
s1 满、s1 fire、s0 也有新请求s1 无气泡换入新请求
s2 产生 override,同时 s0 可启动s1 旧路径清除,s0 接纠正 PC
backend redirect 与 s2 override 同拍backend redirect 胜出,旧流水全部作废
FTQ fulls1 不向后推进,PC/历史 transaction 保持
s3 给出与 s2 相同结果消费 s3,不发送无意义覆盖

6. 为什么 FTQ 会先看到 s1,随后收到修正​

输出 valid 方程位于 BPU.scala:452-455:

ftq_resp.valid =
s1_valid && s2_components_ready && s2_ready
OR s2_fire && s2_redirect
OR s3_fire && s3_redirect

它表达的协议不是“等最终结果再入 FTQ”,而是:

  1. s1 结果足够早,先让前端沿候选路径继续;
  2. s2 与 s1 不同才发 correction;
  3. s3 与 s2 不同才再次 correction。

这样可以用较快但可能不完整的结果隐藏慢表延迟。代价是每次 override 都会浪费已经沿旧路径启动的工作。

示例时序:s2 覆盖 s1​

周期A 取指块状态next PCFTQ 动作
C0A 在 s0 发起未知无
C1A 在 s1,预测不跳到 BB为 A 建表项并开始 B
C2A 在 s2,发现应跳到 TT修正 A 的既有表项,清除 B 路径
C3T 进入 s1后续候选新路径继续

这里的 A 只对应一个 FTQ 表项。s2/s3 携带 ftq_idx,不是重新分配第二个表项。源码 BPU.scala:885-903 为 s2/s3 绑定之前记录的 FTQ 指针。


7. s2 如何判断需要覆盖 s1​

PreviousPredInfo 保存 s1 的关键结论:hit、target、最后分支位置、taken、taken mask 和 CFI index。preds_needs_redirect_vec_dup 比较四类变化:

  1. target diff:最终选中的目标不同;
  2. last branch position diff:块内经过的分支数量不同;
  3. taken diff:方向不同;
  4. taken offset diff:都 taken,但最早 taken CFI 的位置不同。

其中 target 比较先并行计算所有候选,再用 taken mask 选择。源码注释说明,target 往往比 taken 更早生成;这样组织能缩短 taken 到 redirect 的关键路径。

s2_redirect = s2_fire AND any(s2_result differs from saved_s1_result)

为什么 branch 数量变化也要 redirect​

即使最终 next PC 数值偶然相同,经过了多少条件分支会改变全局历史。若不纠正,下一次 TAGE/ITTAGE 索引使用错误历史,错误可能在数百拍后才表现出来。


8. s3 如何判断需要覆盖 s2​

s3 比较保存的 s2 full_pred,主要条件位于 BPU.scala:827-853:

  • 真实分支 taken mask 变化;
  • 目标变化;
  • fall-through 编码错误;
  • FTB multi-hit。

特殊条件 s3_both_first_taken 避免某些 taken-mask 差异造成无意义覆盖:如果两级都在第一个分支处 taken,后面槽位差异不影响实际走过的控制流。

这说明比较“预测结构是否逐位相等”并不总是正确。应比较架构上实际消费的最早控制流结果,忽略不会到达的后续槽位。


9. next-PC 与历史必须共享同一覆盖决策​

可以把 BPU 的推测状态想成一起走的两本账:next PC 回答“下一块去哪里”,分支历史回答“沿哪条分支路径走到那里”。TAGE、SC、ITTAGE 等会用历史查表;所以地址选对了、历史却属于另一条路径,下一块的预测仍可能错。

用一次 S2 改判看清问题​

假设条件分支 A 后面有顺序块 B,跳转目标是 T,没有 stall:

时刻已采用的预测下一 PC该分支对应的推测历史
A 在 S1A 不跳B记作不跳(0)
A 在 S2,发现 S1 错了A 跳T必须改为跳(1)

S2 的 redirect 不只是把 next PC 从 B 改成 T,还要作废沿 B 路径产生的年轻预测,并用 S2 对 A 的结论修正历史。如果只改 PC、不改历史,接下来虽从 T 取指,预测器却拿“曾经走向 B”的历史查表。这就是一种很难定位的状态错位。

反过来,即使两级算出的 next PC 数值碰巧相同,若它们认为经过的条件分支数量不同,历史也可能不同;第 7 章因此把 branch position 的差异列入 redirect 判断。

同一拍有多个候选,选谁?​

每拍可能有保持原状、S1 新预测、S2 改判、S3 改判,或外部 backend redirect。它们同时出现时,语义优先级从高到低是:

backend redirect  >  S3 override  >  S2 override  >  S1 prediction  >  stall 保持

例如 S2 要去 T,但同拍 S3 发现更老预测块应去 U,应采用 S3 的 U;如果同拍后端又送来真实执行结果,则后端 redirect 胜出。不能让 PC 采用最高优先级事件,历史却采用次一级事件。

Predictor 因此为相关状态分别建立选择器,但给它们登记对应的同一批来源:

状态直观作用
next PC下一次从哪里取指
global-history pointer、ghv 写入环形全局历史中读写到哪里、写入哪些分支结果
folded histories、ahead oldest bits从全局历史增量形成的压缩历史,供预测表索引/标记使用
branch-count one-hot这一预测块实际经过多少个条件分支槽位

实现使用 PhyPriorityMuxGenerator;其中 phyPrio 用于调整物理 Mux 的组织,不改变上述语义优先级。理解这一章不必先记住每个 history 信号的位宽,只要记住它们都在描述同一条被选中的推测路径。

可以检查的一条不变量​

令 E 表示本拍最终获胜的事件。凡本拍需要更新的 next_pc、历史指针、折叠历史、branch-count,以及相关历史位的写使能和写入值,都必须与 同一个 E 对应;不需要更新的位保持原值。检查这一条,比单独检查“PC 对不对”“历史对不对”更容易抓出 redirect 同拍的错位。


10. 后端 redirect:不是把 PC 改回去这么简单​

FTQ 送来的 redirect 包含旧 histPtr、真实 shift、taken、是否加入历史和正确 target。BPU 需要:

  1. 从该指针恢复错误分支之前的历史;
  2. 按真实结果推进 0~numBr 位;
  3. 重新计算所有 folded histories;
  4. 重建 ahead oldest bits 与 branch-count 状态;
  5. 选择正确 target 作为新 s0 PC;
  6. flush s1/s2/s3 的错误路径事务。

源码把正确后的指针写成:

updated_ptr = old_ptr - shift

具体加减方向由环形指针编码约定决定,不能凭普通数组直觉判断。必须结合 CGHPtr 和 getHist 的逻辑顺序验证。

commit-time history checker​

当 EnableCommitGHistDiff 开启时,源码维护一份只按已提交真实分支推进的 commitGHist,在 update 时比较预测保存历史与提交真历史的折叠值。这是非常有价值的在线一致性检查:它能抓到很久以前某次 stall/redirect 导致的历史漂移。


11. 三类气泡如何归因​

Predictor 不只执行功能,还给 top-down 分析记录气泡原因。

override bubble​

s2/s3 覆盖早期预测,已经启动的年轻路径作废。它衡量多级预测“先快后准”的代价。

FTQ update/component bubble​

预测组件因为 update、存储冲突或内部资源不能接收新请求,表现为 components_ready=0。

FTQ full stall​

bpu_to_ftq.resp.ready=0,BPU 无处放置早期预测。根因可能在更下游:IFU、IBuffer 或后端长期不消费导致 FTQ 占满。

架构优化时必须分开统计。扩大预测表也许降低 mispredict,却可能加剧 component stall 或降低频率。


12. 波形阅读:一次完整检查应看什么​

第一组:事务身份​

s0_pc, s1_pc, s2_pc, s3_pc
s1_valid, s2_valid, s3_valid
s0_fire, s1_fire, s2_fire, s3_fire
s2_ftq_idx, s3_ftq_idx

先确认 PC 与 FTQ identity 在各级对应,再看预测值。身份错位时,数值比较没有意义。

第二组:背压与清除​

s1/s2/s3_ready
s1/s2/s3_components_ready
s1/s2/s3_flush
bpu_to_ftq.resp.ready

如果 valid 停住,沿 ready 链向下寻找第一个为 0 的条件。

第三组:预测覆盖​

resp.s1/s2/s3.hit
resp.s1/s2/s3.taken
resp.s1/s2/s3.cfiIndex
resp.s1/s2/s3.target
s2_redirect, s3_redirect

判断是哪一类差异触发覆盖:target、方向、CFI offset、branch count、fall-through 还是 multi-hit。

第四组:历史​

s0/s1/s2/s3_ghist_ptr
selected folded history
redirect.cfiUpdate.histPtr/shift/taken

在 redirect 后检查第一条新路径请求使用的历史,而不只是 target PC。


13. 必须写进验证环境的断言​

下面是语义级断言,具体 SVA 或 Chisel 写法由实现决定。

A. stall 保持 transaction​

if s1_valid && !s1_fire && !s1_flush:
next(s1_valid) == 1
next(s1_pc, s1_hist_ptr, s1_folded_hist) == current(...)

B. flush 后不泄漏​

任何被 s1/s2/s3 flush 的事务,后续不得以有效 response 或 update 身份写 FTQ。

C. override identity​

s2/s3 correction 的 ftq_idx 必须等于该预测在 s1 首次分配的 FTQ 项。

D. transaction 原子性​

PC、hist pointer、folded history、branch count 的 source event 必须一致。

E. 历史参考模型​

每次正常预测或 redirect 后,展开环形历史应等于软件移位模型。

F. 副本一致​

所有 numDup 预测副本在有效且 hit 时必须逐位一致。

G. 无重复分配​

s2/s3 override 只能修改既有 FTQ 项,不能增加 FTQ occupancy。


14. 性能与时序权衡​

更早给结果​

优点:更快启动 ICache/IFU。缺点:准确率不足时产生 override bubble,浪费能量和带宽。

等更晚级结果​

优点:减少走错候选路径。缺点:即使最终预测正确,也人为增加每个取指块的启动延迟。

增加一级预测​

不应只问“准确率提高多少”,还要问:

  • 新一级是否延长时钟周期?
  • 覆盖发生率与平均覆盖损失是多少?
  • FTQ 是否能承受更多 correction 端口和状态?
  • 历史、RAS 和 meta 是否能同步恢复?
  • 动态功耗是否因为错误路径查表增加?

一个有用的粗略判断是:

净收益 ≈ 避免的后端误预测次数 × 后端恢复代价
- 新增 override 次数 × 前端覆盖代价
- 频率/功耗/面积损失

15. 本章实验​

实验 1:画出无停顿流水​

连续输入 A、B、C、D 四个取指块,所有级预测一致。写出每周期 s0~s3 内容、四个 fire 和 FTQ 收到的 response。目标是证明稳态能达到每拍一个新块。

实验 2:s2 override​

A 的 s1 预测到 B,s2 改为 T。画出 B 被取消、T 进入 s0、A 的 FTQ 表项被修正的周期。特别标注历史指针从哪个版本切换。

实验 3:FTQ full​

让 resp.ready 连续拉低三拍。检查 s1 PC 和历史保持、s2/s3 是否排空,以及恢复 ready 后是否只发送一次原事务。

实验 4:backend redirect 与 s3 override 同拍​

验证 backend target/history 胜出;s3 correction 不得污染 FTQ 或新路径历史。

实验 5:统计真实 workload​

收集 s2/s3 redirect 原因分布、FTQ full 周期和 component-not-ready 周期。回答:当前主要问题是早期预测不准、晚级目标不准,还是下游背压?


16. 设计评审题与参考答案​

题 1:为什么 branch 数变化也要覆盖?​

参考答案

经过的条件分支数量决定向全局历史加入多少位。next PC 偶然相同不代表预测上下文相同;若不纠正 history,后续 TAGE/ITTAGE 索引会从此漂移。

题 2:为什么 update 不能只用 PC 重新查表?​

参考答案

预测到训练之间可能有其他更新和替换,当前 provider 不一定是当时 provider。训练必须使用预测时保存的 meta,否则会把结果写到错误表项,形成难以收敛的自污染。

题 3:FTQ full 时仍让 PC 前进会怎样?​

参考答案

预测事务失去可追踪身份:后续 IFU、redirect 和 update 无法关联原预测;同时历史可能按未记录分支推进。即便最终取到了正确指令,也无法保证恢复和训练正确。

题 4:为什么不等 s3 再更新推测历史?​

参考答案

这样下一取指块必须等待到 s3 才能使用新历史,破坏每拍预测吞吐。设计选择在早级推测推进,再用 s2/s3 override 修正,实质是在延迟与恢复复杂度之间交换。

题 5:如何判断增加 s4 是否值得?​

参考答案

需要比较它减少的后端 mispredict 损失与新增的前端 override、频率、功耗、meta 和恢复复杂度。还要按 workload 分解,因为长历史结构对控制相关强的程序收益大,对低分支密度程序可能只增加成本。


17. 本章完成标准​

不要用“读完了”作为完成标准。你应提交以下产物:

  1. 一张自己画的 s0~s3 时序与优先级图;
  2. 五个实验中至少三个的真实波形;
  3. 一个 Python 或 Scala 历史 reference model;
  4. 上述 A~G 断言的伪代码或实现;
  5. 一份真实 workload 的 override/FTQ stall 统计与优化建议。

能独立解释这些产物,才算真正掌握这条预测流水,而不是只知道模块名字。