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

  • How to Use This Course
  • Reading Paths
  • Concept Map
  • Meet DCRouter
  • Glossary
  • Part 0: What Verification Is8
  • Part 1: What Can Go Wrong11
  • Part 2: Specifications as Checkable Rules8
  • Part 3: Observability and Controllability7
  • Part 4: Stimulus11
  • Part 5: Checking11
  • Part 6: Coverage10
  • Part 7: Formal and Simulation10
  • Part 8: Verification Levels and Reuse8
  • Part 9: Planning and Risk9
  • Part 10: Emulation and Prototyping8
  • Part 11: Post-Silicon Validation8
  • Part 12: Bug Economics8
  • Part 13: Debug7
    • The Debug Loop
    • Reproducing Failures
    • Following the Wait
    • Bisection and Delta Debugging
    • Triage at Scale
    • Is It the Testbench?
    • Drills: Debug
  • Part 14: Sign-Off7
  • Part 15: Verification in Context8
  • Part 16: Case Studies5
  • Part 17: Capstones5
  • Part 18: Interview Preparation6
Loading lesson

‹Part 13: Debug
  1. Topics
  2. First Principles of Verification Strategy
  3. Part 13: Debug
  4. Triage at Scale

Part 13 · Debug · Senior (S)

Triage at Scale

A broken commit can fail hundreds of regression runs overnight, and nobody can debug them one by one. Failure signatures (the first error line with its numbers abstracted away) group failures by likely cause, so one representative per bucket is debugged. A runnable bucketer turns 500 failures with 490 distinct messages into 6 buckets, with the earliest failure of each as the place to start.

