Directed Validation: Proving the Test Before Trusting the Result

updated: 2026-09-09

Abstract

Four times in these sessions a task recorded a conclusion that no check supported. A status count was carried forward that no run had produced. A task placed a defect in the RTL without opening the document that would have settled it, and repairing the RTL would have broken a block that conformed. A task found a defect, argued it was harmless, and reported the work complete without testing the argument; the defect was real. A task offered a passing test as proof when the test had never been run against a design that could make it fail. Each conclusion was plausible, and each was wrong in the direction that would have cost the most.

The work those failures interrupted was unit-level verification of the TAGE and ITTAGE branch predictors of Pacino, an 8-issue RVA23 RISC-V design. Three RVA23 Co-Design sessions ran twenty-one tasks against the update rules documents, one specification row at a time: seed an entry directly into table memory, drive the update the row describes, then read the entry back out of RAM to confirm the field changed as the row requires. Most tasks found the design already conforming. Four defects were fixed, two of them the same defect appearing independently in both predictors.

The last of the four failures produced the requirement that governs the rest of this work: every check must demonstrate it can fail, whether by running against an unfixed design, by introducing a defect and reverting it, or by seeding stimulus that makes a wrong answer visible. Fifteen technical debt items closed, and both predictors are now directed-validated at the unit level.

Two things frame that result. A structural defect in the TAGE update buses had been recorded seven sessions earlier under a description that was also wrong, and correcting it opened the sequence. And the rule that every target in the Makefile runs, adopted mid-way after a count went stale, turned out to have a measurable cost of its own.

What had not been tested

A TAGE or ITTAGE table entry holds a counter (CTR), a usefulness field (USE), an epoch field (EPC), and, in ITTAGE, an indirect branch target (TGT). An update writes these fields individually; an allocation writes a whole entry. Which write happens under which condition is set out in the update rules documents, one row per condition, and those documents are the authority the RTL is checked against.

Before these sessions, the TAGE and ITTAGE update paths had been fixed where they were known to be broken and left alone where they were not. The counter and usefulness write paths had been proven by RAM readback. The epoch, target, allocation, aging and prediction-side paths had not. Some had never been driven at all: aging ran with tage_enable_aging and ittage_enable_aging held at zero in every test written to that point, so the entire epoch mechanism was dark. Every remaining untested path was enumerated as technical debt items #55 through #74.

Two rules governed how that work would be done. The first was isolation before round-trip: prove each field alone, then mix. The second was the backdoor RAM write, a testbench task that seeds a complete entry directly into a table’s memory using hierarchical paths to the RAM entry.

Both rules were inherited. These sessions added a third, that every check must demonstrate it can fail. The requirement was written after BP-050 was abandoned for arguing its test would have caught a defect without ever showing that it did.

Structural rework, and what it exposed

The redundant bus dimensioning was not found in this work. It was recorded in session-040 as TD #45, an update-index simplification, and the description was wrong: it asked for per-table ports when the per-table dimension was the defect. session-046 marked the entry wrong in CLOSED_TECH_DEBT.md and moved the real fix to TD #66. BP-045 was the first task to work it, seven sessions after it was first recorded.

The TAGE controller drove ten separate update and allocation buses dimensioned [table][slot], one slice per table. Only one table per slot is the provider or the allocation target on any update, so every update drove one meaningful slice and TAGE_NUM_TABLES-1 unused ones. BP-045 collapsed them to [slot], with the target table identified by the *_tbl_sel_u0 selects that already existed. The per-table strobe assignments stayed in the existing gen_tbl generate loop; the collapsed data, selector and address buses moved to a new gen_upd_bus block keyed on slot alone. tage_table.sv needed no change. Its input ports were already dimensioned by slot, and only the interconnect above the module had the redundant dimension.

One of the ten could not collapse. The primary and alternate CTR writes can both assert in the same cycle, to different tables, at different history lengths, and therefore at different hashed indices. A single shared index bus carries one value per slot. t_alt_upd_index_u0 was added alongside the primary update index. That gives three index buses: primary update, alternate update, and allocation. None of this was in the prompt, which assumed a single shared index. The implementation assistant derived the requirement from the CTR update rules and reported it as a deviation. Three index buses are the correct structure, and the deviation was accepted on that basis. It also exposed that ITTAGE had the same requirement and lacked the same bus. That became TD #66’s counterpart, TD #76, closed across BP-046 and BP-048.

