Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Part 0 · What Debugging Is6
  • Part 1 · Reproduce8
    • Determinism and Seeds
    • Recording Everything to Replay
    • Shrinking a Failing Test
    • Reducing the Design
    • Making a Flaky Test Fail
    • When Simulation Cannot Reproduce It
    • Timeouts and Watchdogs in Benches
    • The Reproduction Checklist
  • Part 2 · Observe9
  • Part 3 · Localise10
  • Part 4 · Hypothesise and Test6
  • Part 5 · Bug Classes16
  • Part 6 · Debug by Stage9
  • Part 7 · Hangs and Deadlocks7
  • Part 8 · Intermittent Failures7
  • Part 9 · Fix, Prove, Prevent7
  • Part 10 · Case Studies12
  • Part 11 · Debug as a Team5
  • Part 12 · Interview Prep5
Loading lesson

‹Part 1 · Reproduce
  1. Topics
  2. The Art of Hardware Debug
  3. Part 1 · Reproduce
  4. Making a Flaky Test Fail

Part 1 · Reproduce · Senior (S)

Making a Flaky Test Fail

Turn "flaky" into a measured failure rate with a real seed sweep, find the stressor behind it, and raise the rate on purpose: dbglab's random test goes from 10% to 88%, and a small same-cycle race shows why more stimulus is not more stress.

