XiangShan Prefetcher RTL 实现

核心设计思想是:

  • 上层预取器可以跨层级”指示”下层预取(L1 可以让 L2 预取,L1 可以让 L3 预取),但这种跨层指示走的是 专用旁路通道(sideband,BundleBridge),而不是 TileLink 缓存一致性总线上的 Hint 报文;
  • 每个预取请求都携带”来源标识”(pfSource / MemReqSource,使得请求在跨层传递、在下层流水线处理、以及在性能统计时都能追溯到是哪一级、哪一个预取器发起的;
  • 预取深度按层级递增:越靠近核心的层级(L1)预取得越”近”(lookahead 小),越远的层级(L2/L3)预取得越”远”(lookahead 大),以匹配各级 miss 延迟差异

各层级使用的预取器:

层级 算法 说明
L1 D-Cache StrideStreamSMS(Spatial Memory Streaming) Stride/Stream 由 L1Prefetcher 统一管理;SMS 是独立模块
L2(coupledL2) BOP(Best-Offset Prefetch,含虚拟地址 VBOP + 物理地址 PBOP)、TP(Temporal Prefetch)、PrefetchReceiver(接收 L1 提示) 默认启用 BOP + 接收器,TP 可选
L3(huancun) PrefetchReceiver(接收上层提示,默认)、BOP(代码存在但默认不启用)、TPmeta(TP 的元数据存储介质) L3 默认只做”接收 + 存 TP 元数据”,自身一般不主动生成预取

⚠️ 默认配置下, L3 不是用自己的预取器去预取,而是 接收 L1 通过旁路送来的预取地址 并执行,外加 充当 L2 时序预取(TP)的元数据存储后端


三类数据通路

跨层级预取请求一共有三类完全不同的物理通路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
┌─────────────────────────────────────────────────────────────────────────────┐
│ 通路 A:预取地址旁路 (PrefetchRecv sideband,BundleBridge 点对点) │
│ ───────────────────────────────────────────────────────────────────────── │
│ 用途:L1 预取器"指示"下层(L2/L3)去预取某个物理地址 │
│ 载体:coupledL2.PrefetchRecv / huancun.PrefetchRecv (非 TileLink) │
│ 字段:{ addr(64b), pf_source, addr_valid, l2_pf_en } │
│ 路径:L1Prefetcher/SMS.io.l2_req ──MemBlock仲裁──▶ L2.pf_recv_node │
│ L1Prefetcher.io.l3_req ──MemBlock──▶ Top跨核汇聚──▶ L3.pf_recv_node │
└─────────────────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────────────┐
│ 通路 B:TileLink 主总线 (A 通道 Acquire / Hint) │
│ ────────────────────────────────────────────────────────────────────── │
│ 用途:① 真正去下层取数据(无论是 demand 还是预取触发的 miss) │
│ ② L2 自身预取器(BOP/TP)产生的预取请求,作为 Hint task 在 L2 内消费│
│ 载体:TLBundleA(Acquire/Hint 操作码)+ user 字段携带预取语义 │
│ user 字段:ReqSourceKey / VaddrKey / PrefetchKey / AliasKey │
└──────────────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────────────┐
│ 通路 C:TPmeta 专用 TileLink 节点 (Temporal Prefetch 元数据) │
│ ────────────────────────────────────────────────────────────────────── │
│ 用途:L2 TP 把"miss 地址序列"写到 L3 的 TPmeta SRAM,并读回用于重放预取 │
│ 载体:TPmetaReq / TPmetaResp(专用 BundleBridge 节点,跨核广播响应) │
│ 路径:core_l3_tpmeta_source_port ──▶ l3.tpmeta_recv_node │
│ l3.tpmeta_send_node ──ValidIOBroadcast──▶ core_l3_tpmeta_sink_port│
└──────────────────────────────────────────────────────────────────────────┘

预取来源与目标层级的编码体系

MemReqSource —— 全局内存请求来源枚举

文件:utility/.../BusKeyField.scala:24-45

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
object MemReqSource extends Enumeration {
val NoWhere = Value // 0
val CPUInst = Value // 1
val CPULoadData = Value // 2
val CPUStoreData = Value // 3
val CPUAtomicData = Value // 4
val L1InstPrefetch = Value // 5 L1 指令预取
val L1DataPrefetch = Value // 6 L1 数据预取(在 TileLink 上标记"这是来自 L1D 的预取 miss")
val PTW = Value // 7
val Prefetch2L2BOP = Value // 8 预取到 L2,来自 BOP
val Prefetch2L2PBOP = Value // 9 预取到 L2,来自物理 BOP
val Prefetch2L2SMS = Value // 10 预取到 L2,来自 L1 SMS
val Prefetch2L2Stream = Value // 11 预取到 L2,来自 L1 Stream
val Prefetch2L2Stride = Value // 12 预取到 L2,来自 L1 Stride
val Prefetch2L2TP = Value // 13 预取到 L2,来自 TP
val Prefetch2L2Unknown= Value // 14
val Prefetch2L3Unknown= Value // 15
val ReqSourceCount = Value // 16
val reqSourceBits = log2Ceil(ReqSourceCount.id) // = log2Ceil(16) = 4 bits
}
  • 整个 SoC 用统一的 4-bit 编码标识一笔内存访问的来源,预取相关取值从 8Prefetch2L2BOP)到 15Prefetch2L3Unknown)。
  • 通过 TileLink 的 ReqSourceKey user 字段在总线上传递
1
2
3
4
5
// BusKeyField.scala:48-52
case object ReqSourceKey extends ControlKey[UInt]("reqSource")
case class ReqSourceField() extends BundleField[UInt](ReqSourceKey,
Output(UInt(MemReqSource.reqSourceBits.W)),
x => x := MemReqSource.NoWhere.id.U)

L1PrefetchSource —— L1 内部的预取来源(3 bit)