BP-046 mirrored the change into ITTAGE and renamed twenty-two ittage_cntrl ports that cross the table boundary to the t_ convention. Renaming ports broke tb_ittage_cntrl.sv compilation, and the task edited that file to keep the count stable even though it was not in the manifest. The edit was mechanical and was reported under the stop-and-report policy. The prompt-writing rule that followed is that a task which renames a module’s ports lists every instantiating testbench in its manifest from the start.

The regression counts that BP-045 and BP-046 ran to confirm no behavioral change reported something else. sim_ittage_cntrl returned 32 passing and 44 failing. sim_ittage_table returned 30 and 2, forty-six failures in total. Neither figure had appeared in any status file. tb_ittage_cntrl.sv had been authored green at 76 checks several sessions earlier; the ITTAGE RTL had changed underneath it repeatedly since, and that suite had not been run again. The proven-by-readback claims from the intervening work were true, but they were made against directed rows in tb_ittage, not against the controller suite. PROJECT_STATUS.md carried the stale 76 throughout.

Classification is not adjudication

BP-047 ran triage and fixed nothing. It classified all 46 failures into two root-cause groups.

Of the 46, 32 were stale testbench scaffolding. Thirty of those traced to one defect in the pred_s0 task: it drove tbl_hit_p1, tbl_cntrl_bits_p1 and tbl_pred_tgt_p1 before the p0-to-p1 posedge and cleared them one time unit after it. The prv_alt_scan block in ittage_cntrl.sv is classified nba_sequent because it reads a flop output, as required. After pred_val_p1 captures, the block evaluates against the correct values, then re-evaluates against the cleared inputs and produces zeros. Every failing check in TC-PRED-02 through TC-PRED-06 returned an actual value of zero regardless of stimulus, and TC-PRED-01, which expects all zeros, passed trivially. The other two stale failures were the sim_ittage_table pair, a co-assertion requirement the test violated.

The remaining 14 were classified as an RTL counter routing inversion in ittage_cntrl.sv. The reasoning was that USE and EPC pass under the same using_primary signal, which isolates the fault to the CTR branch assignments.

That classification was diagnosed entirely against what the testbench expected. TC-UPD-01 through TC-UPD-05 asserted that using_primary=1 should fire alt_ctr_wr, and the RTL fired prm_ctr_wr. Treating the test as the reference makes the RTL wrong. Standard TAGE and ITTAGE train the provider’s counter toward the resolved outcome, which is the opposite of what the tests expected. The repair task was therefore required to load ittage_cntrl_ctr_update_rules.md and settle the direction against the rules table before changing any code, with the swap pre-authorized only if the document agreed with the test.

It did not. BP-048 read the 33-row table and found row 1 is the no-hit case with no write, rows 2 through 17 govern the UP=0 regime with alt_ctr_wr as the governing strobe, and rows 18 through 33 govern UP=1 with prm_ctr_wr. The RTL matched the document on every row. All five test cases had the strobes transposed. Only the tests were repaired; g_ctr_upd was not modified. Had the RTL swap been pre-authorized on the strength of the classification, a conforming block would have been broken and the suite would have gone green over it.

BP-048 also fixed a real RTL defect in the same task. t_alc_index_u0 was driven from the primary update index. The correct source is the allocation index field carried in the prediction metadata. No existing test could see it, because the test fixture happened to make alc_idx equal prm_idx. TC-UPD-06 was modified so the two differ. The modified test failed before the fix and passed after, and the defect was corrected against ittage_cntrl_alloc_rules.md.

One item did not resolve. On TC-UPD-05 the task found two authority documents in conflict: rows 22 through 29 of the CTR rules table imply the primary strobe fires on a null counter under misprediction with the data saturating at zero, while ittage_cntrl_decisions.md stated there is no counter write in that case. The task chose the rules table, called the decisions note subordinate implementation guidance, and updated the test to match the RTL. Test and RTL then agreed, and the suite was green. Agreement between two artifacts is not evidence when the authority governing both is the thing in dispute. The resolution was a specification change rather than a code change: ittage_cntrl_decisions.md gained a “Concurrent CTR and TGT Writes” section relaxing the mutual-exclusivity claim, on the grounds that writing zero to a counter already at zero has no effect.

A defect reported and argued away

