预测器训练、置信度与别名
预测器不只是“用 PC 查一张表”。一次预测从读表到训练可能相隔几十个周期,中间还会经历多级覆盖、取指、预译码、执行、redirect 和提交。理解训练的关键,是分清三件事:
- 预测时究竟看到了哪份表项;
- 后端最终证明真实结果是什么;
- 训练时应该修改哪个表项,以及以什么旧状态为起点。
源码入口:
frontend/Composer.scala:拼接、拆分各预测组件的 meta;frontend/NewFtq.scala:保存 meta,提交时生成BranchPredictionUpdate;frontend/Tage.scala、SC.scala、ITTAGE.scala:具体训练策略;frontend/WrBypass.scala:连续更新同一项时的数据前递。
一、一次预测到训练的完整时间线
预测发生时,TAGE可能选择了T2作为provider,使用了计数器值4,并记录T0/T1/T3哪些位置可以分配。等这条分支提交时,T2对应的SRAM行可能已经被别的分支更新。训练不能重新查表后假装“现在读到的值就是当时预测用的值”,而必须把预测时的选择带回来。
二、meta保存的不是答案,而是“当时如何得到答案”
BranchPredictionUpdate.meta在接口上是一个UInt(MaxMetaLength.W),并不是一个叫“Meta”的统一结构体。昆明湖V2的Composer依次连接uFTB、TAGE-SC、FTB、ITTAGE、RAS。每个组件知道自己的一段结构和宽度,把预测时才看得见的旧状态输出为last_stage_meta。后续IFU和后端会带回“指令是什么、实际跳不跳”,却不会替组件重建当时读到哪一路、哪个counter、哪个目标。
预测流水线里,各段是怎样产生的
S0以预测块PC和历史发起访问;uFTB较早给出基础预测,TAGE-SC、FTB、ITTAGE和RAS在后续级读取或覆盖结果。不同组件形成meta的拍数不必相同:各自把所需信息流水化,到最后一级都提供一段last_stage_meta。下面列的是当前源码中的字段;numBr表示每块可记录的条件分支槽数,带[]的字段按槽重复。
| 组件 | 预测时记录进自己的meta | 信息怎样产生 | 提交训练时怎样使用 |
|---|---|---|---|
uFTB FauFTBMeta | hit;非FPGA时还有pred_way | 读uFTB后记录当时是否命中、哪一路命中,再流水到末级。 | 本实现主要用这两个旧值统计预测/提交命中;uFTB写入时还会按更新PC重新检查当前tag,不是仅凭旧pred_way写表。 |
TAGE TageMeta | 每槽providers.{valid,bits}、providerResps.{ctr,u}、altUsed、basecnts、allocates;可选scMeta、pred_cycle、use_alt_on_na | tagged表命中选择provider并读出旧counter/useful;base表给出旧counter;预测时同时标记哪些较长历史表未命中且可分配。pred_cycle和use_alt_on_na在非FPGA配置下存在。 | providers指定旧provider表;providerResps.ctr是训练counter的旧状态;altUsed/basecnts影响base及alternate策略;allocates用于必要时分配新项。pred_cycle/use_alt_on_na主要用于统计,不要把它们当成独立的写表使能。 |
SC SCMeta | 嵌在TAGE的scMeta中:每槽scPreds、各SC表旧ctrs;仅启用SC时存在 | SC读统计计数器、形成对TAGE方向的修正决定,连同旧计数器装入TAGE meta。 | 对照实际方向、TAGE的旧预测与SC的旧预测,按SC条件训练计数器/阈值;SC没有另占Composer的一段。 |
FTB FTBMeta | writeWay、hit;非FPGA时还有pred_cycle | FTB查表后保留当时的命中和命中路。注意这不是FTB表项本身。 | hit/writeWay帮助FTB决定可否直接更新原命中路;若当时未命中,更新路径会重读当前tag再决定位置。 |
ITTAGE ITTageMeta | provider、altProvider、altDiffers、providerU、providerCtr、altProviderCtr、allocate、providerTarget、altProviderTarget;非FPGA时还有pred_cycle | 间接跳转预测时选中provider/alternate,保存双方旧目标、counter、useful及候选分配表。 | 真实目标到来后,比较旧目标并更新provider、alternate或分配新表项;普通条件分支不会使用这段meta训练ITTAGE。 |
RAS RASMeta | ssp、TOSW | 在预测call/ret时保存相应的栈指针状态,末级形成提交meta。 | 提交时配合确认的call/ret做RAS push/pop和状态维护;TOSR/NOS不在这段RAS提交meta中。 |
比如TAGE第0槽保存providers(0)=T2、providerResps(0).ctr=4:它说的是“当时T2命中且读到4”,而不是“提交时T2仍是4”。若后来该分支实际not-taken,训练器据此知道该更新哪个provider及怎样从旧状态出发;若期间有同地址的更近一次更新,WrBypass会提供逻辑最新counter,避免把前一次训练覆盖掉。
Composer与FTQ怎样传递它
当前参数配置中的组件顺序是uFTB → TAGE-SC → FTB → ITTAGE → RAS。Composer对每个组件执行“左移已有拼包,再接上本组件的last_stage_meta”。所以逻辑上从高位到低位是:
{ uFTB_meta | TAGE_meta(内含SC_meta) | FTB_meta | ITTAGE_meta | RAS_meta }
预测末级由Composer打包 提交训练时从低位向高位逆序拆包
Composer按组件的meta_size检查总宽度不超过MaxMetaLength。FTQ在BPU末级有效时,以该项的ftqIdx把整个last_stage_meta写入ftq_meta_mem的MetaEntry.meta;同一项还存last_stage_ftb_entry,但那是另一字段。预测项在FTQ中等待期间,IFU预译码、后端redirect和ROB提交状态会陆续到来;这些新事实修正FTQ的CFI/目标/状态,不会把原来保存的组件meta改写成“新预测”。
待该FTQ项可提交时,commPtr读出同一项的PC、meta、旧FTB表项和其他状态,拼成BranchPredictionUpdate。BPU先用spec_info.histPtr取回预测时对应的全局历史,作为训练用ghist;Composer再反向遍历组件,逐段右移拆出各自的meta,其他训练字段(pc、实际方向、目标等)则随同一更新包送给组件。这就是为什么meta与真实结果能在几十拍之后重新配对。
meta、ftb_entry、spec_info不是同一份东西meta保存各预测组件的旧读结果和选择;ftb_entry描述当时FTB记录的控制流结构,FTQ会结合预译码生成训练用新表项;spec_info另存histPtr、RAS推测恢复状态等。其中histPtr也用于训练以重取历史,不能简单说整份spec_info只供redirect恢复。pc、br_taken_mask、full_target同样是更新包字段,但不在拼接的组件meta里面。
顺着源码读最直接:Parameters.scala:getBPDComponents确定组件顺序;FauFTB.scala、Tage.scala(SC字段见SC.scala)、FTB.scala、ITTAGE.scala、newRAS.scala各自定义meta;Composer.scala打包/逆序拆包;NewFtq.scala的ftq_meta_mem写入与提交回读;BPU.scala把spec_info.histPtr转换为训练用ghist。香山官方Composer文档和FTQ文档也分别解释了这两段职责。
三、训练包里的信息究竟从哪里来
这里容易产生一个误解:BranchPredictionUpdate并非“后端一次性送来的真实结果”。它是FTQ在同一个ftqIdx下,把预测时的快照、IFU取回指令后的预译码、后端执行与提交的反馈拼成的训练包。from_stage和meta仍然描述当初怎样预测,并不是后来测得的真实值。
按发生顺序看,信息走四条路径:
BPU预测 S1/S2/S3 ── pc、原FTB表项、meta、spec_info、预测CFI/target、from_stage ─┐
IFU取回指令 ──────── 预译码:哪些槽是branch/jump、指令长度、JAL目标 ───────┤
后端执行/提交 ────── redirect:taken/target/mispredict;ROB:哪些指令已提交 ─┤
↓
FTQ按同一ftqIdx保存、修正并读取这些信息 ── FTBEntryGen ── BranchPredictionUpdate
“真实信息”并非同一拍产生:预译码只能确认取回的指令是什么;执行才知道条件分支是否跳转、目标是否预测错;提交才知道哪些指令允许训练。被冲刷的错误路径不能仅因为曾经被预测或预译码就当成已提交指令训练。
| 训练包字段 | 最初来自哪里 | FTQ如何形成送回值 |
|---|---|---|
pc | BPU给出的预测块起始地址 | 写入ftq_pc_mem,提交时按commPtr读出。它标识这一块,不是taken指令自己的PC。 |
meta | BPU各预测组件在预测时输出的last_stage_meta | 写入ftq_meta_mem,提交时原样取回,再由Composer拆给各组件;它记录旧表项和选择过程。 |
spec_info | BPU的last_stage_spec_info | 写入ftq_redirect_mem,提交时读回,包含历史指针、RAS等预测快照;不是后端重新计算的“真实历史”。 |
from_stage | BPU最终选中S1/S2/S3哪一级响应 | FTQ保存selectedRespIdxForFtq,提交时读回。 |
cfi_idx | 最初是BPU预测的taken CFI位置 | FTQ保存为cfiIndex_vec;IFU或后端发生redirect时,updateCfiInfo按反馈修正其位置/有效位,提交时取出。valid=0表示该块没有确认的taken CFI,不能只看bits。 |
full_target | 最初是BPU目标;redirect也可能提供修正目标 | FTQ维护目标路径,提交时形成commit_target。它是训练用的最终目标/下一块地址;未跳转时可能是顺序下一块地址,不应一概称为“后端报告的跳转目标”。 |
ftb_entry | BPU保存的旧FTB表项 + IFU预译码 + 最终CFI/目标 | FTBEntryGen据此构造训练用的新表项;例如预译码确认分支位置,才能决定是插入新分支还是修正旧表项。 |
new_br_insert_pos | 不是某个单元直接报告的字段 | FTBEntryGen比较最终CFI位置与旧FTB各branch slot的offset,计算新分支应插在哪个槽。 |
br_taken_mask | 最终CFI位置 + 新FTB表项 | FTBEntryGen逐个比较branch slot offset和cfi_idx,标记本块真正taken的条件分支槽;它不是后端直接传来的一整排taken位。 |
br_committed | IFU提供有效指令范围;ROB回报提交的ftqIdx + ftqOffset | FTQ维护每条指令的commitStateQueue,再按新FTB表项的branch offset投影到各槽。只看br_taken_mask=0,不能区分“未跳转”与“未提交”。 |
jmp_taken | 最终CFI位置 + 新FTB表项的尾部jump slot | FTBEntryGen检查尾部jump是否有效、offset是否对应当前CFI。 |
mispred_mask | 后端redirect报告的isMisPred和指令offset | FTQ按offset记在mispredict_vec;提交时只保留已提交指令对应的位,再映射到FTB槽。IFU发现的FTB假命中另由false_hit表达,二者不要混为一谈。 |
pred_hit / false_hit | BPU的FTB命中结果;IFU预译码对该命中的复核 | FTQ维护entry_hit_status:BPU称命中先记h_hit;若预译码发现预测槽并非相应branch/jump,或检测到预译码误预测,则改记h_false_hit。pred_hit对h_hit和h_false_hit都为1,false_hit只对后者为1。 |
送回BPU之后:哪些字段真的参与写表?
先过总门:FTQ等该项满足提交条件,令update.valid有效。没有update.valid,即使FTQ里存着预测meta,也不能把那次预测当作训练。随后BPU用spec_info.histPtr找回预测当时对应的全局历史,放进送往各预测组件的ghist;再把pc送过去。Composer把同一份训练包广播给组件,但按各组件的meta_size拆出各自的meta。于是TAGE拿到TAGE旧计数器,ITTAGE拿到ITTAGE旧目标,而不是把两者混用。
接下来,各组件并不是“见到训练包就全写一遍”:
| 组件/步骤 | 直接使用的字段 | 在训练中具体做什么 |
|---|---|---|
| FTB表项生成(FTQ内) | 旧ftb_entry、IFU预译码、cfi_idx、full_target、mispred_mask | FTBEntryGen判断发现了新分支、目标变了,还是旧项可保持;产出新的ftb_entry、new_br_insert_pos和各槽的taken/mispredict掩码。这里是整理训练事实,还不是写FTB SRAM。 |
| FTB写表 | pc、新ftb_entry、FTB自己的meta、old_entry、false_hit | pc定位行/tag;meta中的命中way帮助选择写入位置。若old_entry表明表项无需修改,可跳过写入;false_hit还会触发FTB从关闭请求状态重新打开。pred_hit=1不等于“表项有效,无须修正”,因为假命中时它也为1。 |
| TAGE方向训练 | ftb_entry.brValids、br_taken_mask、mispred_mask、pc、由spec_info.histPtr恢复的ghist、TAGE的meta | 先确定哪些branch槽需要训练;某槽的br_taken_mask=1表示实际taken,为0的有效训练槽表示not-taken。pc+ghist计算表索引/tag;meta指定预测时的provider、旧counter和分配候选。训练provider/base counter;需要且允许时,利用mispred_mask决定是否尝试分配更长历史项。连续写同一项时WrBypass前递更新后的counter。 |
| SC统计修正 | br_taken_mask、pc+ghist、TAGE/SC的meta | 用真实方向对照预测时SC与TAGE的意见、旧统计计数器和阈值;只有满足SC自己的更新条件才改统计表或阈值。并非每次TAGE写表,SC都写。 |
| ITTAGE间接目标训练 | ftb_entry的JALR类型、jmp_taken、cfi_idx、full_target、mispred_mask、pc+ghist、ITTAGE的meta | 仅符合条件的已taken间接跳转且非ret进入此路径;用full_target与预测时保存的provider/alternate目标比较,更新目标、计数器或分配项。普通条件分支不会因此训练ITTAGE。 |
br_committed、from_stage、pred_hit和new_br_insert_pos也确实装在训练包里,但不要据此推断它们都直接控制写表。在当前昆明湖V2源码中,br_committed被BPU可选的提交历史一致性检查使用;TAGE的直接更新条件看的是FTQ发出的update.valid、新FTB表项的有效branch槽等,而不是直接读取这个字段。from_stage、pred_hit、new_br_insert_pos在这条BPU训练写表路径中没有独立的写使能用途;它们仍可用于描述或调试本次预测。spec_info也不能笼统说成“只供恢复”:其中histPtr在训练时用于重取全局历史,而RAS相关状态主要用于redirect恢复。
把字段放进同一次训练里看
假设一个FTQ项的旧FTB表项记录两个条件分支槽:offset 2和offset 7。BPU预测了方向并把两个槽当时的provider/counter存进meta。实际执行后,offset 2 not-taken、offset 7 taken且预测错误;两条指令都提交了。训练包可理解为:
pc = 此预测块的起始地址 spec_info.histPtr = 预测时历史的位置
cfi_idx = valid, offset 7 br_taken_mask = [0, 1]
br_committed = [1, 1] mispred_mask = [0, 1]
meta = 两槽各自预测时的旧状态 full_target = offset 7 的最终目标
训练时,BPU先用histPtr找回历史;TAGE对两个有效槽分别以not-taken、taken训练counter,并只在满足其分配条件时对offset 7尝试新项;SC检查这两个槽上自己是否纠错或犯错。若预译码确认旧FTB表项已完整、目标也没变,FTB可能因为old_entry=1而不写表:方向预测错不意味着FTB结构一定错。from_stage说明那次最终采用哪一级预测,却不会独自改变counter。如果offset 7在提交前被冲刷,不能仅凭先前的预译码和预测meta把它当成已提交分支训练。
读波形时也要区分三个“错”:mispred_mask记录后端确认的预测错误;false_hit表示IFU预译码发现FTB命中内容不符;meta则只是预测时读到的旧表状态。把三者混成一个“训练是否正确”位,会看不清到底该修方向计数器、FTB表项还是取指结构。
源码对照(本站对应的昆明湖V2 Chisel树):FrontendBundle.scala的BranchPredictionUpdate定义字段;NewFtq.scala的fromBpu写入、fromIfu.pdWb与fromBackend.redirect/rob_commits维护事实,提交段给update.*赋值;同文件的FTBEntryGen整理训练表项。训练端的实际消费分别见BPU.scala(histPtr→ghist)、Composer.scala(拆meta)、FTB.scala、Tage.scala、SC.scala和ITTAGE.scala。香山官方的FTQ模块文档也按输入路径解释FTQ存储结构。
最后,FTQ必须保证上述读写都对应同一个ftqIdx。如果pc来自A项、meta来自B项,即使每个字段单独看都合法,也会训练错误的预测表。
四、方向TAGE一次训练到底做什么
对块内每个有效条件分支,先得到:
updateTaken = 真实方向
updateMispred = 最终预测是否错误
provider = meta中记录的provider table
providerCtr/U = 预测时看到的旧状态
altUsed = 当时是否采用alternate/base
1. 更新provider counter
存在provider时,对其饱和计数器向真实方向更新。这里使用meta中的旧counter;若近期已经有同地址更新,则由WrBypass提供更新后的逻辑最新counter,避免连续两次训练都从同一个旧值起步。
2. 更新useful位
只有provider与alternate意见不同时,结果才能说明provider是否真正有额外价值:
- provider正确、alternate错误:提高或保持provider的useful;
- provider错误、alternate正确:降低provider的useful;
- 两者答案相同:不能据此证明provider比alternate更有用。
u不是预测置信度,它主要回答“这个entry是否值得占据容量”。counter强弱才更接近方向置信度。
3. 更新base与use_alt_on_na
弱provider与base意见不同时,真实结果训练use_alt_on_na:以后遇到类似弱provider,是相信新分配的tagged entry,还是退回训练更久的base。若本次最终使用了base,base counter也必须被训练。
4. 误预测时分配更长历史entry
只有需要分配时,才考虑比当前provider使用更长历史的表。预测meta已经记录哪些表当时未命中且u=0。训练阶段在这些候选中选择一个位置,写入新tag,并把counter初始化到靠近真实方向的弱状态,而不是直接设成强状态。
若长期没有可分配位置,bank tick累积并最终触发useful老化,为新模式释放容量。这意味着“分配失败率高”可能来自容量压力或useful老化太慢,不一定是哈希设计错误。
五、置信度不是一个单独的数
| 层次 | 典型状态 | 回答的问题 |
|---|---|---|
| 方向强度 | base/TAGE饱和counter | 当前entry倾向taken还是not-taken,有多强 |
| entry价值 | useful位 | 这个tagged entry是否比alternate更有贡献 |
| 选择策略 | use_alt_on_na | 弱provider出现时是否应退回base |
| 统计修正强度 | SC求和与阈值 | 是否有足够证据覆盖TAGE |
| 目标置信 | ITTAGE counter/provider状态 | 间接目标是否应采用provider或alternate |
所以“预测正确率低就把counter加宽”通常没有根据。问题可能是tag alias、历史长度不合适、provider选择、SC阈值或更新延迟。
六、alias不止三类
1. Index alias:同一行竞争
不同PC/历史得到相同index。若tag不同,会表现为互相替换;若是无tag的base表,会直接共享counter。
(PC=A, history=HA) ─┐
├─ index 317
(PC=B, history=HB) ─┘
这类冲突增加容量或改善index hash通常有效。
2. Tag alias:假命中
index相同且有限tag也相同,但实际是不同上下文。预测器把别人的entry当成自己的provider。增加tag位能降低概率,却增加每项面积和读出能耗。
3. Folded-history alias
两条不同的长历史被折叠成相同短值,即使PC相同也会访问同一index/tag组合。增加表项不一定解决;需要分析fold宽、历史长度和hash混合。
4. Destructive与constructive alias
- destructive:两个分支偏好相反,互相把counter推向错误方向;
- constructive:两个分支偏好相同,共享状态反而加快训练。
因此只统计“发生碰撞次数”不够,还要看碰撞双方是否导致错误或替换。
5. Update interference
多条在途分支可能先后使用同一旧entry,随后连续提交更新。如果第二次训练仍从SRAM里过期的counter开始,第一次训练会被覆盖。这不是index/tag碰撞,而是时间上的读写相关。
6. Bank conflict
预测读和训练写命中同一单端口SRAM bank时,读可能被标记无效或延迟。它是结构冲突,不应误算成预测算法alias。源码单独统计tage_table_bank_conflict。
七、WrBypass究竟解决什么
假设计数器初始为3,连续两条已提交分支都taken:
没有bypass:
update A读旧值3 → 写4
update B仍使用旧值3 → 又写4 // A的效果丢失
有bypass:
update A:3 → 4,并把4记录进bypass
update B命中相同index:取bypass的4 → 5
WrBypass内部用小型CAM记录近期写入的index(有些实例还带tag/way),并保存逻辑最新数据。它不是预测主表,也不消除不同PC的算法alias;它解决的是连续update之间的RAW相关。
验证至少要覆盖:
- 同index连续taken/not-taken更新;
- 不同way部分写;
- bypass容量替换;
- 饱和counter的silent update;
- SRAM写与预测读同bank冲突。
八、用一个具体例子串起来
假设分支A预测时:
PC=0x80001000
T2命中,ctr=4,u=1
base预测not-taken
T2弱taken,use_alt_on_na选择provider
最终预测taken
FTQ保存T2编号、旧ctr=4/u=1、base counter、altUsed和可分配mask。若后端最终得到not-taken且该分支提交:
- FTQ生成
updateTaken=0、mispred=1并带回旧meta; - T2 counter向not-taken减一;
- 因provider与base意见不同且provider错误,T2 useful受到负向训练;
use_alt_on_na学习以后更倾向base;- 从比T2历史更长且当时可分配的表中选择一个新entry;
- 新entry以弱not-taken状态开始,而不是直接强not-taken。
这就是一次误预测为何可能同时更新多个结构。
九、性能分析应该收集什么
至少按预测组件和表号收集:
- provider/alternate命中与最终采用比例;
- provider弱/强状态下的正确率;
altDiffers时provider与alternate各自正确率;- allocate success/failure、useful reset次数;
- 每张表的替换率与有效entry占用;
- bank conflict、
WrBypasshit/miss; - 从预测到update的周期分布;
- 同一index上的PC冲突对及其方向相关性;
- SC覆盖TAGE后变对与变错的次数;
- ITTAGE provider/alternate/FTB目标来源及正确率。
只看总MPKI无法判断应该增加容量、tag、历史长度,还是修复训练时序。
十、波形调试顺序
遇到“训练后反而更差”时,按以下顺序检查:
- update的
pc/ftqIdx/meta/cfi_idx是否属于同一预测事务; - 真实taken、target、mispred mask是否正确;
- meta解包后的provider编号和旧counter是否等于预测时波形;
- update index/tag是否重新计算正确;
- 是否命中
WrBypass,新counter是否从逻辑最新值计算; - 是否发生SRAM bank conflict或写被mask;
- 是否错误分配到比provider更短的历史表;
- 被flush的错误路径是否意外产生了训练。
自测题
- 为什么提交时重新查询TAGE,不能替代保存预测meta?
u=0是否表示该entry预测不可信?为什么?- 两个分支index相同但tag不同,会产生什么影响?
WrBypass能否解决tag alias?- 若allocate failure很高,应先观察哪些统计量,再决定扩容?