文件:L1PrefetchInterface.scala:30-47(trait HasL1PrefetchSourceParameter

1
2
3
4
5
6
7
L1_HW_PREFETCH_NULL    = 0   未被预取
L1_HW_PREFETCH_CLEAR = 1 被 demand 访问命中后清除标记
L1_HW_PREFETCH_STRIDE = 2 Stride 预取
L1_HW_PREFETCH_STREAM = 3 Stream 预取
L1_HW_PREFETCH_STORE = 4 Store 预取
L1_HW_PREFETCH_AGT = 5 SMS 的 AGT 路径
L1_HW_PREFETCH_PHT_* SMS 的 PHT 路径(cur/inc/dec)
  • 这是 L1 D-Cache 内部用来标记一个 cache line 是被哪个算法预取进来的(保存在 cache 的 meta 里),用于训练反馈与 FDP 统计。
  • 当 L1 要把预取请求送到 L2 时,会把这个内部编码 映射MemReqSource 编码

PfSource —— L2 内部的预取来源(3 bit)

文件:coupledL2/prefetch/PrefetchParameters.scala:39

1
2
3
4
5
6
7
8
9
10
11
object PfSource extends Enumeration {
val NoWhere = Value // 0
val SMS = Value // 1
val BOP = Value // 2
val PBOP = Value // 3
val Stream = Value // 4
val Stride = Value // 5
val TP = Value // 6
// PfSourceCount = 7 → pfSourceBits = 3
def fromMemReqSource(s: UInt): UInt = ... // 把 MemReqSource 映射回 PfSource
}
  • L2 内部用更紧凑的 3-bit PfSource 表示来源(区分 SMS/BOP/PBOP/Stream/Stride/TP)
  • fromMemReqSource() 负责把外部传来的 4-bit MemReqSource 翻译成 L2 内部的 3-bit PfSource

SINK_L1 / SINK_L2 / SINK_L3 —— 目标层级标记(2 bit)

文件:L1PrefetchComponent.scala

1
2
3
SINK_L1 = 0b00   预取到 L1 D-Cache
SINK_L2 = 0b01 预取到 L2
SINK_L3 = 0b10 预取到 L3
  • 这是 L1 预取器内部 用来标记”这条预取请求应该落在哪一级”的字段,保存在多级过滤表 MLPReqFilterBundle.sink
  • L1 的过滤/仲裁逻辑会根据 sink 把请求分发到三个不同的输出端口(io.l1_req / io.l2_req / io.l3_req

当预取真正变成一笔下行内存访问(通路 B)时,预取语义被塞进 TileLink A 通道的 user/echo 字段:

字段(Key) 含义 典型来源
ReqSourceKey MemReqSource 编码(4 bit),标识请求来源 L1 MissQueue 写入
VaddrKey 虚拟地址高位(用于下层预取器做虚拟地址训练) L1/L2 写入
PrefetchKey 是否需要下层回送预取 hint L1 写入
AliasKey Cache 别名位(VIPT 别名消歧) L1 写入
IsKeywordKey(echo) 关键字优先(critical-word-first) L1 写入

注意区分:ReqSourceKey 走的是 TileLink(通路 B),而预取”指示”用的 pf_source 走的是 PrefetchRecv 旁路(通路 A)。两者都用 MemReqSource 这套数值,但物理载体不同,不要混淆。


L1 数据预取器

L1 数据侧实际上例化了 两个独立的预取器实例MemBlock.scala:413-460):

1
2
3
4
5
6
7
8
9
10
// 实例 1:SMS(Spatial Memory Streaming)—— 只向 L2 预取
val prefetcherOpt: Option[BasePrefecher] = coreParams.prefetcher.map {
case _: SMSParams => val sms = Module(new SMSPrefetcher()); ...; sms
}
prefetcherOpt.foreach{ pf => pf.io.l1_req.ready := false.B } // SMS 不向 L1 发预取

// 实例 2:L1Prefetcher(内含 Stream + Stride)—— 向 L1/L2/L3 预取
val l1PrefetcherOpt: Option[BasePrefecher] = coreParams.prefetcher.map {
case _ => val l1Prefetcher = Module(new L1Prefetcher()); ...; l1Prefetcher
}

两个实例的分工

  • prefetcherOpt(SMS):擅长识别”空间访问模式”(一个 region 内不规则但稳定的访问足迹),只输出到 L2io.l1_req.ready 被钉死为 false)。
  • l1PrefetcherOpt(L1Prefetcher):内含 Stream + Stride 两个子算法,同时输出 L1 / L2 / L3 三条预取流

基类与接口:BasePrefecher / PrefetcherIO

文件:BasePrefecher.scala:58-72

1
2
3
4
5
6
7
8
9
10
class PrefetcherIO()(implicit p: Parameters) extends XSBundle {
val ld_in = Flipped(Vec(LdExuCnt, ValidIO(new LdPrefetchTrainBundle()))) // 来自 Load 单元的训练
val st_in = Flipped(Vec(StaExuCnt, ValidIO(new StPrefetchTrainBundle()))) // 来自 Store 单元的训练
val tlb_req = new TlbRequestIO(nRespDups = 2) // 预取器向 TLB 请求 V→P 翻译
val pmp_resp = Flipped(new PMPRespBundle()) // PMP 权限检查结果
val l1_req = DecoupledIO(new L1PrefetchReq()) // → L1(带反压 ready)
val l2_req = ValidIO(new L2PrefetchReq()) // → L2(单向 valid,无反压)
val l3_req = ValidIO(UInt(PAddrBits.W)) // → L3(单向 valid,仅物理地址)
val enable = Input(Bool())
}

三个输出端口的本质区别

  • l1_reqDecoupledIO(有 ready 反压)——因为它要去抢占宝贵的 L1 D-Cache load 流水线端口,必须能被反压;
  • l2_req / l3_reqValidIO(无反压,即生即发)——它们只是”提示”下层,丢了也无所谓(下层有自己的过滤队列)

训练输入 bundle PrefetchReqBundleBasePrefecher.scala:74-80):

