L2 / L3 Cache
GSys LibreCore places L2 and optional L3 below L1, on the memory-side / APU
path. This is an integration product, not an L1 rewrite. L1 remains in
core/cache_subsystem/; L2/L3 live under corev_apu/l2_cache/ and
corev_apu/l3_cache/.
Playbook: agents/guides/AGENTS-l2l3-cache.md.
Scaffold status: architecture/l2-l3-cache/README.md.
Hierarchy
core L1(s)
→ coherence hub (multi-core)
→ L2 (g6lc_l2_top)
→ L3 (optional; wraps L2 engine)
→ server prefetcher (optional)
→ DRAM / AXI memory| Level | Module | Config (concept) |
|---|---|---|
| L2 | g6lc_l2_top | L2En, size / assoc / MSHR / banks; eWT knobs WtAxiAllocEn, L2WriteUpdateEn, L2PostedWriteEn, L2CmoEn, L2TagSramEn |
| L3 | g6lc_l3_top | L3En (requires L2En), L3InclusiveEn |
| L2 prefetch | g6lc_l2_pf (inside g6lc_l2_top) | L2PrefetchEn + L2Pf* geometry/throttling — default off in every package (see below) |
| Prefetch (LLC edge) | g6lc_server_prefetcher | ServerPrefetchEn, streams, distance |
Default on many packages: L2En=0 (off). Server / OoO packages may auto-enable.
The whole hierarchy is write-through at every level — “enhanced write-through”
(eWT): hub-owned invalidation coherence, read-allocate, write-update, posted writes
and propagated CBO. Why not write-back, and what each piece buys, is recorded in
architecture/l2-l3-cache/ewt-caching.md; the per-milestone evidence is
core/ooo/AGENTS-ooo-plan.md T8a–T10e.
Product features (L2 done)
- MSHR line merge with concurrent fills and hit-under-miss same-line waiter merging
- Killed-fill retention — a back-invalidated fill entry cannot be merged into by a later reader
- Same-edge refill vs back-invalidation discard — a fill landing on the edge of its own kill is dropped
- OoO-gated write-service fairness (
FAIR_WRITES=OoOEn) and optional per-set round-robin victim selection (RR_EN) - Banked data arrays via
tc_sram - Non-cacheable bypass for MMIO / PMA
- Write-through + read-allocate policies as configured
- Parallel tags
L3 + server prefetcher
L3 reuses L2 engine structure at the next level. The server prefetcher sits on the L3→DRAM (or L2→DRAM) AXI edge:
- Next-line at configurable distance on demand miss
- Multi-stream stride training
- Demand always wins arbitration; prefetch only when AR is idle
Complements L1 HPDCACHE stride (HwPrefetchEn): L1 for tight loops, LLC edge
for server streams (packet buffers, page copy, guest memory).
Inclusive hierarchy (config-gated)
When enabled, victim / invalidation paths keep L1 (and L2 tags) coherent with
L3/L2 policy — see g6lc_l3_inclusive_inv and L2 tag match-inval ports in the
architecture README. Treat inclusive mode as a product option with its own
verification cost.
Invariants
- RVWMO preserved
- PMA — MMIO uncached via NC bypass
- CBO end-to-end when Zicbo* enabled
- Prefer 64 B lines with
Zic64bfor software alignment
WT cores, allocation and the optional L3 (2026-09-26)
Three facts a contributor needs before touching this layer:
- The WT L1 never asked the L2 to allocate.
core/axi_shim.svtags every requestCACHE_MODIFIABLEonly, andg6lc_l2_topallocates only when an allocate bit is set, sog6lc64_smt2,g6lc64_smt2_ooo_intandg6lc64_ooo_int2ran a serializing bypass (measured: 948,230 L2 requests, 0 hits on the four-hart OpenSBI boot).WtAxiAllocEnfixes the attribute at the WT adapter; it is on in every eWT package (the two SMT2 targets re-qualified with re-baselined anchors at M1a). - Inclusion is a package policy (
L3InclusiveEn). The WT L1 plus hub-owned invalidations do not need an inclusive L3, sog6lc64_ooo_int2_l3andg6lc64_smt2_l3run a non-inclusive 1 MiB / 16-way L3 (SRAM tags; MSHR 8 on int2_l3 withCohMaxOutstanding=8), and since M6 the in-order two-core HPDCACHE packagesg6lc64_stream8_l3andg6lc64_server_math_l3carry the same L3 (their strict two-core OpenSBI boots complete in 11,691,278 / 11,691,235 cycles);g6lc64_ooo_serverkeeps the inclusive back-invalidation. Before write-update the L3 recorded no hits on the boot (every write-through purged the written line); withL2WriteUpdateEnthe latency-40 boot records its first L3 hits, and the measured benefit is a footprint one — about −6 % on a 512 KiB scan at DRAM latency 40, nothing when the footprint fits the L2. - Tags can live in SRAM (
L2TagSramEn,g6lc_l2_tag.tech-spec.md): valid bits stay in flops, the tag row is read one cycle ahead (launched on every transition intoS_TAG), an inval-match read has port priority and clears one cycle later against the valids snapshotted in its read cycle. The flop path is retained and the two are cycle-identical on the full four-hart run and in a deterministic dual-simulation lane (L2TB_EQ_TAGS); a Yosys miter does not close on the unpaired tag-store init and is an open item.
Write-through update (L2WriteUpdateEn, 2026-09-27)
A write-through on a resident L2/L3 line used to purge the line (~6,060 purges per 8,192
stores on the write/read kernel; ~90k of the four-hart boot’s ~100k L2 misses followed a
purge). With L2WriteUpdateEn (default 0; on in every eWT package after the M1a
anchor re-baseline) an eligible
write — cacheable, no ATOP, no lock, burst inside the line — merges its bytes into the
resident line through the data port while the write-through still reaches memory. Same-line
in-flight fills are still killed, and merged bytes become visible only after memory’s B
response. Measured: boot l2_miss ~103k→~1.5k, l2_selfinv ~91.6k→~190, l2_wupd
~770–833k; strict boots pass on all enabled packages and the latency-40 boot improves
11.5–13.2 %. Observability: l2_wupd/l3_wupd fields in the testbench [mc_cache] line.
CBO / CMO end-to-end (L2CmoEn, M1a 2026-09-27)
Zicbom block operations reach every level of the hierarchy. The core emits
cmo_valid/cmo_op/cmo_addr and waits for cmo_done: on WT targets the CBO’s
store-port request is intercepted before the write buffer (the old decode hung the
store buffer and corrupted one byte), issued once the buffer is empty, and the store
buffer’s data_rvalid is pulsed when the cluster answers. The cluster’s
g6lc_cmo_engine round-robins the cores, broadcasts the L1 invalidation to every
core, match-invalidates the L2 (l2_back_inval_*, arbitrated behind the inclusive
victim source) and the L3 (l3_back_inval_*); cbo.clean/cbo.flush complete once
l2_write_idle_o/l3_write_idle_o report no unacknowledged write. On HPDCACHE
targets the adapter’s own CMO response is held until cmo_done_i, so the same
contract reaches the shared levels. cbo.zero works everywhere as a commit-queue
zero-write burst. Directed evidence: mc_cbo_ewt (a +mem_poke write behind the
caches becomes visible to issuer and peer alike) on int2, int2_l3, smt2, smt2_l3,
smt2_ooo_int, stream8 and server_math, with firing negative arms.
Also new: CohMaxOutstanding (hub shared AR/AW slot count, default 4, legal 2–14 —
8 on the int2 packages with MSHR 8, measured to remove all read-burst stalls on
tb_g6lc_coherence_credits).
Posted writes (L2PostedWriteEn, M1b 2026-09-28; WR_ID M1d)
The L2 write bypass no longer blocks on memory’s B. Every accepted write takes an
entry in g6lc_l2_wtrk (L2WriteTrackDepth, default 4 — slave id, downstream
id, line address, blocking flag, in AW-issue order); a postable write
(atop=='0 && !lock && id != FILL_ID) is forwarded on the reserved downstream
write id WR_ID = '1 - 1 (14; FILL_ID = 15 stays the bypass trail) and returns
the FSM to S_IDLE after the last forwarded W beat, so hits and misses are
served while the write drains. The slave B still comes from memory — it is
routed to the oldest tracker entry of its downstream id and returned with the
entry’s slave id, so software-visible ordering is unchanged. Because all posted
writes share WR_ID, AXI same-id ordering applies same-line posted writes in the
L2’s merge order with no L2-side serialization (M1d/T9e). ATOP, locked and
FILL-ID-alias writes keep the blocking path on their original id with a tracker
entry, so B routing is uniform. NC/lock bypass reads post the same way through a
read tracker (L2ReadTrackDepth) and an R arbiter that keeps each slave-side
burst atomic. Ordering: a cacheable read miss to a tracked line holds until the
write’s B (R1); a write to a tracked line holds only when the two downstream ids
differ — posted-vs-posted never holds, posted-vs-blocking still does (R2); the
same-line fill kill is unchanged (R3); B routes per downstream id to the oldest
entry (R4); a same-id hit or fill serve holds behind a tracked read (R5); the hub
credit bound covers fills + both trackers (R6). l2_write_idle_o now means
tracker-empty — that is what cbo.clean/cbo.flush wait on. Observability:
[mc_cache] gains l2_wtrk_full, l2_line_hold, l2_posted, l2_rdtrk plus
the hold split l2_hold_r1/l2_hold_r1_wu/l2_hold_r2 and the hub counters
hub_aw_sc_collide/hub_ar_hold/hub_aw_hold_slot/hub_aw_hold_other, and PMU
group-2 events 7/8 count posted-write hold cycles at L2/L3. Timing: the hazard CAMs
are small compares on registered request fields, the R arbiter adds one mux level on
the slave R path, and nothing enters the S_TAG hit compare — structural FO4 on
g6lc_l2_top stays 28.5. On in every L2 package.
Measured on the four-hart boots after WR_ID: l2_hold_r2 = 0, int2 latency-40
40,024,648 cycles (−1.7 % vs posted-before-WR_ID), int2_l3 latency-40 40,405,333
(−3.7 %). The hub-side read-behind-write hold (hub_ar_hold) is 0–285 cycles per
boot, hub_aw_sc_collide counts offers only (the collide is not in the AW grant,
hub_aw_hold_slot = 0), and the residual write stall is plain AW serialization —
about one hold cycle per posted write on the single W channel.
L2 stream/stride prefetcher (L2PrefetchEn, M5/N3 — proven, off)
g6lc_l2_pf trains on demand-miss commits per 4 KiB region (next-line, and stride
after two equal deltas), issues at most one PF-flagged MSHR fill per cycle with
L2PfMshrReserve entries kept for demand, drops candidates on resident lines,
in-flight fills, tracked writes and page boundaries, is killed by the same same-line
write as a demand fill, and marks installed ways so a demand hit counts as
l2_pf_useful once. Burst throttling (L2PfMaxOutstanding, L2PfQuiet, two-hit
confidence) halves the issues for the same gain. Measured on g6lc64_ooo_int2_l3:
512 KiB scan −9 % / −8 % (latency 0 / 40), write/read −12 % / −8 %, pointer chase
neutral — but the four-hart boot pays +0.3 % at both latencies while an enabled-but-
silent prefetcher boots byte-identical to the anchor, so the cost is the live offers,
not the datapath. It fails the “boot ≤ anchor” adoption bar and ships off in every
package (L3PrefetchEn likewise). Leaf tb_g6lc_l2_pf with four mutations, HUM
scenarios 62–64, PMU group-2 events 9/10.
For DRAM-latency experiments use the testbench’s DramLatency parameter
(corev_apu/tb/g6lc_tb_dram_latency.sv, first beat of each burst delayed). The vendor
axi_delayer fixed delay serializes every beat and truncates to 4 bits — it cannot model memory
latency and its results were withdrawn.
DTS / software
Describe next-level caches in device tree when software or firmware must know
geometry. Notes: architecture/l2-l3-cache/dts-l3-prefetch.md. Keep
AGENTS-dts-validation.md in sync when nodes change.
Verification
| Suite / path | Focus |
|---|---|
ooo-l3-tests | Stream + L2/L3 with OoO server target |
verif/tests/custom/l3/l3_stride_stream.S | Stride / stream stress |
| Multi-core stream | mc-stream-tests (when catalogued) |
g6lc-build verify --target g6lc64_ooo_int2 # WT + allocating L2 + COH_OOO cluster (integer anchor)
g6lc-build verify --target g6lc64_ooo_int2_l3 # + non-inclusive L3, SRAM tags, RV64GC+B, ring 16
g6lc-build verify --target g6lc64_smt2_l3 # single core, SMT2, L2 + L3
g6lc-build verify --target g6lc64_stream8_l3 # in-order two-core HPDCACHE + L3 (M6)
g6lc-build verify --target g6lc64_server_math_l3 # in-order two-core HPDCACHE + L3 (M6)
g6lc-build verify --target g6lc64_ooo_server # opt-in / unqualified (COH_FILTERED leg)
g6lc-build test --suite ooo-l3-testsLeaf and bench evidence lives under verif/tb/l2/ (tb_g6lc_l2_hum hit-under-miss and
posted-write contract, tb_g6lc_coherence_hub composed hub+L2+L3 with CMO scenarios,
tb_g6lc_l2_pf, and the SymbiYosys tasks in verif/tb/l2/formal/ for the write
tracker: no two entries share a line with different downstream ids, B conservation,
posted writes never enter the blocking B state).
PMU
Cluster → core PMU group 2 is wired for cache hierarchy events (see architecture README). Use counters when debugging thrash vs DRAM bandwidth.
Related
- Server Profiles
- Out-of-Order
- SMT — dual-hart MLP benefits from L2 MSHR
- Controllers: Controllers & PHY
- Spec: memory model / PMA / CMO under
agents/guides/AGENTS-l2l3-cache.md