Closing Predictor Technical Debt in LP, TAGE and SC

Abstract

During frontend testing the simulations showed the loop predictor was unable to produce high confidence predictions and its predictions were ignored. The problem was lack of speculation in the iteration count, and repeated overwriting of the count and confidence with stale updates.

The usefulness of the LP was improved by changing to a read-before-write semantic in updates, addition of a speculative counter with checkpoint based save and restore, and reducing the priority of TAGE/SC predictions when there is a high confidence LP prediction.

The second half of this session was used for technical debt closure. There were 16 items, most closed as specified.

BP-122 concerned the loop predictor and the addition of assertions. BP-126 addressed 16 known RTL/verification debt items, including the SC update queue implementation. BP-127 modified the SC indexing scheme.

The loop predictor

Observed tight loop behavior

A loop prediction is used only when its entry’s confidence is high. In the initial design the iteration count advanced only when a branch resolved. In a tight loop several iterations are predicted before the first resolves, so the count lagged the loop. Each update also rewrote the entry’s count and confidence from the snapshot taken at its prediction. Confidence never reached the threshold, so the loop predictor trained but its predictions were never used (TD#169).

The fix

A speculative iteration count now advances at prediction, at p2, after a p2 re-read of the entry. A checkpoint per FTQ entry is written at p2. After a redirect, the redirecting block’s count is restored from its checkpoint and every other count is marked unknown until its next exit. This closed TD#7.

The update now reads the entry before writing it, in one cycle with no bubble, and trains from the iteration number carried with the prediction. No update overwrites a more recent one.

A change was also made to direction priority. Previously TAGE at p2 and SC at p3 could override a high confidence LP prediction.

I ruled that a high confidence LP prediction should out rank both. fe_decisions.md section 3.3 was modified and the RTL was built to that ruling. The LP prediction also outranks an FTB fast-path direction. The direction ranking is now LP, SC, TAGE, FTB.

Loop predictor results

The predictor is trusted after five exits at the earliest. This was later than BP-121’s warm-up points. Programs lp and loops were lengthened and their warm-up points moved. Both columns run the lengthened programs, and the before column is the final RTL.

Program Cycles Mispredicts After warm-up LP directions, p1 / p2
lp 550 to 513 19 to 13 7 to 1 0 / 0 to 94 / 86
loops 556 to 537 22 to 18 2 to 1 0 / 0 to 56 / 49

In each program, the one conditional that still mispredicts after warm-up is the outer loop’s final exit. The other 17 programs did not change. tb_loop_pred grew to 9,408 checks, covering the read before write, the p2 re-read, restore and invalidation. tb_bp_cluster gained 20 checks of the priority and both restore paths. Each new case was shown failing against the received RTL, or, where it used the new ports, against a mutation of the final RTL.

Assertions for the previous task’s fixes

The purpose of TD#170 was addition of a property (assertion) for each BP-121 fix in units with an existing property file.

Property Unit Fix reverted Where it fired
R20 a_every_rsv_written ftq_resolve a mapped resolution does not write its slot unit test, all 19 programs
E11 a_rsv_end_kills_above ftq_entry no clear above a resolved end 10 programs
R21 a_jump_ft_is_jump_end ftq_resolve taken jump keeps the predicted fall-through unit test, 17 programs
E13 a_rsv_jump_ft_lands ftq_entry resolved fall-through not written unit test, 17 programs
F1 a_no_update_after_request ftq the hold before an FTB update removed unit test, all 19 programs
N11 a_mispredict_bundle ftq_npc no corrected history bundle unit test, all 19 programs
H1 a_corrected_newest bp_history corrected rollback ignored unit test, all 19 programs
RS1 a_dropped_p2_moves_nothing ras ras_p2_keep ignored tb_ras only

The loop predictor’s new behavior has properties LP1 and LP2, and the FTQ’s new slot bits N12, each fired by its own mutation. Twelve mutations of the new loop predictor logic, L1 to L12, each failed at least one check.

Two results are partial, and the IA reported both. H2, the bp_history checkpoint rewrite, was never shown firing on its own: the mutation that removes the rewrite also removes the corrected rollback, H1 fires first and stops the simulation, and no mutation of the rewrite alone was run. H2 holds in every run. RS1 fires only in tb_ras, in a new case TC-35, because the cycle it covers, a p2 RAS operation dropped with no p3 repair, does not occur in the 19 programs. The program-level check of that fix remains tb_fe_top’s check that no squashed block performs a RAS operation.

The rest of BP-122

The task carried three cleanups and a report.

sim_main_predecode.cpp and its build directory were deleted, and the bpu targets no longer leave obj/ or waves.fst in the tree.

tb_fe_top had a mode that ran every program in one simulation, carrying predictor tables between programs. It was removed. A reset between programs would have had to be forced into every RAM by the testbench, since the FTB has no reset and the other tables are filled once by fast-init plusargs.

TD#165 reported that ras.sv repairs the stack wrongly when a slot’s branch type at p3 differs from its type at p2. That cannot happen, because the p3 type is the p2 type registered, so I closed it as unreachable by construction.

The regression passed all 123 targets.

Closing the predictor debt

BP-126 took the sixteen open predictor RTL and verification items in the technical debt table as one task. Most closed as specified and are listed only in Technical Debt Referenced at the end of this post. The findings below were not expected. The task ran in automated mode, so its three design choices were taken provisionally, and I confirmed all three.

The tagged counters stepped the wrong way

tage_cntrl stepped each tagged counter up when the prediction was correct and down when it was wrong. The predicted direction is the counter’s top bit, so a correct not-taken prediction at 3’b011 became 3’b100, which predicts taken. tage_table_entry_formats.md says the counter steps toward the resolved direction. Taken entries step the same way under both rules, so only not-taken entries were affected. No unit test had failed, because the counter tests encoded the same rule as the controller. A new test written from the entry format failed seven of nine cases on the unfixed controller, all of them not-taken. ITTAGE was not changed: its counter is a target confidence, not a direction.

The folds through the hashes

tb_bp_cluster now keeps its own model of the global and path history, written from bp_history_decisions.md and driven from the cluster nets that feed bp_history, not from bp_history’s state. From it the monitor computes the index and tag that every table using a history fold should receive, and compares them on every prediction, including after rollbacks. With the starting bp_history.sv, IT5’s index fold read 0 where the model expected 0x100.

The SC index formula and its prose

Writing the SC part of that model, the IA found that sc_table_hash_rules.md gave two rules. The formula shifts inp_pc_p2, which is already PC[VA_WIDTH-1:1], right by PC_HASH_SHIFT of 2, so the index starts at PC[3]. The prose says the shift drops only the instruction offset, which with the C extension is PC[0]. The RTL followed the formula. The IA reported the conflict as PD-17 for a ruling, covered in the next section.

Coverage and accuracy

TAGE’s 73.8% line coverage (TD#100) was partly an accounting effect. The .info file counts a line as covered only when it is hit in every instance, so a line hit in T1 to T3 and not in T4 read as uncovered. The rest were real gaps, now tested. The TAGE RTL reads 588 of 614 lines, and each uncovered line is listed with the reason it cannot be reached.

All 36 programs retired exactly the same instructions before and after. Three items moved the cycle and mispredict figures, attributed by removing each from the final tree: T0’s reset to weakly taken, the SC update queue, which changed training timing, and the counter direction fix. The largest changes were 81 fewer cycles in loops and 14 more mispredicts in region.

BP-126 passed with 155 targets. Each of 27 reversions, run on scratch copies of the final tree, failed the check for its rule.

The SC index

I ruled for the prose, for the SC only. TAGE and ITTAGE keep PC_HASH_SHIFT. Each SC table’s 9-bit index moves from PC[11:3] to PC[9:1].

inp_pc_p2 is the block start PC, the same on both slots, so the change separates blocks, not branches. ST0, a PC slice with no fold, now separates blocks whose starts differ only in bits 2:1, but aliases blocks 1 KiB apart rather than 4 KiB apart. ST1 to ST3 XOR the same PC bits with their folds. The trade was unmeasured when I ruled.

BP-127 changed one line of sc_table.sv, and no port changed. A new tb_sc_table test checks ST0 and ST1 indices worked out by hand for two block starts that differ only in bits 2:1. On the starting tree both give the same ST0 index. The tb_bp_cluster monitor moved to the new rule and now covers ST0.

Program Cycles Mispredicts After warm-up
hist 889 to 871 (-18) 27 to 22 (-5) 5 to 1
lp 458 to 427 (-31) 10 to 8 (-2) 1 to 1
misp_call 509 to 492 (-17) 23 to 22 (-1) 16 to 15
multistart 440 to 412 (-28) 6 to 7 (+1) 1 to 2
ftbonly 525 to 525 (0) 10 to 11 (+1) 1 to 2
recur 763 to 765 (+2) 28 to 28 (0) 17 to 17

The other 30 programs did not change, and all 36 retired exactly. Restoring only the old index line restored every baseline. A trace of ST0 sharing shows both sides of the trade. In hist and lp, block starts that had shared an entry no longer do. In ftbonly, whose code repeats at 2 KiB intervals, every copy now lands on the 1 KiB alias, and up to 9 block starts share an entry. No technical debt item was opened for the ftbonly alias.

Experiment Summary

Experiment Description Status Checks Runtime Context
BP-122 Loop predictor in use; properties for the BP-121 fixes; three cleanups; five declarations reported PASS 123/123 targets; tb_loop_pred 9,408; tb_bp_cluster 1,998; 20 property and loop predictor mutations caught, H2 not shown alone 1h 59m 49s 65%
BP-126 Sixteen predictor RTL and verification items; D-1 counter direction PASS 155/155 targets; 27 reversions caught; 36 programs retire exactly 1h 57m 4s 68%
BP-127 PD-17: SC index PC term PC[9:1] PASS 155/155 targets; 6 reversions caught; 2 starting-tree runs fail 32m 0s 18%

Design Process Notes

The IA contribution

The IA used the mutation process from previous sessions [1]. After fixing and testing the RTL, it reverted the fix in a scratch copy and confirmed that the test failed.

Some fixes added new ports, and the checks for those fixes used the new ports. A check like that cannot compile against RTL with the fix reverted. For these checks, the IA changed the logic in a scratch copy of the final RTL instead of reverting the fix. A reversion is preferred because it tests the original defect. The IA reported which checks were tested this way.

Correctness in this project is checked against the planning documents. Duplicate planning content without the full context will mislead the IA. In BP-126 the IA read the copy of the TAGE counter update table in tb_tage_manual_tasks.svh, which has no legend, and reported a conflict between tage_cntrl_ctr_update_rules.md and tage_table_entry_formats.md.

The PA contribution

In these sessions the IA was given more autonomy to propose and implement provisional solutions to problems. The PA wrote the three task files and recorded each result in the planning documents. BP-126 and BP-127 were written as problems: each states what must be true at the end and leaves method and order to the IA. A defect found while working a problem is in scope and fixed in the run.

BP-121’s task asked for package changes but not for port declarations, so its new ports were reported by name only. Every task now requires each port added, removed or changed to be reported with its declaration.

The PA made three errors. It suggested sharing the loop predictor table between slots, which would create the write conflicts the per-slot banks avoid. Its first argument for PD-17 said the PC bits separate branches in a block. The SC’s PC is the block start, and the two planning documents it took the error from were corrected. While writing BP-127, it took an answer I had given about a different item as approval, opened a technical debt item, and wrote an SC fold change into six planning documents and the task. All of it was reverted. I have no firm evidence for the cause; context load is one possibility.

My contribution

After BP-121 I ruled the loop predictor’s read-before-write update, reversing my earlier ruling to leave it as specified, and accepted the possible speedpath cost.

In BP-122 I chose the restore scheme, ruled that a trusted loop prediction outranks TAGE and SC, approved lengthening lp and loops, and closed TD#165.

I set BP-126’s scope, confirmed its three provisional choices, ruled PD-17 for the prose and for the SC only, and withdrew the SC fold change written without my ruling.

The generalization

In these sessions five implementations contributed to performance loss. Every program still retired correctly, because a predictor that is idle or wrong costs cycles, not results, so retirement checks could not find them. Three were already known. Two were found by checks written from the planning documents rather than from the RTL.

  • Loop predictor: already known (TD#169), found by counting how many of its predictions were used.
  • TAGE counter direction: found by a new unit test written from the entry format document.
  • SC index: found while writing the monitor’s model from sc_table_hash_rules.md.
  • IT5 folds: already known (TD#102). The fold monitor confirmed it: the index fold read 0 where the model expected 0x100.
  • SC queue: already known as a stub (TD#123).

In this project a predictor check takes its expected value from the planning document, and a predictor that receives updates is reported with how many of its predictions were used.

What comes next

At the close of the tasks in this range every predictor is in use, every predictor RTL and verification item in the technical debt table is closed, and the regression passes all 155 targets. TD#168, the FTB’s shared jump field, was kept by ruling at the time and later closed by BP-140. The next session works through the remaining open technical debt items.

Technical Debt Referenced

The table below reports status as of the close of session 077. Later experiments outside the range have since changed the state of some items.

# Item Resolution path
7 Loop predictor speculative count rollback undefined. CLOSED BP-122. Checkpoint per FTQ entry; the redirecting block’s count is restored, other counts are marked unknown.
37 trx_type stability through tage_cntrl. CLOSED BP-126, negative result: trx_type is read only at entry.
38 Verilator 5.048 covergroup support. CLOSED BP-126: ignored with a warning, no coverage recorded. Revisit on a Verilator after the fix for issue 7099 [2].
67 TAGE sram_init non-fast path. CLOSED BP-126: sim_tage runs the walk with an init check.
68 ITTAGE sram_init non-fast path. CLOSED BP-126: sim_ittage runs the walk; sim_bp_cluster_init added.
69 TAGE fold recompute after rollback. CLOSED BP-126: fold monitor in tb_bp_cluster.
70 ITTAGE fold recompute after rollback. CLOSED BP-126: fold monitor in tb_bp_cluster.
75 No fast ITTAGE target. CLOSED BP-126: sim_ittage_fast.
81 tb_ftb coverage skews to br0. CLOSED BP-126: 35 br1 checks.
84 Fold traced end to end, producer to consumer. CLOSED BP-126: fold monitor in tb_bp_cluster, every prediction.
100 TAGE line coverage below the stated 90%. CLOSED BP-126: per-file table, RTL 588/614, every uncovered line explained.
101 ras.sv declares ras_pc_p2 and never reads it. CLOSED BP-126: removed.
102 bp_history generates no IT5 folds. CLOSED BP-126.
103 TAGE T0 resets strongly not taken. CLOSED BP-126: T0 resets to 2’b10, TAGE_T0_SRAM_INIT_VALUE.
104 Stale FTB header comments. CLOSED BP-126.
121 A full RAS commit stack reads as empty. CLOSED BP-126: occupancy count beside CSP.
123 SC update queue not built at the unit level. CLOSED BP-126: 8-entry queue in sc.sv.
165 RAS repair wrong when a slot’s p2 and p3 branch types differ. CLOSED by ruling, session 076: unreachable by construction.
168 FTB entry shares one jump field per region. KEPT by ruling at session 077 (ftb_decisions.md 4.6). Later closed by BP-140, session 081.
169 Loop predictor trained, never used. CLOSED BP-122.
170 No assertions for the BP-121 fixes. CLOSED BP-122. H2 not shown firing alone; RS1 fires in tb_ras only.

References

[1] “Branch Prediction Cluster Simulation and Mutation-Based Verification” BLOG bpu/20. Accessed 2026-10-26.

[2] Verilator issue 7099, “Support covergroup coverpoint bins with sample() and get_inst_coverage()”, https://github.com/verilator/verilator/issues/7099.


Jeff Nye is a microprocessor architect with 35 years of industry experience spanning performance modeling, RTL implementation, and architecture for high-performance OOO processors. He has contributed RTL to Pentium 4, ARM V7, TI C6x and RISC-V designs, and recently served as sole architect and full-stack implementer of the TAGE-SC-L + ITTAGE branch prediction cluster in an 8-issue RVA23 RISC-V processor - from research through timing closure at 2.75 GHz. He holds +20 issued patents in processor design, architecture, and hardware virtualization. He is the author of Pacino and the uarchlabs methodology documented here.

Connect on LinkedIn.

Updated w/ ticfinder.