1
2
3
4
5
6
7
class PrefetchReqBundle extends XSBundle {
val vaddr = UInt(VAddrBits.W)
val paddr = UInt(PAddrBits.W)
val pc = UInt(VAddrBits.W)
val miss = Bool() // 该次访问是否 L1 miss(触发训练的主因)
val pfHitStream = Bool() // 该次访问是否命中了 Stream 预取来的行
}

Stride 预取器

文件:L1StridePrefetcher.scala

核心数据结构 StrideMetaBundleL1StridePrefetcher.scala:58-110):

1
2
3
4
5
6
class StrideMetaBundle extends XSBundle {
val pre_vaddr = UInt(STRIDE_VADDR_BITS.W) // 上次访问的虚地址(低位)
val stride = UInt(STRIDE_BITS.W) // 已学习到的步长
val confidence = UInt(STRIDE_CONF_BITS.W) // 置信度 [0, 3]
val hash_pc = UInt(HASH_TAG_WIDTH.W) // PC 哈希(索引键)
}

关键参数L1StridePrefetcher.scala:40-56):

参数 含义
STRIDE_ENTRY_NUM 10 训练表条目数(以 PC 哈希索引
STRIDE_CONF_BITS 2 置信度位宽,MAX_CONF = 3
STRIDE_LOOK_AHEAD_BLOCKS 2 基础预取深度
l1_stride_ratio 2 L1 预取深度 = stride << 2(约 4 倍步长)
l2_stride_ratio 5 L2 预取深度 = stride << 5(约 32 倍步长)

置信度机制与发射条件update() 方法):

  • 新步长 new_stride = new_vaddr - pre_vaddr
  • new_stride == strideconfidence++(上限 MAX_CONF=3);
  • 否则 confidence--,且仅当 confidence ≤ 1 时才替换 stride(低置信下才允许切换步长,避免抖动);
  • 只有 confidence == MAX_CONF 时才允许发射预取

L1/L2 双深度发射(4 级流水 S0–S4,L1StridePrefetcher.scala:191-233):

1
2
3
4
val s2_l1_depth   = s2_stride << l1_stride_ratio   // 近距离
val s2_l1_pf_vaddr = s2_vaddr + s2_l1_depth // → L1 预取地址(sink=SINK_L1)
val s2_l2_depth = s2_stride << l2_stride_ratio // 远距离
val s2_l2_pf_vaddr = s2_vaddr + s2_l2_depth // → L2 预取地址(sink=SINK_L2)
  • S3 输出 L1 预取请求,S4 输出 L2 预取请求;
  • 与 Stream 的协同:S0 会向 Stream 表发起 stream_lookup_req,若该地址已被 Stream 识别为活跃流,则抑制 Stride 的 L1 输出(避免两个算法重复预取同一区域)。

Stream 预取器

文件:L1StreamPrefetcher.scala

核心数据结构 StreamBitVectorBundleL1StreamPrefetcher.scala:54-111):

1
2
3
4
5
6
7
class StreamBitVectorBundle extends XSBundle {
val tag = UInt(REGION_TAG_BITS.W) // 流所在 region 的 tag
val bit_vec = UInt(BIT_VEC_WIDTH.W) // 位图:region 内每个 cache block 一位
val active = Bool() // 流是否激活
val cnt = UInt(...) // 访问计数
val decr_mode = Bool() // 是否为递减(反向)流
}

关键参数L1StreamPrefetcher.scala:16-52):

参数 含义
BIT_VEC_ARRAY_SIZE 16 流检测表条目数
BIT_VEC_WIDTH 16 一个 region 含 16 个 cache block
ACTIVE_THRESHOLD BIT_VEC_WIDTH - 4 = 12 region 内命中超过 12 块即激活该流
DEPTH_CACHE_BLOCKS 16 L1 预取深度(基础 16 块 = 1KB)
WIDTH_CACHE_BLOCKS 2 每次预取宽度 2 块
L2_DEPTH_RATIO 2 L2 深度 = L1 深度 × 2
L3_DEPTH_RATIO 3 L3 深度 = L1 深度 × 3

工作原理(5 级流水 S0–S5)

  • S0 邻域检测:对当前访问,同时检查 region_tagregion_tag+1region_tag-1 三个 region。只有当相邻 region 存在活跃流时,才认为本次访问是流的延续;
  • S1 分配/更新:若是新流,根据命中的是 +1 还是 -1 邻域决定 decr_mode(正向流 / 反向流);
  • S2 生成三级预取地址
    1
    2
    3
    s2_l1_vaddr = s2_vaddr + dynamic_depth              // L1
    s2_l2_vaddr = s2_vaddr + (dynamic_depth << ratio) // L2
    s2_l3_vaddr = s2_vaddr + (dynamic_depth << l3_ratio)// L3
  • S3/S4/S5 分别输出 L1 / L2 / L3 预取请求;
  • dynamic_depth运行时可调 的(来自 FDP 反馈,见第 10 节)。

Stream 是唯一会主动产生 L3 预取(io.l3_req)的 L1 子算法——它在最远的 L3_DEPTH_RATIO=3 深度上预取,把数据提前拉到 L3。

SMS 预取器(Spatial Memory Streaming)

文件:SMSPrefetcher.scala(约 1358 行,是 L1 最复杂的预取器)

SMS 针对 空间局部性模式:把虚拟地址空间划分为固定大小的 region,学习”访问了 region 内某个偏移后,通常还会访问哪些偏移”的足迹(footprint)。

Region 概念SMSPrefetcher.scala:63-101):

1
2
3
虚拟地址布局: | REGION_TAG | REGION_OFFSET(4b) | BLOCK_OFFSET(6b) |
└─ region 唯一标识 ─┘ └─ region 内块号(0~15) ┘ └─ 块内偏移 ┘
REGION_SIZE = 1024 (1KB),REGION_BLKS = 16(每 region 16 个 64B 块)