TD #57 covered the ITTAGE target write path, which had never been audited. BP-049 verified that the target is written when the provider’s counter is zero on misprediction and added four directed tests covering both provider regimes. The suite went from 81 to 105 checks with no failures. It marked itself complete and reported TD #57 closed.

In its own notes it recorded that the per-table target write enable in ittage_table.sv gated on (prm_match | alt_match), so a target write reached every hitting table. Only the provider should have been written. It classified this as existing behavior not prohibited by the specification, and stated that the incidental update had no observable effect on correctness.

Both statements were wrong. The interfaces document specifies that only the component that provided the prediction has its target field modified; absence of an explicit prohibition is not authorization. The effect is also observable. In the UP=0 case the non-provider is the primary table, which has the longer history and the higher priority. Its target field is overwritten with a value resolved under a different history length. On the next prediction at that address the primary hits and is selected as provider ahead of the alternate. The corrupted entry is the one consulted first.

The tests could not have caught it. Each read back only the entry it expected to change. A test that verifies a write landed says nothing about whether a second write landed somewhere it should not have. The same blind spot had produced the alc_index defect one task earlier.

BP-049a ran with ittage_interfaces.md in the manifest, where the provider-only invariant is stated explicitly, and the defect was fixed. The single t_tgt_wr_u0 strobe was split into t_prm_tgt_wr_u0, asserted only for UP=1 with the primary counter at zero, and t_alt_tgt_wr_u0, asserted only for UP=0 with the alternate counter at zero. The table’s target write enable became prm_tgt_wr & prm_match | alt_tgt_wr & alt_match, mutually exclusive by construction, with the non-provider gating to zero in both regimes. The tests were extended to read back the non-provider entry and assert it unchanged. Both extended checks failed before the fix and passed after, with the failing values showing the exact corruption: expected c000, actual e000, and expected c000, actual b000.

An earlier attempt in the same task passed using_primary into the table and gated there. It was backed out after Verilator scheduling on partial-bit generate assignments produced incorrect results. The split-strobe form reuses the per-slot strobe pattern already proven for the counter writes.

Showing that a check can fail

BP-050 took up the ITTAGE epoch write. It found the write enable provider-only, added tests that read back both the provider and the non-provider entry, and passed. It was abandoned.

The gate was correct and the finding was right, but the task never demonstrated that its non-provider check could fail. It argued that a (prm_match | alt_match) defect would make the check trip. It did not run the test against a defective design to find out. A test that has only ever been run against conforming RTL is indistinguishable from a test that cannot fail, and BP-049 had just shown what that costs.

BP-050b redid the work with the missing step. The epoch write enable was established as provider-only by construction: it rides the mutually exclusive primary and alternate counter strobes, and the epoch write condition is a strict subset of the counter write condition, so no path writes the epoch field without also writing the counter. No RTL change was required. Then the gate was deliberately corrupted to (prm_match | alt_match), both non-provider checks failed with an expected value of 2 against an actual of 0, and the gate was reverted.

Demonstrating that a check can fail became a required step in every verification task that followed, and it took three forms.

Where a defect was fixed, the test ran against the unfixed design first. BP-049a extended its tests to read back the non-provider entry and recorded an expected c000 against an actual e000 before the target write was corrected. BP-051 added the missing guard on use_alt_on_na (UAON, use alternate on newly allocated, a saturating counter with a threshold of 8), removed it again to watch TC-UAON-08 return an expected 8 against an actual 9, and restored it.

Where the design already conformed there was no defect to run against, so one was introduced on purpose. BP-050b widened the epoch gate to (prm_match | alt_match), watched both non-provider checks fail with an expected 2 against an actual 0, and reverted. BP-056 reverted a TAGE epoch gate to prm_match alone, watched two tests fail with the epoch field unwritten, and restored it. BP-057 removed the guard it had just added and watched the counter move. Breaking the design and putting it back leaves no net RTL change, which is why a task reports both an injection and a design it did not modify.

Where neither applied, the discrimination came from the stimulus. BP-052 seeded a pre-state of USE=2’b10 so an aging-disabled result could not be mistaken for an age-1 result. BP-058 seeded USE=2’b10 in TC-87 for the same reason. BP-060 seeded the provider counter at 3’b111 and again at the weak-band boundary 3’b100, so pred_strong had to change between the two for the test to pass.

