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 Path
  • Cache Coherency Glossary
  • The Shared Course Model
  • Cache Coherency Revision Sheet
  • Why Coherence Exists8
  • States and Protocols12
  • Transactions12
  • Races and Controller Design8
  • Progress and the Fabric16
  • Ordering and Synchronization12
  • Performance and Scalability12
  • Real Protocols12
  • SoC Integration12
  • Power, Clocking and Reliability12
  • Verification12
    • Coherency Scoreboards and Reference Models
    • Formal Properties for Coherency Safety
      • SWMR as an Assertion
      • Safety Versus Liveness
      • Assumptions and Vacuity
      • Small Models and Parameters
    • Coverage Closure and Waiver Governance
  • Debug and Bring-up12
  • Interview and Design Review17
  • Capstone: A Coherent Model Lab5
Loading lesson

‹Formal Properties for Coherency Safety
  1. Topics
  2. Cache Coherency
  3. Verification
  4. Formal Properties for Coherency Safety
  5. Assumptions and Vacuity

Part 11 · Verification · All levels

Assumptions and Vacuity

Constrain the environment to legal behaviour without hiding bugs, recognise vacuous passes, and detect both with cover properties and witness traces.

On this page

    The proof passed. What did it actually prove?

    A formal proof says: on every trace the environment assumptions allow, the assertions hold. Both halves matter. Assumptions describe what the rest of the system may do: the caches, the network, the cores. Too few, and the tool reports impossible inputs as bugs. Too many, and the proof covers a smaller world than the real one. A proof under a wrong assumption is a proof about a different system.

    Legal assumptions for a directory controller

    Assumption on the environment

    Why it is legal

    What it must not become

    A cache sends at most one request for X at a time (one MSHR per line)

    The real cache controller guarantees it

    'No two caches request X at the same time'

    GetM comes only from I or S, PutM only from M

    Legal message sequences of the protocol

    'GetM never arrives while a sharer exists'

    An Inv-Ack follows only an Inv to that cache

    Responses answer requests

    'The Inv-Ack arrives within 2 cycles'

    Finite outstanding requests: at most k in the network

    Real buffers are finite, and it keeps the model finite

    A bound smaller than any real race needs

    Messages are eventually delivered (A1)

    Needed for liveness only

    'Delivered within N cycles' in a safety proof, which removes long-delay interleavings

    The right-hand column is the danger: each entry is a plausible simplification that removes exactly the interleavings in which coherence bugs live. Assumptions on the environment should ideally be proved as assertions on the real neighbouring block (assume-guarantee): the cache's 'one request per line' assumed at H is asserted at the cache.

    Worked case: an assumption that empties the proof

    Use the early-grant bug that Small Models and Parameters finds: H sends M permission at the same time as the Inv, instead of after the Inv-Ack. A formal engineer, tired of counterexamples that start with a shared line, adds an assumption:

    design.sv
    1
    2
    3
    4
    // bad-example
    // Over-constrained: "a GetM never arrives while another cache holds S".
    asm_no_shared_getm: assume property (@(posedge clk) disable iff (!rst_n)
      getm_valid |-> (sharers & ~(1 << getm_src)) == '0);

    Under it, SWMR proves. But the bug needs exactly that situation: C0 = S / 0, C1 sends GetM. The assumption deleted every trace in which an Inv is ever sent, so the Inv path, the ack counter and the early grant are never exercised. Any assertion about them is now vacuous: its antecedent (an Inv is in flight, an ack is pending) is never true, so 'antecedent implies consequent' holds trivially.

    Detecting it: covers and witnesses

    A cover property asks the tool for a trace in which a condition happens. If the cover is unreachable, every assertion whose antecedent depends on it is vacuous. Write covers for the scenarios the proof is meant to include, before trusting the proof:

    design.sv
    1
    2
    3
    4
    5
    6
    // snippet
    c_getm_with_sharer: cover property (@(posedge clk) disable iff (!rst_n)
      getm_valid && (sharers & ~(1 << getm_src)) != '0);
    c_inv_sent:   cover property (@(posedge clk) disable iff (!rst_n) inv_valid);
    c_ack_counts: cover property (@(posedge clk) disable iff (!rst_n)
      ack_pending ##1 !ack_pending);

    Many tools also report vacuity per assertion and produce a witness trace: a trace in which the antecedent fires and the consequent is checked. Read at least one witness for each important assertion; it is the evidence that the property was exercised. The program below repeats the worked case on an atomic sketch of the course model: with the assumption, SWMR holds and the 'an Inv is sent' cover is unreachable; without it the bug appears; the correct design passes with the cover reached.

    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
    # Atomic sketch of the course model: C0, C1 in I, S or M, Inv messages in flight,
    # and H's grant rule. Fwd-GetS and Fwd-GetM are collapsed into one step.
    def moves(s, early_grant, over_constrained):
        st, inv, want = s
        for i in (0, 1):
            j = 1 - i
            if st[i] == "I" and want is None and not inv:           # load miss
                new = list(st); new[i] = "S"
                new[j] = "S" if st[j] == "M" else st[j]
                yield (tuple(new), inv, None)
            if st[i] != "M" and want is None and not inv:           # store needing M
                if over_constrained and st[j] == "S":               # ASSUMPTION
                    continue
                new = list(st)
                sharers = frozenset({j}) if st[j] == "S" else frozenset()
                new[j] = "I" if st[j] == "M" else st[j]
                if early_grant or not sharers:
                    new[i] = "M"                                     # grant now
                    yield (tuple(new), sharers, None)
                else:
                    yield (tuple(new), sharers, i)                   # wait for Inv-Ack
        for j in inv:                                                # Inv delivered
            new = list(st); new[j] = "I"
            rest = inv - {j}
            if want is not None and not rest:
                new[want] = "M"                                      # all acks: grant
            yield (tuple(new), rest, None if not rest else want)
    def explore(early_grant, over_constrained):
        init = (("I", "I"), frozenset(), None)
        seen, todo = {init}, [init]
        while todo:
            for t in moves(todo.pop(), early_grant, over_constrained):
                if t not in seen:
                    seen.add(t); todo.append(t)
        swmr = all("M" not in st or sorted(st) == ["I", "M"] for st, _, _ in seen)
        cover_inv = any(inv for _, inv, _ in seen)   # witness: an Inv is ever sent
        return swmr, cover_inv
    assert explore(early_grant=True, over_constrained=True) == (True, False)   # "proof" passes, cover unreachable
    assert explore(early_grant=True, over_constrained=False) == (False, True)  # bug found once assumption removed
    assert explore(early_grant=False, over_constrained=False) == (True, True)  # correct design, non-vacuous
    print("PASS: over-constrained assumption hides the early-grant bug, the Inv cover exposes it")
    Decision flowchart: a proved assertion is accepted only if its covers are reached, a witness exercises the consequent and a seeded bug is caught; otherwise look for a blocking assumption or a weak property.
    Triage of a passing proof: covers, then witnesses, then a seeded bug, before the result counts.

    Common pitfall

    Adding an assumption to silence a counterexample without first deciding whether the counterexample is legal. If the real environment can produce that trace, the assumption hides a bug. Record each assumption with the reason it is true of the real system.

    Common pitfall

    Leaving assumptions from a bring-up session in the sign-off run. A temporary 'only C0 issues requests' makes every race unreachable. Review the assumption list as carefully as the assertion list.

    Interview

    When asked how you trust a formal pass: list the assumptions and their justification, show covers for the key scenarios (GetM with sharers, Inv sent, late Inv-Ack), mention vacuity reports and witness traces, and show a seeded bug being caught.

    Check yourself Your SWMR proof passes, and the cover c_inv_sent is unreachable. What do you conclude, and what do you do?

    The proof never saw an upgrade with a sharer, so it says nothing about the Inv and ack path where early-grant bugs live. Look for the assumption (or a reset or tie-off) that blocks it, remove or weaken it, rerun the cover until it is reached, then rerun the proof.

    Related topics

    • Shared course model

    • Verification course: Assumptions and Over-Constraint

    • Verification course: What Formal Does

    Previous

    128 / 167

    Next: Small Models and Parameters

    Up next

    Up nextSmall Models and Parameters

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • SWMR as an Assertion