五大子模块

  1. StridePFSMSPrefetcher.scala:139-257):SMS 内置的 stride 检测器(16 条目,带符号 stride,2 级置信度),用于捕捉 region 间的规则步长。

  2. ActiveGenerationTable (AGT)SMSPrefetcher.scala:286-567):活跃生成表,跟踪当前正在被访问的 region,累积其访问位图 region_bits。当一个 region “成熟”(访问计数超过 act_threshold,默认 12)时,把它的足迹”毕业”到 PHT。表项 AGTEntryregion_bits(已访问块)、region_tagaccess_cntdecr_mode 等。

  3. PatternHistoryTable (PHT)SMSPrefetcher.scala:584-907):模式历史表(64 项 × 2 路,SRAM 实现),以 2-bit 饱和计数器 记录每个 region 内各块的访问概率。当新访问命中 PHT 时,按计数器预测”接下来要访问哪些块”,可同时生成当前 region、下一 region(incr)、上一 region(decr)三种预取模式。

  4. PrefetchFilterSMSPrefetcher.scala:920-1112):预取过滤表(16 项),负责 V→P 翻译(发 TLB 请求)、PMP 权限检查、去重,并把 region 位图按块逐个发射为最终预取请求。Drop 条件包括 page/access/guest-page fault、uncache/MMIO、PMP 拒绝。

  5. SMSTrainFilterSMSPrefetcher.scala:1114-1214):训练流去重(8 项 FIFO,块级去重,保持程序序)。

SMS 的输出(关键)SMSPrefetcher.scala:1326-1336):

1
2
3
4
5
6
7
// SMS 只向 L2 发预取
io.l2_req.valid := pf_filter.io.l2_pf_addr.valid && io.enable && is_valid_address
io.l2_req.bits.source := MemReqSource.Prefetch2L2SMS.id.U // 来源标记为 SMS

// SMS 不向 L1 发预取
io.l1_req.valid := false.B
io.l1_req.bits.pf_source.value := L1_HW_PREFETCH_NULL

即:SMS 的预取目标固定是 L2,来源标记为 Prefetch2L2SMS(数值 10)。

L1 预取顶层整合:L1PrefetchComponent.scala

文件:L1PrefetchComponent.scala(约 923 行)

核心是 MutiLevelPrefetchFilter(多级预取过滤器)L1PrefetchComponent.scala:254-843),它把 Stream 和 Stride 的请求汇聚,并维护 两张过滤表

  • L1 过滤表MLP_L1_SIZE = 16):只处理 sink == SINK_L1 的请求;
  • L2/L3 过滤表MLP_L2L3_SIZE = 16):处理 sink == SINK_L2 / SINK_L3 的请求。

过滤表项 MLPReqFilterBundleL1PrefetchComponent.scala:254-359)关键字段:

1
2
3
4
5
val bit_vec  = UInt(BIT_VEC_WIDTH.W)   // 待预取块位图
val sent_vec = UInt(BIT_VEC_WIDTH.W) // 已发送块位图
val sink = UInt(SINK_BITS.W) // 目标层级 SINK_L1/L2/L3
val is_vaddr = Bool() // 当前地址是虚地址还是已翻译的物理地址
val source = new L1PrefetchSource() // 预取来源

三路输出分发L1PrefetchComponent.scala:700-820):

1
2
3
4
5
6
7
8
9
10
11
12
// → L1:DecoupledIO,有反压,逐块发送
io.l1_req.valid := s1_pf_valid && !s1_pf_evict && in_pmem && io.enable

// → L2:ValidIO,即生即发;来源做映射
io.l2_pf_addr.valid := l2_pf_req_arb.io.out.valid
io.l2_pf_addr.bits.source := MuxLookup(source.value, Prefetch2L2Unknown.id.U)(Seq(
L1_HW_PREFETCH_STRIDE -> MemReqSource.Prefetch2L2Stride.id.U, // 12
L1_HW_PREFETCH_STREAM -> MemReqSource.Prefetch2L2Stream.id.U // 11
))

// → L3:ValidIO,仅当 sink==SINK_L3,直接转发物理地址
io.l3_pf_addr.valid := l3_pf_req_arb.io.out.valid

预取器优先级L1PrefetchComponent.scala:890-903):在 L1 和 L2/L3 两条仲裁路径上,Stream 优先级都高于 Stride

1
2
3
4
pf_queue_filter.io.l1_prefetch_req.bits := Mux(
stream_bit_vec_array.io.l1_prefetch_req.valid, // Stream 优先
stream_bit_vec_array.io.l1_prefetch_req.bits,
stride_meta_array.io.l1_prefetch_req.bits)

L1 预取来源到 MemReqSource 的映射小结