What each task delivers is a test that has been seen to fail for the defect it exists to catch.

BP-050 produced one more finding. At session start tb_ittage_cntrl.sv and tb_ittage_table.sv did not compile at all. BP-049a had renamed t_tgt_wr_u0 across ittage_cntrl.sv and ittage_table.sv and had run only sim_ittage. The 77 and 32 passing counts recorded as current truth in the session handoff were false from the moment of the rename. BP-050a enumerated all twenty targets, repaired both testbenches with no RTL change, and the escape was recorded as BUG-002. BP-050’s own claim to have repaired them did not hold either, which is how the same class of error appeared twice one task apart.

The rule that came out of this is that every simulation and lint target in the Makefile runs, whether or not it is a dependency of all, and that status counts come from a run in the current session. Its cost is measurable. BP-054a ran for three and a half minutes and consumed 72% of context, almost entirely from twenty targets’ console output. Compaction occurred in all seven tasks of the final session. A phased application of the rule, so that early tasks in a sequence run fewer targets, is the planned revision.

Completing ITTAGE

With the epoch write proven, the remaining ITTAGE items ran in dependency order. Aging reads the epoch field, so the epoch write is proven first.

BP-051 found the second real RTL defect. The UAON update block was missing its single-hit guard: with either component count at zero the counter should hold, and without the guard a single-hit transaction with a stale alternate target defaulting to zero could move it on a comparison carrying no training signal. The guard was added and proven by removal, TC-UAON-08 failing with an expected 8 against an actual 9. The fix invalidated a test elsewhere. tc_tgt_b_ext in tb_ittage had encoded its UAON decrement step around the missing guard. Its single-hit scenario produced a decrement only because the guard was absent. It was rewritten as a two-hit setup.

BP-052 proved the aging and epoch path, dark until this point. All seven rules conformed. The task found that the epoch advances one tick after the interval reaches zero, and wrote its tests to match the RTL while noting the document did not specify the boundary. That is the same resolution BP-047 made toward the testbench, resolving toward the artifact in front of it, and here it was left standing with no documented rule behind the test. The correction came six tasks later at BP-058, the TAGE aging task, which was told not to assume the timing and proved it explicitly at the boundary posedge. The N+1 rule was then written into tage_cntrl_use_update_rules.md. The ITTAGE document still carries the gap.

BP-053 proved the thirteen allocation rules with no RTL change and found a specification error. The write-data field order in the allocation rules document did not match the structural entry layout, with the target and epoch fields transposed. Rather than correct one document, the field order was extracted into ittage_table_entry_formats.md and tage_table_entry_formats.md as the single source, referenced from the interface documents. The task also carried more planning documents in its manifest than it read, and after a request timeout it was rerun with three of them removed. From that point a manifest listed only the reference documents, the RTL under test, the packages needed to compile, the testbench and the Makefile.

BP-054 proved the twelve prediction-path rules and found two further specification errors. Both were carried across from TAGE. ittage_pred_strong was documented as the provider counter differing from 3 and from 4, which is the TAGE weak-band definition for a direction counter. ITTAGE’s counter is a confidence counter and the correct condition is non-zero. Separately, the final-target section referred to a single stored ittage_pred_tgt field. It was rewritten so the consumer selects between the primary and alternate targets using ittage_using_primary, which avoids storing a third 35- to 40-bit target. BP-054a confirmed the corrected document against the RTL and testbench with no code change.

BP-055 ran the ITTAGE round-trip capstone across counter, usefulness, allocation, epoch and target writes in one flow. Its first failure was again in the test, not the RTL. IT3 was seeded with an epoch value of 3 against a local epoch of 0. That gives an age of 1 and an effective usefulness of USE>>1, which is zero. The usefulness increment then wrote 1 over a seed of 1 and produced no visible change. Three expectations were corrected. The rule recorded from it is that when a usefulness delta must be observable, the epoch field is seeded equal to the local epoch so the effective and raw values agree.

TAGE, the same sequence

The ITTAGE sequence became the template for TAGE, and the outcomes mirrored it closely enough to be useful as a comparison.

BP-056 proved the TAGE epoch write. The gate had been changed in an earlier hand fix to prm_match | alt_match and never proven. The local epoch was forced to 2'b10 against a seeded epoch of 2'b00 so the write landed a visible change; a same-value write would have proved nothing. Injection reverted the gate to prm_match alone and the two alternate-match tests returned with the epoch field unwritten. The TAGE epoch write rides the usefulness write enable, not the counter subset relation that ITTAGE uses; the two predictors reach the same invariant by different structures.