On this page

    Why this matters

    A test marked flaky gets rerun until it passes, and the bug behind it ships. Measuring the failure rate and then raising it on purpose turns a nightly lottery into a repro you can run before lunch.

    After this lesson you can: check whether a test is really flaky, measure its failure rate over a seed range by signature, predict the rate from a mechanism, and raise it by biasing the stimulus that causes it.

    Note

    Level: Senior (S) · Debug-loop step: Reproduce

    Before this: Determinism and Seeds, The Debug Loop

    Red on Tuesday, green on Wednesday

    dbglab's nightly regression runs the random test on fresh seeds every night. About one night in three, one seed goes red with a RAM word that reads xxxxxxxx. The ticket has been reopened four times. (The history is illustrative; every run below is real.) Each time someone reran the seed, saw it fail, then ran the next night's seeds, saw green, and closed it as flaky, cannot reproduce.

    Your lead wants the bug fixed or the test removed. Before either, a sharper question: is this test flaky at all, and if it fails one time in N, how do you make it fail every time you ask?

    What people usually do, and why it fails

    The usual moves are to rerun until green, add an automatic retry, or make the test longer "to stress it". All three treat the failure as noise, and a retry hides it for good. Rerunning a different seed is not rerunning the test: a new seed is new stimulus.

    The other common move is to sweep a few dozen seeds, see two failures, and write "fails about 5%". That number is too rough to prove a fix with later.

    The method: from flaky to a rate to a repro

    1. Check it is flaky. Rerun the same seed, commit and command. If it fails the same way every time, it is not flaky; it is a rare path that some seeds take. That is the usual case, and the good one (determinism and seeds).

    2. Name the likely reason for 1 in N. A rare path in the random stimulus; two events that only sometimes land in the same cycle; a clock ratio that only sometimes lines up edges; state that is never initialised and only sometimes read; or the environment (simulator, options, machine).

    3. Measure the rate by signature. Sweep a fixed range of seeds, group the FAIL lines by message, and count only your bug's signature. Write the rate with its range.

    4. Find the stressor. Ask what every failing seed does that passing seeds do not. Turn the answer into a predicted rate and check it against the measured one.

    5. Raise the rate by biasing the stressor. Change the stimulus weights so the mechanism happens more often. Check that the signature did not change.

    6. Keep the stressed setting as a named test. It is your repro now, and later your proof.

    The picture: one rate, ten samples

    dbglab's random test with only bug B4 present: failures in each block of 100 seeds. The true rate is near 10%; single blocks say anything from 5% to 16%.

    Every bar is the same test with the same bug. Seeds 301 to 400 alone would have said 5%; seeds 201 to 300, 16%. Sweep wide before you write a number down.

    Do it: measure, predict and raise B4's rate

    Is it flaky? Rerun the same seed

    Seed 17 fails at cycle 7949 every time, in the same way; the determinism lesson shows the runs. So the test is not flaky. Some seeds take a path that fails, and the question is which path.

    Measure by signature, and watch for masking

    Terminal
    $ for s in $(seq 1 400); do tb/run.sh random $s | grep FAIL; done > sweep400.txt
    $ wc -l < sweep400.txt
         135
    $ grep -c xxxxxxxx sweep400.txt
    41

    135 failures, but only 41 have this bug's signature: an unknown value. The other 94 are a different bug (B3, the beat counter that wraps, from the debug loop), and they mask this one: a seed that hits B3 first stops before it can show B4. To count B4 cleanly, apply B3's known one-line fix in a scratch copy and sweep wider:

    Terminal
    $ sed -n 36p rtl/dma_engine.sv                  # scratch copy with B3 fixed
      localparam int CW = 10;           // beat counter width
    $ for s in $(seq 1 1000); do tb/run.sh random $s | grep FAIL; done > sweep1000.txt
    $ wc -l < sweep1000.txt
         100
    $ grep -vc xxxxxxxx sweep1000.txt
    0
    $ head -3 sweep1000.txt
    FAIL test=random seed=16 cycle=686: SCOREBOARD: RAM word 759 (address 00001bdc) read xxxxxxxx, expected 00000000 (last written by DMA run #3)
    FAIL test=random seed=17 cycle=7949: SCOREBOARD: RAM word 816 (address 00001cc0) read xxxxxxxx, expected 00000000 (last written by DMA run #4)
    FAIL test=random seed=19 cycle=415: SCOREBOARD: RAM word 984 (address 00001f60) read xxxxxxxx, expected 00000000 (last written by DMA run #3)

    Read it: 100 failures in 1,000 seeds, every one with an X. The rate is 10.0%, and a 95% interval for 100 in 1,000 is 8.3% to 12.0%. Notice the other thing every line shares: the expected value is always 00000000, and the last writer is always a DMA run. That is the clue to the stressor.

    Find the stressor and predict the rate

    Expected zero, written by the DMA, means a DMA fill that writes zeros. The random test's DMA chooser shows how often that happens:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // snippet: tb/tests.svh, r_start_dma (lines 186-203)
      k = urange(0, 99);
      ...
      else if (k < 25) begin                                             // zero a buffer
        r_fill = 1'b1;
        if (m_fill != 32'h0) csr_wr(CSR_FILL, 32'h0);
        mode = 8'h1 << CTRL_FILL;
      end
      else if (k < 33) begin                                             // pattern fill
        r_fill = 1'b1;
        csr_wr(CSR_FILL, rnd());
        mode = 8'h1 << CTRL_FILL;
      end

    A zero fill is 1 DMA run in 100, a pattern fill 8 in 100. A zero fill writes FILL only if the bench's model says FILL is not already zero, so the first zero fill of a run trusts FILL's reset value. A pattern fill writes FILL first. Hypothesis: the failure needs a zero fill before any pattern fill, and FILL does not really reset to zero. That predicts a rate: of the fills in a run, the first is a zero fill with probability 1 in 9, about 11%. The measured 10.0% agrees. (Why FILL is X is the bug itself; finding it is a later part's job.)

    The wrong knob: run longer

    The intuitive way to stress a random test is to make it longer. The mechanism says that cannot work: only the first fill matters, so more operations add nothing. Check the prediction on seeds 1 to 200:

    Setting (seeds 1 to 200)

    Fail

    Rate

    Time

    default, 1,500 operations

    21

    10.5%

    48 s

    +OPS=6000

    21

    10.5%

    2 min 44 s

    +OPS=300

    23

    11.5%

    17 s

    Four times the operations, three and a half times the wall time, the same rate. A fifth of the operations, the same rate too. The cost moved; the information did not.

    The right knob: bias the stressor

    In a scratch copy of the bench, widen the zero-fill band from 1 in 100 to 8 in 100, leaving pattern fills at 1 in 100. Prediction: the first fill is a zero fill 8 times in 9, about 89%.

    Terminal
    $ sed -n 194p tb/tests.svh                      # scratch copy: zero fills 8 in 100, pattern fills 1 in 100
      else if (k < 32) begin                                             // zero a buffer
    $ for s in $(seq 1 200); do tb/run.sh random $s | grep FAIL; done > biased200.txt
    $ wc -l < biased200.txt
         177
    $ grep -vc xxxxxxxx biased200.txt
    0
    $ head -2 biased200.txt
    FAIL test=random seed=1 cycle=3478: SCOREBOARD: RAM word 169 (address 000012a4) read xxxxxxxx, expected 00000000 (last written by DMA run #17)
    FAIL test=random seed=2 cycle=1230: SCOREBOARD: RAM word 817 (address 00001cc4) read xxxxxxxx, expected 00000000 (last written by DMA run #2)

    177 of 200, 88.5%, against a prediction of 89%. The signature is unchanged: X, expected zero, written by a DMA fill. Seed 1, which passed before, now fails at cycle 3478. The close match between predicted and measured rates is evidence that you understand the mechanism, before you have opened a waveform.

    A smaller case: two events in the same cycle

    Many intermittents are collisions: two events that are fine alone and wrong in the same cycle. In this counter of outstanding requests, when issue and retire arrive together the else if order drops the retire. The bench draws the gaps between requests from the seed, so only some seeds collide.

    outstanding_ctr.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // outstanding_ctr counts requests issued but not yet retired.
    // The bug: when issue and retire land in the same cycle, the if/else order drops the retire.
    module outstanding_ctr (
      input  logic       clk, rst_n, issue, retire,
      output logic [3:0] count
    );
      always_ff @(posedge clk or negedge rst_n)
        if (!rst_n)      count <= '0;
        else if (issue)  count <= count + 4'd1;
        else if (retire) count <= count - 4'd1;    // never reached when issue is also high
    endmodule
    
    // The fix, checked on the same stimulus: both events count.
    module outstanding_ctr_fixed (
      input  logic       clk, rst_n, issue, retire,
      output logic [3:0] count
    );
      always_ff @(posedge clk or negedge rst_n)
        if (!rst_n) count <= '0;
        else        count <= count + 4'(issue) - 4'(retire);
    endmodule
    

    First the view a nightly regression would give you: forty seeds.

    Terminal
    $ iverilog -g2012 -o f.vvp flaky_tb.sv
    $ vvp -n f.vvp +SWEEP=40
      seed 33 fails: issue and retire together at cycle 45
    SWEEP: 1 of 40 seeds fail with gaps of 1 to 40 cycles
    $ vvp -n f.vvp +SEED=33
    FAIL seed=33 gap=40: count=1 after every request retired (issue and retire together at cycle 45)
    $ vvp -n f.vvp
    gaps 1-40: 82 of 1000 seeds fail; gaps 1-4: 560 of 1000
    PASS: the lost retire is seed-dependent, and short gaps raise its rate

    One failure in forty looks like 2.5%; the 1,000-seed rate is 8.2%. Seed 33 fails the same way every time, so again this is a rare path, not a flaky test. The stressor is a collision, so the knob is the spacing of events. The obvious stress, packing requests as close as possible, is not the best one:

    Largest gap (+GAP)

    1

    2

    4

    8

    16

    40

    Seeds failing out of 1,000

    363

    522

    560

    427

    213

    82

    With one-cycle gaps all four issues fall within four cycles, so only the retires with the shortest latencies can land on one; collisions get rarer again. The rate peaks when gaps are near the retire latency (2 to 9 cycles). Stress the mechanism, not the volume. Verilator 5.048 prints the same lines, since the bench draws its own numbers.

    What the evidence says

    Both bugs are deterministic per seed, so neither test is flaky in the strict sense. Each has a measured rate, a named stressor, and a predicted rate that matches. The biased dbglab bench and +GAP=4 are now the repros: they fail in seconds with the same signature, and you will run them again to prove the fix. Reproduce is done; Observe can start from a seed that fails at cycle 1230 instead of one in ten nights.

    Traps

    Common pitfall

    A bias that changes the bug. Push a weight too far and you may hit a different failure: a new message, a new signature, or a hang. Check every failing line of the stressed sweep against the original signature, as grep -vc xxxxxxxx did above, before you trust the higher rate.

    Common pitfall

    "Fixed" because a short sweep passed. At 10%, a sweep of 20 seeds misses the bug about one time in eight. Prove a fix on the stressed repro, where the bug fails almost every seed, and then on the wide sweep. How many clean runs are enough is a calculation, covered in Part 8.

    Common pitfall

    Some tests really are flaky. If the same seed sometimes passes, look for an input you are not recording: a file left by a previous run, a parallel job, a different tool version (recording everything to replay). Fix that before any design bug.

    Why this matters

    Senior lens. Ask for every random knob in the bench to be a plusarg with a name, such as a zero-fill weight or a maximum gap, and keep a few stressed settings in the regression next to the default. The default finds bugs you did not expect; the stressed settings keep the ones you already found from coming back quietly.

    Track failure rates per signature, not pass rates per test: "random: 90% pass" hides two bugs.

    Interview

    Asked: "A test fails about once in fifty runs. The team wants to add a retry. What do you do?"

    Weak answer: I would run it a few hundred more times to see if it is real, and if it keeps failing rarely I would add the retry and open a low-priority bug.

    Strong answer: First I check whether the same seed fails every time; usually it does, so it is a rare path, not a flaky test. Then I measure the rate over a few hundred seeds, grouped by failure signature, and look at what the failing seeds share. That gives a hypothesis about the stressor and a predicted rate. I bias the stimulus toward that stressor until it fails most seeds with the same signature, and that stressed test becomes the repro and later the proof of the fix. A retry would hide a real bug, so I would only accept one for a proven infrastructure problem.

    Follow-ups: (1) How would you know your biasing has not created a different bug? (2) How many clean runs do you need before you believe the fix?

    Check yourself A colleague raises dbglab's random test to +OPS=20000 overnight to "stress" B4 and is surprised the rate does not move. Explain it, and say what they should change instead.

    B4 needs a zero fill before any pattern fill, so only the first fill of a run matters. Longer runs add more fills after the first one and leave that probability at about 1 in 9. The real runs agree: 21 failures in 200 seeds at the default length and at +OPS=6000. Change the weights that decide the first fill: more zero fills, fewer pattern fills, which took the rate to 88.5%.

    Check yourself With flaky_tb, someone sets +GAP=1, the tightest spacing possible, and gets a lower rate than +GAP=4. Is the stress broken?

    No, the mechanism explains it. The bug needs an issue in the same cycle as a retire, and a retire comes 2 to 9 cycles after its issue. With gaps of one cycle, the four issues are packed into four cycles, so only a retire of latency 2 or 3 can meet one: 363 failures in 1,000. Gaps near the latency give the most overlap: 560 at +GAP=4. Stress what the mechanism needs, which is not always "more" or "faster".

    Remember

    • A different seed is a different test. A test is flaky only if the same seed, commit and command sometimes pass.

    • Measure the rate over hundreds of seeds and count by signature: 100 seeds can say 5% or 16% for the same 10% bug.

    • Raise the rate by biasing the stressor the mechanism needs. Running longer or wider rarely helps.

    Rule of thumb: never call a test flaky until you have a failure rate, a signature and a guess at what the failing seeds share.

    How this was checked

    dbglab at HEAD 49c5f04 (rebuilt with dbglab/build-history.sh), Icarus Verilog 13.0, 2026-10-17. for s in $(seq 1 400); do tb/run.sh random $s | grep FAIL; done: 135 FAIL lines, 41 with xxxxxxxx. In a copy with B3's one-line fix (CW = 10 in rtl/dma_engine.sv), seeds 1 to 1000: 100 FAIL lines, all with xxxxxxxx (89 SCOREBOARD, 11 STREAM); per block of 100 seeds 11, 10, 16, 5, 13, 10, 10, 9, 6, 10; 4 min 3 s.

    Same copy, seeds 1 to 200: 21 fail by default (48 s), 21 with +OPS=6000 (2 min 44 s), 23 with +OPS=300 (17 s). A further copy with the zero-fill band of r_start_dma widened (k < 25 to k < 32 at tb/tests.svh:194): 177 of 200 fail, all with xxxxxxxx, 21 s. Wilson 95% intervals: 100/1000 is 8.3% to 12.0%.

    flaky_tb as shown, iverilog -g2012 and vvp -n f.vvp with no plusargs, +SWEEP=40, +SWEEP=100, +SEED=33 and +SWEEP=1000 +GAP=<g> for g = 1, 2, 4, 8, 16, 40; verilator --binary --timing --assert -Wno-fatal --top-module flaky_tb printed the same lines.

    Limits: the dbglab rates hold for this bench's random weights and these seeds; a bench change moves them. The bias was made in a scratch copy, not in dbglab. A rate from a sweep is an estimate with an interval, not a constant.

    Keep going

    • Practice: Coverage closure using constrained random

    • Code lab: Randomize with

    • Code lab: Enable counter

    • Interview: run a mock interview on this topic

    Related topics

    • Part 1 · Reproduce

    • Shrinking a failing test

    • The reproduction checklist

    • Part 8: intermittent failures

    • RTL: debugging with waveforms

    Previous

    11 / 107

    Next: When Simulation Cannot Reproduce It

    Up next

    Up nextWhen Simulation Cannot Reproduce It

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Determinism and Seeds
    • Recording Everything to Replay
    • Shrinking a Failing Test