L1 内部(L1PrefetchSource 映射到(MemReqSource 目标
L1_HW_PREFETCH_STRIDE (2) Prefetch2L2Stride (12) L2
L1_HW_PREFETCH_STREAM (3) Prefetch2L2Stream (11) L2
SMS(独立模块,直接赋值) Prefetch2L2SMS (10) L2

L1 → L2 / L1 → L3 通路(旁路 sideband)

载体:PrefetchRecv Bundle

L2 侧定义(coupledL2/Common.scala:345):

1
2
3
4
5
6
class PrefetchRecv extends Bundle {
val addr = UInt(64.W) // 预取的物理地址
val pf_source = UInt(MemReqSource.reqSourceBits.W) // 预取来源(4 bit)
val addr_valid = Bool() // 地址有效
val l2_pf_en = Bool() // L2 预取总开关
}

L3 侧定义(huancun/Common.scala:234少一个 pf_source):

1
2
3
4
5
class PrefetchRecv extends Bundle {
val addr = UInt(64.W)
val addr_valid = Bool()
val l2_pf_en = Bool()
}

这两个 Bundle 通过 BundleBridgeSource / BundleBridgeSink 点对点连接——这是 Diplomacy 框架里一条 独立于 TileLink 缓存总线 的旁路(sideband),专门用来传”预取地址提示”。

L1 → L2:在 MemBlock 中仲裁合并

文件:MemBlock.scala:593-609

1
2
3
4
5
6
7
8
9
10
11
12
13
prefetcherOpt.foreach(sms_pf => {           // sms_pf = SMS 预取器
l1PrefetcherOpt.foreach(l1_pf => { // l1_pf = L1Prefetcher (Stream/Stride)
val sms_pf_to_l2 = DelayNWithValid(sms_pf.io.l2_req, 2) // SMS 的 L2 请求,延迟 2 拍
val l1_pf_to_l2 = DelayNWithValid(l1_pf.io.l2_req, 2) // Stream/Stride 的 L2 请求,延迟 2 拍

// 仲裁:L1Prefetcher(Stream/Stride) 优先于 SMS
outer.l2_pf_sender_opt.get.out.head._1.addr_valid := sms_pf_to_l2.valid || l1_pf_to_l2.valid
outer.l2_pf_sender_opt.get.out.head._1.addr :=
Mux(l1_pf_to_l2.valid, l1_pf_to_l2.bits.addr, sms_pf_to_l2.bits.addr)
outer.l2_pf_sender_opt.get.out.head._1.pf_source :=
Mux(l1_pf_to_l2.valid, l1_pf_to_l2.bits.source, sms_pf_to_l2.bits.source)
outer.l2_pf_sender_opt.get.out.head._1.l2_pf_en :=
RegNextN(io.ooo_to_mem.csrCtrl.pf_ctrl.l2_pf_enable, 2, Some(true.B))

要点

  • SMS 与 Stream/Stride 的 L2 预取请求 在 MemBlock 里合并成一条 l2_pf_sender 旁路输出;
  • 仲裁规则:Stream/Stride(l1_pf)优先于 SMS(sms_pfMux 条件以 l1_pf_to_l2.valid 为先);
  • pf_source 字段一并传递,下层据此知道是 SMS / Stream / Stride;
  • 性能计数器 sms_block_by_l1pf 专门统计”SMS 被 L1Prefetcher 抢占”的次数(MemBlock.scala:625)。

L2 顶层接收(CoupledL2.scala:362-366):

1
2
3
4
5
6
pf_recv_node match {
case Some(x) =>
prefetcher.get.io.recv_addr.valid := x.in.head._1.addr_valid
prefetcher.get.io.recv_addr.bits.addr := x.in.head._1.addr
prefetcher.get.io.recv_addr.bits.pfSource := x.in.head._1.pf_source
}

L1 → L3:跨核汇聚到 L3

文件:MemBlock.scala:611-613

1
2
3
4
val l1_pf_to_l3 = ValidIODelay(l1_pf.io.l3_req, 4)   // 只有 L1Prefetcher(Stream) 的 L3 请求,延迟 4 拍
outer.l3_pf_sender_opt.foreach(_.out.head._1.addr_valid := l1_pf_to_l3.valid)
outer.l3_pf_sender_opt.foreach(_.out.head._1.addr := l1_pf_to_l3.bits)
outer.l3_pf_sender_opt.foreach(_.out.head._1.l2_pf_en := RegNextN(...pf_ctrl.l2_pf_enable, 4, ...))

要点

  • 只有 L1Prefetcher(即 Stream,因为 Stride 不发 L3)io.l3_req 会通过 l3_pf_sender 旁路发出;SMS 不发 L3
  • 延迟 4 拍(比 L2 通路的 2 拍更长,匹配 L3 更远的距离);
  • 注意 L3 的 PrefetchRecv 没有 pf_source 字段,所以到 L3 时来源信息丢失

SoC 顶层把各个核的 L3 预取请求 汇聚 到 L3(Top.scala:376-382):

1
2
3
4
5
6
7
8
9
10
11
12
l3.pf_recv_node match {
case Some(recv) =>
// 多个核的 L3 预取请求做 OR 汇聚
l3_pf_sender_opt.get.out.head._1.addr_valid :=
VecInit(memblock_pf_recv_nodes.map(_.get.in.head._1.addr_valid)).asUInt.orR
for (i <- ...) {
when(memblock_pf_recv_nodes(i).get.in.head._1.addr_valid) {
l3_pf_sender_opt.get.out.head._1.addr := memblock_pf_recv_nodes(i).get.in.head._1.addr
l3_pf_sender_opt.get.out.head._1.l2_pf_en := memblock_pf_recv_nodes(i).get.in.head._1.l2_pf_en
}
}
}

L2/L3 接收端:PrefetchReceiver

L2 PrefetchReceivercoupledL2/prefetch/PrefetchReceiver.scala:36-60)——透传来源

1
2
3
4
5
6
io.req.bits.tag      := parseFullAddress(io.recv_addr.bits.addr)._1
io.req.bits.set := parseFullAddress(io.recv_addr.bits.addr)._2
io.req.bits.needT := false.B // 预取永远是 read
io.req.bits.source := 0.U // 固定为 dcache
io.req.bits.pfSource := io.recv_addr.bits.pfSource // ★ 透传来源(SMS/Stream/Stride)
io.req.valid := enable && io.recv_addr.valid

L3 PrefetchReceiverhuancun/prefetch/PrefetchReceiver.scala:20-33)——来源被硬编码

1
2
3
4
5
6
io.req.bits.tag      := parseFullAddress(io.recv_addr.bits)._1
io.req.bits.set := parseFullAddress(io.recv_addr.bits)._2
io.req.bits.needT := false.B
io.req.bits.source := 0.U
io.req.bits.pfSource := MemReqSource.Prefetch2L2Stream.id.U // ★ TODO: 暂时统一标记为 Stream
io.req.valid := io.recv_addr.valid

L2 预取器(coupledL2)

文件:coupledL2/prefetch/Prefetcher.scala(顶层)

L2 预取顶层 Prefetcher 把多个预取源汇聚成一条统一的预取请求流。默认配置(见第 12 节)下包含:

1
PrefetchReceiver (来自 L1 的 Hint)  +  VBOP  +  PBOP  +  TP(可选)

预取请求/训练/响应 Bundle

文件:Prefetcher.scala:112-176

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class PrefetchReq extends PrefetchBundle {
val tag, set = ...
val vaddr = Option[UInt] // 可选虚地址
val needT = Bool() // 是否需要写权限(PREFETCH_WRITE vs READ)
val source = UInt(sourceIdBits.W)
val pfSource = UInt(MemReqSource.reqSourceBits.W) // 跨层级来源(4 bit)
def isBOP/isPBOP/isSMS/isTP : Bool // 来源判定
def needAck : Bool = isBOP || isPBOP // BOP/PBOP 需要应答
def fromL2 : Bool = isBOP || isPBOP || isSMS || isTP
}

class PrefetchTrain extends PrefetchBundle {
val tag, set, needT, source, vaddr = ...
val hit = Bool() // L2 是否命中
val prefetched = Bool() // 该块是否曾被预取
val pfsource = UInt(PfSource.pfSourceBits.W) // L2 内部来源(3 bit)
val reqsource = UInt(MemReqSource.reqSourceBits.W) // 原始请求来源(4 bit)
}

class PrefetchIO extends PrefetchBundle {
val train = Flipped(DecoupledIO(new PrefetchTrain)) // L2 流水线 → 预取器(训练)
val tlb_req = new L2ToL1TlbIO(nRespDups = 1) // 虚拟地址翻译
val req = DecoupledIO(new PrefetchReq) // 预取器 → L2 流水线
val resp = Flipped(DecoupledIO(new PrefetchResp)) // 预取响应(→ 训练)
val recv_addr = Flipped(ValidIO(...)) // ★ 来自 L1 的旁路提示入口
}

BOP(Best-Offset Prefetch)

文件:coupledL2/prefetch/BestOffsetPrefetch.scala(L2 最大的预取器,约 32KB)

BOP 的思想:在一组候选 offset 中,通过”打分竞赛”找出当前最能命中的 best offset,然后用 预取地址 = 当前地址 + bestOffset 预取

三大组件

  1. RecentRequestTable (RRT)(最近请求表):记录最近发生的访问地址(去掉块内偏移),用 双哈希异或 索引:

    1
    2
    3
    4
    def hash1(addr) = lineAddr(addr)(rrIdxBits-1, 0)
    def hash2(addr) = lineAddr(addr)(2*rrIdxBits-1, rrIdxBits)
    def idx(addr) = hash1(addr) ^ hash2(addr) // XOR 折叠
    def tag(addr) = lineAddr(addr)(rrTagBits+rrIdxBits-1, rrIdxBits)

    每项仅 {valid(1b), tag(12b)},用单端口 SRAM(256 项)实现。

  2. OffsetScoreTable(offset 打分表):状态机 s_idle → s_learn。每个合格的 L2 访问测试一个候选 offset d:若 (当前地址 - d) 在 RRT 中命中,则该 offset 的 score++。学习终止条件:某 offset 达到 scoreMax(31)轮数达到 roundMax(50);结束后选出 bestOffset 作为下一轮 prefetchOffset。若 bestScore < badScore,则 prefetchDisable 置位(暂停预取)。

  3. DelayQueue(延迟队列,16 项,dQLatency=175 拍):把 miss 地址延迟一段时间再写入 RRT,模拟”这个地址确实经历了一次内存访问延迟”,使打分更符合真实时序。

关键参数BOPParameters):

参数 VBOP PBOP
virtualTrain(虚拟地址训练) true false
rrTableEntries 256 256
scoreBits / scoreMax 5 / 31 5 / 31
roundMax 50 50
badScore 2 1
offsetList 规模 ~96 个 offset ~32 个 offset(范围更小)

VBOP vs PBOP

  • VBOP(虚拟地址 BOP):用虚地址训练,生成的预取地址需要经 PrefetchReqBuffer(16 项)发 TLB 请求 翻译成物理地址,跨页时按页边界处理。来源标记 Prefetch2L2BOP
  • PBOP(物理地址 BOP):直接用物理地址,无需 TLB,跨物理页则直接丢弃预取。来源标记 Prefetch2L2PBOP

L2 预取顶层仲裁

文件:Prefetcher.scala:232-426

使能控制(来自核心 CSR PrefetchCtrlFromCore):

1
2
3
4
val pfRcv_en = RegNextN(pfCtrl.l2_pf_master_en && pfCtrl.l2_pf_recv_en, 2)
val pbop_en = pfCtrl.l2_pf_master_en && pfCtrl.l2_pbop_en
val vbop_en = pfCtrl.l2_pf_master_en && pfCtrl.l2_vbop_en
val tp_en = pfCtrl.l2_pf_master_en && pfCtrl.l2_tp_en

训练分配:BOP/PBOP 不接收 L1 预取触发的训练(避免被 L1 预取行”污染”训练):

1
2
3
vbop.io.train.valid := io.train.valid && (io.train.bits.reqsource =/= L1DataPrefetch.id.U)
pbop.io.train.valid := io.train.valid && (io.train.bits.reqsource =/= L1DataPrefetch.id.U)
tp.io.train <> io.train

固定优先级仲裁ParallelPriorityMuxPrefetcher.scala:343-380):

1
PrefetchReceiver(L1 提示)  >  VBOP  >  PBOP  >  TP

汇聚后进入 PrefetchQueue(流式队列:永远 ready,满了丢最旧的,保证总是发最新请求),再经 1 拍流水线送往 L2 主流水线。


这是 通路 B 的一个重要分支:L2 的 BOP/TP 等预取器产生的 PrefetchReq不走旁路,而是在 SinkA 中被转换成 TileLink Hint 操作,进入 L2 主流水线统一处理。

文件:coupledL2/SinkA.scala:79-116

1
2
3
4
5
6
7
8
9
10
11
12
def fromPrefetchReqtoTaskBundle(req: PrefetchReq): TaskBundle = {
val task = Wire(new TaskBundle)
task.channel := "b001".U // A 通道
task.opcode := Hint // ★ TileLink Hint 操作码
task.param := Mux(req.needT, PREFETCH_WRITE, PREFETCH_READ)
task.tag/set/off := parseAddress(Cat(req.tag, req.set, 0.U))
task.sourceId := req.source
task.reqSource := req.pfSource // ★ 预取来源保留到 reqSource
task.fromL2pft.foreach(_ := req.needAck) // BOP/PBOP 需要应答
task.vaddr.foreach(_ := req.vaddr.getOrElse(0.U))
task
}

SinkA 仲裁SinkA.scala:117-131)——外部 demand(A 通道)优先于预取:

1
2
3
4
5
io.task.valid := io.a.valid || io.prefetchReq.get.valid
io.task.bits := Mux(io.a.valid, fromTLAtoTaskBundle(io.a.bits),
fromPrefetchReqtoTaskBundle(io.prefetchReq.get.bits))
io.a.ready := io.task.ready
io.prefetchReq.get.ready := io.task.ready && !io.a.valid // demand 优先

这条 Hint 在 L2 内部被消费——它驱动 L2 去判断命中/缺失,缺失则向 L3/主存发 Acquire 取数。L2 并不会把这个 Hint 当作”提示 L3 预取”再转发给 L3


L3 预取器(huancun)

文件:huancun/prefetch/Prefetcher.scalaPrefetchReceiver.scalaBestOffsetPrefetch.scala

L3 预取顶层的三种配置模式

Prefetcher.scalamatch-case 按参数类型选择实现:

  1. 纯 BOPcase bop: BOPParameters):BestOffsetPrefetch → PrefetchQueue → Pipeline → io.req
  2. L1Hint + BOPcase receiver: PrefetchReceiverParams):PrefetchReceiver(接收旁路提示,2 拍延迟)与 BOP 并存,接收器优先
  3. 纯接收器case receiver: L3PrefetchReceiverParams):只有 PrefetchReceiver无算法生成