BP-057 found the fourth defect, and it is the ITTAGE defect from BP-051 in the other predictor. The uaon_upd_ff gate was missing && u_alt_tagged[s]. When the provider hit a tagged table and the alternate fell through to the untagged base table T0, the counter moved on a comparison with no tag behind it. The two-line guard was added and recorded as BUG-003; TC-81 failed before and passed after. The TAGE manifestation is narrower. The ITTAGE defect needed only a single hit; the TAGE one needs a tagged provider with an untagged alternate.

BP-058 proved the aging path with no RTL change. It confirmed age = (lcl_epoch - EPC) mod 4, and an effective usefulness of the raw value at age 0, a right shift at age 1, and zero at age 2 or greater. One test seeds usefulness at 2'b11 so the four ages produce 11, 01, 00 and 00, which distinguishes a wrong age from a right one. Another proves the behavioral consequence instead of the counter value. A fresh entry is not an allocation candidate; the same entry aged to an effective usefulness of zero is. Unlike the ITTAGE interval, the TAGE aging interval is a 32-bit input port, not a static parameter, so the boundary is always reachable and there is no risk of a mechanism untestable at current parameters.

BP-059 proved allocation and folded RAM-level write isolation into the same task, which the capstone would otherwise have had to carry. The task already ran at sim_tage with the tables present. The selected table is read back changed and a non-selected table seeded to a distinct value is read back unchanged. This is the check the ITTAGE allocation task could not reach, having run controller-only. The write-data field order was cross-checked against the entry-format document and matched, so the transposition found in ITTAGE has no TAGE counterpart.

The task also corrected the prompt rather than the design. The requirement asked for a no-consecutive-skip test, a description carried over from the ITTAGE allocation work. The TAGE guard is alc_comp_p1[s] == 0, which stops the scan at the first candidate. A no-consecutive-skip guard would carry on past a selected table. The two policies differ only when two allocatable candidates are adjacent, which the seeded stimulus did not produce, so the test proves first-match selection and the label was corrected to match.

BP-060 proved the prediction path with no RTL change. The pred_strong definition was the specific item under watch, given the carryover error found in the ITTAGE document. The TAGE document states the provider counter differs from 3 and from 4, the RTL implements (ctr != 3'b011) && (ctr != 3'b100), and they agree. The carryover ran one direction only.

BP-061 came last, the TAGE round-trip capstone. Entries were placed to collide at a single RAM address, with T1 through T4 all mapping to index 512, which is the arrangement most likely to expose a cross-path write. Aging was disabled and the epoch seeded equal to the local epoch so usefulness deltas stayed visible. That is the BP-055 lesson applied directly. The interference checks confirm that a counter and epoch write survive a usefulness write to a different table, and that an allocation disturbs neither the provider entry nor a fourth-table reference held constant through all four steps. One field is not discriminating: with the epoch equal to the local epoch throughout, the epoch writes are zero over zero, so a stuck value would pass. Epoch landing is proven separately with a real delta and an injection, and the capstone’s purpose is interference.

Counts as evidence

Four times a status count was carried forward that no run had produced.

The controller suite carried 76 passing through several sessions of RTL churn without being re-run. BP-049a’s rename left two testbenches uncompilable while their previous counts were recorded as current. BP-050 reported an all-targets pass of 123, 77 and 32 that BP-050a could not reproduce, because the repair it claimed had not held. And the TAGE arithmetic did not close across three consecutive tasks: one task reported 69, the next added five test cases and reported 73, which resolves only if the prior baseline was 68; the task after that reported a pass with no integer at all, which does not satisfy a rule requiring a count from the current session. The ledger closed once an integer was produced again, at which point the whole chain of 73, 81, 87, 95, 102 and 103 reconciled end to end.

The pattern is the same as the one the injection step addresses. A count and a passing test are both assertions about a system’s state, and both are worth exactly as much as the run that produced them.

Experiment summary