On this page

    Note

    Level: Senior (S) · Answers: How do we find the cause fast?

    Before this: Bisection and Delta Debugging

    Monday morning: 500 of the weekend's regression runs failed. Each log ends with a different-looking error, because each contains a different time, flit value and port. Opening logs one at a time, an engineer might reach fifty by lunch and still not know whether there are two bugs or forty.

    500 tests failed overnight. Where do you start?

    The obvious answer, and why it breaks

    The obvious approaches are to start at the top of the list or with whatever looks worst. The top of the list is arbitrary, and "worst-looking" is usually a consequence rather than a cause. Most mass failures come from a handful of causes; the job is to find the handful.

    The idea: signatures and buckets

    • Take the first error of each run: later errors are usually consequences (lesson 13.1).

    • Abstract it into a signature: replace times, values, ports and counts with placeholders, keeping the words and rule names.

    • Bucket by signature, largest bucket first.

    • Debug one representative per bucket: the earliest failure, which has the least noise before it.

    • Re-bucket after each fix: buckets that disappear were the same bug.

    Build it: bucketing 500 failures

    500 failure lines in the formats DCRouter's testbenches print, generated from a few underlying causes with random times, values and ports.

    example.py
    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
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    """Triage at scale: bucketing 500 overnight failures by signature.
    
    500 failing runs (illustrative) print first-error lines in the formats DCRouter's testbenches
    use. Underneath, there are only a few causes, in unequal numbers. The raw lines are almost all
    different because they contain times, flit values and port numbers. A signature replaces
    every number with N, every flit value with H, every bit vector with B, and keeps rule names such as R13; failures with the same signature go in one
    bucket. The model prints the buckets, largest first, with one representative each: the run
    that failed earliest, which is usually the easiest to debug.
    """
    import random
    import re
    
    rng = random.Random(5)
    templates = [
        (0.46, lambda: f"ERROR @{rng.randrange(100, 400000)}: out{rng.randrange(4)}: unexpected flit {rng.randrange(65536):04x}"),
        (0.22, lambda: f"ERROR @{rng.randrange(100, 400000)}: credit audit: {rng.randrange(1, 4)} flits left output {rng.randrange(4)} into a stalled sink, expected 4"),
        (0.15, lambda: f"ERROR @{rng.randrange(100, 400000)}: out{rng.randrange(4)}: flit {rng.randrange(65536):04x}, expected {rng.randrange(65536):04x} from input {rng.randrange(4)}"),
        (0.10, lambda: f"ERROR @{rng.randrange(100, 400000)}: R13: in_vld 1000 did not wake the router"),
        (0.05, lambda: f"ERROR @{rng.randrange(100, 400000)}: {rng.randrange(20, 200)} flits still pending after 40000 cycles (lost or stuck)"),
        (0.02, lambda: "FAIL: global timeout"),
    ]
    lines = []
    for _ in range(500):
        u, acc = rng.random(), 0.0
        for w, make in templates:
            acc += w
            if u < acc:
                lines.append(make())
                break
        else:
            lines.append(templates[-1][1]())
    
    def signature(line):
        s = re.sub(r"(flit|expected) [0-9a-f]{4}\b", r"\1 H", line)   # flit values are hex
        s = re.sub(r"in_vld [01]{4}", "in_vld B", s)                   # bit vectors
        return re.sub(r"(?<![R\d])\d+", "N", s)                          # numbers, but keep rule names like R13
    
    buckets = {}
    for line in lines:
        buckets.setdefault(signature(line), []).append(line)
    time_of = lambda l: int(re.search(r"@(\d+)", l).group(1)) if "@" in l else 10**9
    print(f"failures: {len(lines)}, distinct raw lines: {len(set(lines))}, buckets: {len(buckets)}")
    for sig, members in sorted(buckets.items(), key=lambda kv: -len(kv[1])):
        first = min(members, key=time_of)
        print(f"{len(members):4d}  {sig}")
        print(f"      first: {first}")
    assert len(buckets) <= 8 and len(set(lines)) > 400
    print(f"PASS: {len(set(lines))} different failure lines fall into {len(buckets)} signatures; debug one representative per bucket, largest first")

    It prints:

    Diagram
    failures: 500, distinct raw lines: 490, buckets: 6
     238  ERROR @N: outN: unexpected flit H
          first: ERROR @160: out2: unexpected flit 3d2f
     104  ERROR @N: credit audit: N flits left output N into a stalled sink, expected N
          first: ERROR @4859: credit audit: 2 flits left output 0 into a stalled sink, expected 4
      70  ERROR @N: outN: flit H, expected H from input N
          first: ERROR @14054: out3: flit b314, expected b8da from input 1
      49  ERROR @N: R13: in_vld B did not wake the router
          first: ERROR @931: R13: in_vld 1000 did not wake the router
      28  ERROR @N: N flits still pending after N cycles (lost or stuck)
          first: ERROR @7625: 97 flits still pending after 40000 cycles (lost or stuck)
      11  FAIL: global timeout
          first: FAIL: global timeout
    PASS: 490 different failure lines fall into 6 signatures; debug one representative per bucket, largest first

    How to read it: 490 of the 500 lines are textually different, yet there are only 6 signatures. Starting with the largest bucket (238 "unexpected flit" failures) and its earliest run at time 160 is the obvious first move. The R13 bucket names its rule outright. The 11 global timeouts carry no information in their first line, so they need the wait-following method (lesson 13.3), or they will turn out to be consequences of another bucket's bug.

    Making signatures work

    Practice

    Why

    Failure messages with stable wording and named rules

    Signatures group by cause, not by phrasing

    Abstract numbers, keep identifiers that matter

    R13 kept; times and flit values removed (port numbers can be kept or dropped by choice)

    First error only

    Cascades of consequences do not create false buckets

    Link buckets to known bugs

    Known failures do not get debugged twice

    How real teams triage at scale

    Practice

    Purpose

    Automatic signature bucketing in regression dashboards

    Mass failures summarised as a few causes

    Buckets linked to bug tickets and first-failing commits

    Owners and suspects assigned automatically

    Rerun of one representative per bucket with waves and debug enabled

    Debug data ready when the engineer arrives

    What breaks

    A team (illustrative) bucketed failures by the full error text. Every failure with a different timestamp became its own bucket, and the dashboard showed 480 "unique" failures from what turned out to be three bugs. Abstracting numbers out of the signature collapsed them to the three causes in a minute.

    Why this matters

    Senior lens. Build triage into the regression: first error only, signatures with numbers abstracted, buckets ranked by size and linked to known bugs and first-failing commits, a representative rerun with debug data. Then 500 failures are a morning's work, not a week's.

    Interview

    Asked: "500 tests failed overnight. Where do you start?"

    Weak answer: Open the first failing log and work down the list.

    Strong answer: Bucket them. Take the first error of each run, abstract it into a signature by replacing times, values and counts with placeholders while keeping words and rule names, and group by signature. Then debug one representative per bucket, the earliest failure, largest bucket first, and re-bucket after each fix, since buckets often share a cause. Uninformative signatures like timeouts get the wait-following method. In a small example, 500 failures with 490 distinct messages collapsed to 6 buckets.

    Follow-ups: (1) Why only the first error? (2) What makes a failure message good for bucketing?

    Check yourself Why does the bucketer keep "R13" but replace other numbers?

    R13 names the rule that failed, which identifies the cause; times and values differ between runs of the same bug.

    Check yourself Why debug the earliest failure in a bucket?

    It has the least activity before the failure, so its trace is shortest and least confused by consequences.

    Key takeaways

    • Mass failures usually have a few causes: find them by signature.

    • Use the first error, abstract numbers, keep rule names, bucket and rank by size.

    • Debug one representative per bucket, the earliest, and re-bucket after each fix.

    Keep going

    Next: Lesson 13.6, Is It the Testbench?. Testbench bugs versus design bugs.

    Related topics

    • Part 13 overview: Debug

    Previous

    127 / 160

    Next: Is It the Testbench?

    Up next

    Up nextIs It the Testbench?

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • The Debug Loop
    • Reproducing Failures
    • Following the Wait