默认配置是第 3 种——所以默认情况下 L3 不主动生成预取,只执行 L1 经旁路送来的预取地址

L3 的 BOP(默认未启用)

huancun/prefetch/BestOffsetPrefetch.scala 实现了一个与 L2 结构相同、规模更小的 BOP:

  • RRT 256 项、单端口 SRAM、双哈希 XOR 索引;
  • OffsetScoreTable 36 个 offset、scoreBits=5roundMax=50badScore=1
  • 生成预取时做 跨页保护getPPN(newAddr) =/= getPPN(oldAddr) 则不发);
  • 来源标记 Prefetch2L2BOP

它在默认 L3 配置里没有被实例化,属于”可选能力”。

L3 接收端来源信息丢失

5.4 所述,L3 的 PrefetchReceiver 把所有来自上层的预取硬编码为 Prefetch2L2StreamPrefetchReceiver.scala:30,带 TODO: add L3 pfSource),因为旁路 huancun.PrefetchRecv 没有携带 pf_source 字段。这是一个已知的、待 Hint 机制完善后解决的简化。


Temporal Prefetch:经主存的跨层级时间预取

L2 侧的 TP 引擎

文件:coupledL2/prefetch/TemporalPrefetch.scala

参数TPParameters):tpTableEntries=16384tpTableAssoc=16(→ nrSet=1024)、triggerQueueDepth=4tpThrottleCycles(发射节流)。