Experiment Description Status Checks Runtime Context
BP-045 TAGE update/alloc buses collapsed to per-slot form, third index bus added (#66) PASS 68/68 17m 54s 82%
BP-046 ITTAGE alternate index bus added, table-facing ports renamed to t_ (#76) PASS counts held 32/44, 30/2, 81/0 17m 15s 83%
BP-047 ITTAGE escape triage, 46 failures classified into two root-cause groups TRIAGE ONLY 46 classified, 0 fixed 11m 52s 60%
BP-048 CTR direction adjudicated against rules table: test transposed, RTL conforming; alc_index defect fixed PASS cntrl 77/0, table 32/0 11m 9s 58%
BP-049 ITTAGE target write verification (#57): non-provider write reported and rationalized, marked complete in error PARTIAL 105/0 16m 55s 82%
BP-049a Target write strobe split, provider-only invariant proven by non-provider readback (#57) PASS 113/0 1h 1m 34s 60% + compaction
BP-050 ITTAGE EPC write, first attempt: discriminating power argued but never demonstrated ABANDONED – 17m 3s 20% + compaction
BP-050a All Makefile targets enumerated and run; BP-049a port-rename escape found and repaired (BUG-002) PASS 20/20 targets 8m 54s 57%
BP-050b ITTAGE EPC write proof with defect injection and revert (#56) PASS sim_ittage 125/0 24m 20s 47%
BP-051 ITTAGE UAON single-hit guard: real RTL defect found and fixed (#59) PASS cntrl 92/0, ittage 125/0 35m 7s 53%
BP-052 ITTAGE aging / epoch path, previously dark (#61) PASS cntrl 112/0 22m 34s 30%
BP-053 ITTAGE allocation policy and gating; entry-format documents created (#63) PASS cntrl 147/0 30m 45s 57% + compaction
BP-054 ITTAGE prediction-side correctness; two document errors corrected (#65) PASS sim_ittage 164/0 28m 7s 57%
BP-054a Corrected decisions document verified against RTL and testbench (#65) PASS 20/20 targets 3m 32s 72%
BP-055 ITTAGE round-trip capstone across CTR, USE, alloc, EPC, TGT (#72) PASS 211/0, 20 targets 1h 12m 54s 58% + compaction
BP-056 TAGE EPC write proof with defect injection and revert (#55) PASS sim_tage 73/0 46m 6s 24% + compaction
BP-057 TAGE UAON single-hit guard: real RTL defect found and fixed, BUG-003 (#58) PASS sim_tage 81/0 36m 33s 66% + compaction
BP-058 TAGE aging / epoch path, previously dark (#60) PASS sim_tage 87/0 26m 41s 48% + compaction
BP-059 TAGE allocation with RAM-level write isolation folded in (#62) PASS sim_tage 95/0 29m 28s 51% + compaction
BP-060 TAGE prediction-side correctness (#64) PASS sim_tage 102/0 20m 45s 25% + compaction
BP-061 TAGE round-trip capstone across CTR, USE, alloc, EPC (#71) PASS sim_tage 103/0 29m 5s 50% + compaction

Status is the outcome of the task as specified, not a test result. BP-046 is a PASS that reports 44 failures: it completed its structural change and confirmed the counts were unchanged by it. Those failures were pre-existing and, until that run, unreported.

What comes next

Both predictors are now directed-validated at the unit level. The remaining items are deferred, not unresolved. They are the sram_init non-fast path for both units; rollback and history recompute, which move to cluster integration; the dual-slot configuration; the ITTAGE counter width reduction from three bits to two; and a missing fast-init simulation target. The counter width reduction will churn any counter and aging tests written before it lands, which argues for doing it before any further ITTAGE test work.

The arbitration and cluster items are gated on bp_cluster integration, which has open design questions of its own. The next predictor units (FTB, SC and RAS) are not started. The directed-validation sequence used here is the template for them.

Technical debt referenced

The table below reports status as of the close of session-049, the last session covered by this post. Sessions after 049 have changed the state of some items carried alongside these.

# Item Resolution path
55 tage EPC write proof. Changed RTL, never proven by readback. CLOSED BP-056. epc_we gate changed as a USE rider in earlier work. Seed entry, drive EPC-writing update, read EPC back via prediction, confirm landing.
56 ittage EPC write proof. Changed RTL, never proven by readback. CLOSED BP-050b. epc_we_s0/s1 fixed alongside use_we with no positive test. Readback-verify per provider, UP=1 and UP=0.
57 ittage TGT target replacement. Untested. Successor to #51. CLOSED BP-049a. Suspect for the same provider-gating defect as CTR and USE. Target written on mispredict when CTR null only. Trace path, readback-verify reachable rows. ITTAGE only; TAGE has no target field.
58 tage UAON trigger rules. Tested only as setup, never as DUT. CLOSED BP-057. tage_cntrl_uaon_update_rules.md promoted from Draft to authority, directed test per row, use_alt_on_na asserted and cleared per rule. BUG-003 found and fixed.
59 ittage UAON trigger rules. Tested only as setup, never as DUT. CLOSED BP-051. ittage_cntrl_uaon_update_rules.md promoted from Draft. USE tests had relied on UAON asserting as a precondition without it ever being verified.
60 tage aging / epoch path. Entire path dark. CLOSED BP-058. Supersedes #41. All prior tests ran with tage_enable_aging=0. Drive aging enabled, exercise EPC-versus-epoch compare and USE decrement over the interval.
61 ittage aging / epoch path. Entire path dark. CLOSED BP-052. Same as #60 for ITTAGE. Consumes the EPC field whose write changed in earlier work; see #56.
62 tage allocation policy + write gating. Never the feature under test. Successor to #51. CLOSED BP-059. Allocation had been treated as residue to invalidate, never verified. Which table allocates, write-enable gating, allocation index. RAM-level write isolation verified.
63 ittage allocation policy + gating. Never the feature under test. Successor to #51. CLOSED BP-053. Same as #62. Allocation on mispredict, CTR-null condition, alloc_we gating, allocated entry state read back.
64 tage prediction-side correctness. Not directed-tested. CLOSED BP-060. Prediction path had been exercised only as setup for update tests. Directed-test provider selection, using_primary, pred_strong and the target mux against seeded entries.
65 ittage prediction-side correctness. Not directed-tested. CLOSED BP-054 and BP-054a. Same as #64 for ITTAGE. Resolves the #42 test aspect: provider, using_primary and target operate at s2, not s3.
66 TAGE structural rework. Per-table 2D update and allocation buses where shared per-slot buses are correct. Sequence before TAGE allocation #62. CLOSED BP-045. Collapsed to shared per-slot buses routed by the existing selects; t_alt_upd_index_u0 added for the concurrent primary and alternate CTR write case.
71 tage round-trip. Combined test, run only after individual tests pass. CLOSED BP-061. Mixed CTR, USE, allocation and EPC in one flow. Run only after #55, #58, #60, #62 and #64 were each proven alone; mixing before isolation reproduces multi-cause ambiguity.
72 ittage round-trip capstone. Combined test, run only after individual tests pass. CLOSED BP-055. Mixed CTR, USE, allocation, EPC and TGT in one flow, after #56, #57, #59, #61, #63 and #65. Allocation RAM-level write isolation verified.
76 ittage should have independent index buses for primary and alternate table updates. CLOSED across BP-046 and BP-048. History lengths differ and indices are hashed per length, so primary and alternate updates require separate buses. The t_alc_index_u0 source defect was the remainder, closed in BP-048.

Design process notes

What the implementation assistant contributed

The implementation assistant produced every directed test in these sessions, approximately 120 test cases across the two predictors, each citing the specification row it exercises. It root-caused four RTL defects to file and line and fixed them in the same session, and it produced the diagnostic reasoning behind the largest failure group in BP-047, the nba_sequent re-evaluation against cleared inputs, which required tracing Verilator’s block-classification behaviour, not just re-running a failing comparison.

Three of the specification errors were found by the implementation assistant reporting a discrepancy it was not asked to look for: the allocation write-data field order, the pred_strong carryover, and the stored final-target field. In BP-045 it went further and contradicted the task: it derived the three-index bus requirement from the CTR update rules and reported the prompt’s single-index assumption as wrong. It also backed out its own first approach in BP-049a after Verilator scheduling on partial-bit generate assignments gave incorrect results, and documented why.

It failed the same way every time. Where the task allowed judgment about what a finding meant, it resolved toward the artifact in front of it: BP-047 treated the testbench as the reference for the counter direction, BP-049 treated the absence of an explicit prohibition as authorization, BP-050 argued a check’s discriminating power instead of demonstrating it, and BP-052 wrote tests to match RTL behavior the document did not specify. In each case the reasoning is defensible in isolation and wrong against the specification. The pattern is not carelessness in execution; it is the absence of the authority document at the moment the judgment was made.

What the planning assistant contributed

The planning assistant scoped and sequenced all twenty-one tasks, including the dependency order that put every epoch write proof before its corresponding aging proof, and the isolation-before-round-trip ordering that held both capstones until every path was proven alone.

Its most consequential contribution was the guard on BP-048. Presented with a classification that placed the swapped strobes in the RTL, it identified that the diagnosis rested entirely on the testbench being correct, and required the repair task to settle the direction against the rules table before changing code, with the swap pre-authorized only on the document’s agreement. The document disagreed and a conforming block was left alone.

It also caught the argued-not-demonstrated gap in BP-050 and the rationalized defect in BP-049, and identified the count arithmetic failures across BP-055 through BP-058.

Its own errors were in scope and in labelling. Manifests were oversized on several tasks, once contributing to a timeout, and the manifest omission on BP-049, which left the interfaces document out, is the direct cause of the rationalized target-write defect. The BP-059 requirement carried a no-consecutive-skip label across from the ITTAGE allocation work when the TAGE policy is stop-at-first, which is the same cross-track contamination that appeared in the specification documents, this time in a prompt.

What the architect contributed

Every expected value originated with the architect, as did every specification correction. The rejection of BP-049’s completion claim, the decision to extract the entry field order into standalone format documents and not patch one rules table, the ruling that the N+1 epoch timing belonged in the TAGE aging section and not only in a test, and the judgment that the epoch wrap needed no documentation change were all architect decisions.

The all-targets rule and its planned phased revision are also architect decisions, made after measuring the rule’s context cost, not before.

What changed in the method

Six rules came out of these sessions. Each was written against a specific failure, and none of them was in place when that failure happened.

Rule Written after
Every check demonstrates it can fail, by running against an unfixed design, by injecting a defect and reverting it, or by seeding stimulus that makes a wrong answer visible. BP-050, which argued its check would catch a defect and never ran it against one.
Every simulation and lint target in the Makefile runs, whether or not it is a dependency of all, and every status count comes from a run in the current session. BP-049a’s port rename left two testbenches uncompilable while their previous counts stood as current truth (BUG-002).
A task adjudicates against the authority document before it changes code. A change is pre-authorized only if the document agrees. BP-047 placed the counter swap in the RTL on the strength of the tests alone; the document showed the RTL conforming on all 33 rows.
A task that renames a module’s ports lists every instantiating testbench in its manifest from the start. BP-046 broke tb_ittage_cntrl.sv and had to edit a file that was not in its manifest.
A manifest carries only what the task will read: the reference documents, the RTL under test, the packages needed to compile, the testbench and the Makefile. BP-053 timed out carrying more planning documents than it opened.
A value used by two documents is extracted into one format document that both reference, rather than corrected in the one where it was found. BP-053 found the allocation write-data field order transposed against the structural entry layout.

The all-targets rule is the one with a measured cost against it, and a phased revision is planned. The others cost nothing to keep.

The generalization

Sixteen of the twenty-one tasks were verification work, and thirteen of those ended with no RTL change. The design conformed to its specification nearly everywhere it was checked, which means the output is almost entirely tests, and their passing does not establish what they are worth.

A test written against conforming RTL and never run against anything else has an unknown detection capability. It may encode the requirement, or it may encode whatever the design does. The two are indistinguishable from the outside, and both produce the same green result. Separating them is cheap. Break the design in the way the test claims to detect, confirm the test fails, restore. Where there is nothing to break, choose stimulus that makes a wrong answer visible. The values recorded here are what make the passing results mean anything. An expected 2 returned 0 when the epoch gate was widened. An expected 8 returned 9 when the UAON guard was removed. An expected c000 returned e000 against the uncorrected target write path. Each took minutes.

The other three failures are not about tests at all, and the same principle covers them. A count, a rationalization and a classification are each a claim that no run has settled.

The two predictors provide the one controlled comparison available. The same UAON defect exists in both, found independently by the same directed method and not by transferring the fix. Where a document error crossed from one predictor to the other, the directed test on the receiving side found it, and the deliberate check of the same item on the originating side came back clean. A method that finds the same class of defect twice in two independent implementations, and that distinguishes a genuine carryover from a suspected one, is doing something a single pass cannot demonstrate.


References

No references required for this post.


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 more than 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.