多阶段流程

  • S0:训练请求查询 L2 本地的 tpMetaTable(记录 trigger tag);
  • S1:tag 匹配,判定 meta hit/miss,选 victim way;
  • S2 决策
    • 若 meta hit(且配置 hitAsTrigger)→ 把该地址作为新 trigger 推入 triggerQueue,并发起 读 TPmetadataReadQueue);
    • 若 meta misstriggerQueue 非空 → 启动 Recorder,开始记录后续 miss 地址;
  • Recorder:把连续 miss 地址(>> offsetBits)存入 recorder_data,攒够 recordThres 个后 → 写本地 tpMetaTable + 发起 写 TPmetadataWriteQueue);
  • Sender:从读回的 tpDataQueue 取出地址序列,按 tpThrottleCycles 节流逐个发射预取请求,来源标记 Prefetch2L2TP

与 L3 的接口TemporalPrefetch.scalatpmeta_port):

1
2
3
4
5
class TPmetaIO(...) extends Bundle {
val req = DecoupledIO(new TPmetaReq(...)) // 读/写 TPmeta(wmode 区分)
val resp = Flipped(ValidIO(new TPmetaResp(...)))
}
// 写优先于读

L3 侧的 TPmeta 存储

文件:huancun/prefetch/TPmeta.scalaTPmetaParameters.scala

L3 的 TPmeta 模块就是一块 专门存放 TP 元数据的 SRAM

1
2
3
4
5
6
7
8
9
10
// TPmetaParameters(DefaultTPmetaParameters)
metaEntries = 16384, metaAssoc = 16, nrSet = 1024
busBytes = 256 (busBits=2048), deltaBits = 36, nrDelta = 16

// metaEntry(TPmeta.scala:32-36)
class metaEntry extends TPmetaBundle {
val rawData = Vec(log2Floor(512/(fullAddressBits-offsetBits)), UInt((fullAddressBits-offsetBits).W))
val hartid = UInt(hartIdLen.W) // 多核隔离
}
// SRAM: set=1024, way=16, singlePort=true, shouldReset=false(跨启动保留 pattern)

读写时序(TPmeta.scala:38-73):

  • 写:wmode=true 时按 way 的 one-hot 写入 rawData + hartid
  • 读:2 拍延迟返回;响应需校验 hartid 匹配,保证多核数据隔离;
  • shouldReset=false:复位时 不清零,让学习到的访问模式可以跨程序启动保留。

TP 跨层连接(SoC 顶层)

文件:Top.scala:185-196

1
2
3
4
5
6
7
l3.tpmeta_recv_node.foreach(recv => {
recv := core.core_l3_tpmeta_source_port.get // 各核 L2 的 TPmeta 请求 → L3
})
l3.tpmeta_send_node.foreach(send => {
val broadcast = LazyModule(new ValidIOBroadcast[TPmetaResp]())
core.core_l3_tpmeta_sink_port.get := broadcast.node // L3 的 TPmeta 响应 → 广播回各核 L2
})

完整 TP 数据流

1
2
3
4
5
6
7
8
9
L2 TP Recorder ──TPmetaReq(write)──▶ core_l3_tpmeta_source_port ──▶ l3.tpmeta_recv_node

L3 TPmeta SRAM 写入

L2 TP(再次命中 trigger)──TPmetaReq(read)──▶ ...recv_node ──▶ TPmeta SRAM 读出

l3.tpmeta_send_node ──ValidIOBroadcast──▶ 各核 core_l3_tpmeta_sink_port

L2 TP Sender 重放预取(pfSource=Prefetch2L2TP)

这里 L3 充当的是 TP 的”远端大容量元数据存储”,而不是预取算法本身。TP 的”大脑”在 L2,”记忆库”在 L3


FDP:反馈式预取调节

文件:FDP.scala(Feedback Directed Prefetching)

FDP 通过运行时统计 预取准确率、时效性、污染率,动态调节 L1 预取的激进程度(dynamic_depth),形成闭环。

三个辅助结构

  1. CounterFilterFDP.scala:61-150):FIFO(大小 ≈ 4 级流水 × Load 单元数),去除 Load 流水线中对 同一 cache line 的重复 prefetch-hit 计数。

  2. BloomFilterFDP.scala:170-206):布隆过滤器,快速判断一个预取地址 是否已在途(MSHR)或已预取,避免重复发射。哈希方式是把块物理地址高低两段 XOR:

    1
    def get_addr(paddr) = { val b = paddr(.., blockOffBits); b(low) ^ b(high) }

    接口:set(发射时置位)/ clr(填充完成时清除)/ query(新请求查询)

  3. FDPrefetcherMonitorFDP.scala:228-303):每 INTERVAL=8192 周期统计一批计数器:

    计数器 含义
    total_prefetch 总预取数
    useful_prefetch 有用预取(被 demand 命中)数
    late_prefetch 晚到预取数
    demand_miss 需求 miss 数
    pollution 污染(因预取导致的 miss)数

    据此计算 准确率 = useful/total时效性 = late/total污染率 = pollution/demand_missXSPerfRolling),输出调节信号:

    1
    2
    3
    4
    5
    6
    class PrefetchControlBundle extends XSBundle {
    val enable = Bool() // 总开关
    val dynamic_depth = UInt(6.W) // 动态预取深度 → 喂给 Stream 的 dynamic_depth
    val confidence = UInt(1.W) // 是否允许覆盖 Load 端口
    val flush = Bool() // 清空预取状态
    }

一条预取请求的完整生命周期

把前面所有环节串起来,以下是六条预取路径的对比。

路径 1:L1 自取(最快,sink = SINK_L1)

1
2
3
Load → L1Prefetcher(Stream/Stride) 训练 → 生成 sink=SINK_L1 的请求
→ MutiLevelPrefetchFilter(L1 过滤表) → io.l1_req (DecoupledIO)
→ Pipeline → 抢占 L1 D-Cache load 流水线端口 → 直接填入 L1
  • 内部来源标记 L1_HW_PREFETCH_STREAM/STRIDE;有反压;零跨层延迟。

路径 2:L1 让 L2 取(通路 A 旁路,最常见的跨层)

1
2
3
4
5
L1Prefetcher(Stream/Stride) 或 SMS → io.l2_req (ValidIO)
→ MemBlock 仲裁(L1Prefetcher 优先于 SMS) + 延迟 2 拍
→ l2_pf_sender (PrefetchRecv 旁路, 带 pf_source)
→ L2.pf_recv_node → L2.PrefetchReceiver(透传 pfSource)
→ L2 PrefetchQueue → L2 SinkA(转 Hint) → L2 主流水线 → 填入 L2
  • 来源:Prefetch2L2SMS/Stream/Stride(10/11/12);无 TLB(地址已是物理);无反压。

路径 3:L2 自取(通路 B,L2 内部)

1
2
3
L2 BOP/PBOP/TP 训练(基于 L2 的 miss/prefetch-hit) → PrefetchReq(pfSource=BOP/PBOP/TP)
→ L2 SinkA.fromPrefetchReqtoTaskBundle → Hint task(reqSource=pfSource)
→ L2 主流水线判定命中/缺失 → 缺失则向下取数 → 填入 L2
  • BOP/PBOP 的 needAck=true;BOP/PBOP 不接受 L1 预取触发的训练

路径 4:L1 让 L3 取(通路 A 旁路,跨两级)

1
2
3
4
L1Prefetcher(仅 Stream, L3_DEPTH_RATIO=3) → io.l3_req (ValidIO)
→ MemBlock 延迟 4 拍 → l3_pf_sender (huancun.PrefetchRecv, 无 pf_source)
→ Top 跨核 OR 汇聚 → L3.pf_recv_node → L3.PrefetchReceiver(来源硬编码 Stream)
→ L3 PrefetchQueue → L3 主流水线 → 填入 L3
  • SMS 不走这条;到 L3 来源信息丢失(TODO)。

路径 5:L3 自取(默认未启用)

1
(仅当 L3 配置 BOPParameters 时)L3 BOP 训练 → PrefetchReq → L3 主流水线 → 填入 L3
  • 默认 L3 只配 L3PrefetchReceiverParams,此路径默认关闭。

路径 6:TP 经主存(通路 C,跨层最远)

1
2
3
4
5
L2 TP Recorder 记录 miss 序列 → TPmetaReq(write) → core_l3_tpmeta_source_port
→ L3 TPmeta SRAM(16K×16, 跨启动保留)
... 再次命中 trigger ...
L2 TP → TPmetaReq(read) → L3 TPmeta 读出 → tpmeta_send_node → ValidIOBroadcast → 各核 L2
→ L2 TP Sender 按节流重放 → PrefetchReq(pfSource=Prefetch2L2TP) → 填入 L2