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
  • How to Read the Diagrams
  • Part 0: What Is a Protocol?10
  • Part 1: Timing and Clocking11
  • Part 2: Encoding and Framing12
  • Part 3: Handshakes13
    • Valid-Only Streams
    • Wait States and Stretching
    • The Valid/Ready Handshake
    • The Valid/Ready Rules
    • Request/Acknowledge: Two-Phase and Four-Phase
    • Push and Pull
    • Global Stall or Elastic Pipeline
    • Register Slices
    • Skid Buffers
    • Bubbles and Half Throughput
    • Latency-Insensitive Design
    • Protocols Compared: Handshakes
    • Drills: Handshakes
  • Part 4: Flow Control17
  • Part 5: Transactions19
  • Part 6: Concurrency and Performance17
  • Part 7: Ordering13
  • Part 8: Sharing and Arbitration12
  • Part 9: Deadlock and Liveness13
  • Part 10: Reliability and Recovery20
  • Part 11: Bring-Up and Link Management10
  • Part 12: Power8
  • Part 13: Shared State and Coherence9
  • Part 14: Security and Isolation7
  • Part 15: Specifying and Verifying11
  • Part 16: Protocols Dissected17
  • Part 17: Capstones5
  • Part 18: Interview Preparation6
Loading lesson

‹Part 3: Handshakes
  1. Topics
  2. First Principles of Hardware Protocols
  3. Part 3: Handshakes
  4. Latency-Insensitive Design

Part 3 · Handshakes · Principal (P)

Latency-Insensitive Design

The theory behind valid/ready: systems whose results do not depend on how many cycles each hop takes, so pipeline stages can be added late without changing behaviour.

On this page

    Note

    Level: Principal (P) · Answers: Is the receiver ready?

    Before this: Skid Buffers, Bubbles and Half Throughput

    Late in a project, physical design reports that a dozen long routes between blocks need extra pipeline stages to close timing. In a design whose blocks count cycles ("the response arrives exactly four cycles after the request"), every added stage breaks a block's assumptions, and each fix needs a design change and new verification. In a design built on valid/ready everywhere, the same request is routine: add register slices, rerun timing, done.

    What property lets you add cycles of latency anywhere without changing what a system computes?

    The obvious answer, and why it breaks

    The obvious way to design blocks is to rely on known latencies: a block sends a request and samples the answer a fixed number of cycles later. It is efficient, and it couples every block to the exact timing of every path it uses. Change one path's latency, by adding a pipeline stage or moving a block, and the result arrives in the wrong cycle. The behaviour of the system depends on the latency of its wires, which is exactly the thing physical design must be free to change.

    The idea: correctness by data, not by cycle count

    A latency-insensitive system is one whose results depend only on the sequence of values each block receives, never on when they arrive. Each block waits for valid data before it acts and holds its output until it is accepted, which is the valid/ready contract of this part. Then any channel can take any number of cycles, and the system computes the same results; only the latency and, possibly, the throughput change.

    The idea has a formal basis. Carloni, McMillan and Sangiovanni-Vincentelli's theory of latency-insensitive design (2001) showed how a synchronous system can be made to tolerate arbitrary added latency on its channels. Cortadella, Kishinevsky and colleagues' work on synchronous elastic circuits (the SELF protocol) gave a simple valid/stop handshake, close to valid/ready, with proofs that networks of such elastic blocks compose: connecting correct elastic blocks gives a correct elastic system.

    The same two blocks with and without three elastic stages between them. Because both follow the handshake, B receives the same sequence either way.

    Build it: same values at any depth

    The model sends 0, 1, 2, ... through 0, 1, 3 and 6 full-throughput elastic stages into a sink that stalls on a fixed pseudo-random 30% of cycles after a short ready start. It records when the first word arrives, how long 30 words take, and checks that the sequence the sink receives is identical every time.

    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
    49
    """Latency-insensitive design: add pipeline stages, change nothing but latency.
    
    A source sends 0, 1, 2, ... through N elastic stages (one-entry valid/ready stages
    that can accept a word in the same cycle they pass one on) to a sink that is ready
    for the first 10 cycles and then on a fixed pseudo-random 70% of cycles. Whatever N is, the sink must see exactly the
    same sequence of values; only the time of the first word changes.
    """
    import random
    
    
    def run(n_stages, words=30, seed=5):
        rng = random.Random(seed)
        sink_ready = [True] * 10 + [rng.random() < 0.7 for _ in range(10_000)]
        stages = [None] * n_stages          # each holds one word or None
        nxt, out, first_at, t = 0, [], None, 0
        while len(out) < words:
            rdy_sink = sink_ready[t]
            # ready flows backwards: a stage can take a word if it is empty or emptying
            ready = [False] * (n_stages + 1)
            ready[n_stages] = rdy_sink
            for i in range(n_stages - 1, -1, -1):
                ready[i] = stages[i] is None or ready[i + 1]
            # the clock edge: words move forward wherever the next place is ready
            if n_stages == 0:
                if rdy_sink:
                    out.append(nxt); nxt += 1
            else:
                if stages[-1] is not None and rdy_sink:
                    out.append(stages[-1]); stages[-1] = None
                for i in range(n_stages - 1, 0, -1):
                    if stages[i - 1] is not None and stages[i] is None:
                        stages[i], stages[i - 1] = stages[i - 1], None
                if ready[0] and stages[0] is None:
                    stages[0] = nxt; nxt += 1
            if out and first_at is None:
                first_at = t
            t += 1
        return out, first_at, t
    
    
    print("stages  first word at cycle  cycles for 30 words  same values?")
    reference = None
    for n in (0, 1, 3, 6):
        out, first, total = run(n)
        reference = reference or out
        same = out == reference == list(range(30))
        assert same
        print(f"{n:6d}  {first:19d}  {total:19d}  {same}")
    print("PASS: every pipeline depth delivers the same values in the same order; only latency changes")

    It prints:

    Diagram
    stages  first word at cycle  cycles for 30 words  same values?
         0                    0                   43  True
         1                    1                   45  True
         3                    3                   47  True
         6                    6                   53  True
    PASS: every pipeline depth delivers the same values in the same order; only latency changes

    The first word arrives exactly one cycle later per stage, and the 30 values arrive identical and in order at every depth. The total time grows a little more than the added latency because the sink's stalls fall on different words when everything is shifted, but nothing about what is delivered changes. That is the property the late timing fix needs.

    What it costs, and what it does not guarantee

    • Latency always grows with added stages; latency insensitivity makes that safe, not free.

    • Throughput is preserved only if every stage can accept a word while passing one on (lesson 3.10), and only where there is no loop. Around a feedback loop, a channel whose result is needed before the next item can start, every added stage lowers throughput: a loop with k items in flight and n cycles of latency runs at no more than k/n items per cycle. This is the credit loop of Part 4 in another form.

    • Blocks must truly wait for data. A block that "knows" an answer comes back in four cycles and samples it then breaks the property, whatever handshake its ports have.

    The design space

    Approach

    What it buys

    What it costs

    Fixed-latency interfaces

    Minimum logic and latency

    Every timing change is a design change

    Latency-insensitive (elastic) interfaces

    Paths can be re-pipelined late without changing behaviour

    A handshake per channel; care with throughput around loops

    Elastic plus credits on long or looping paths

    Throughput preserved across long round trips

    Buffers sized to the round trip (lesson 4.5)

    How real designs use it

    Where

    How

    On-chip interfaces such as AXI

    Every channel uses VALID/READY with rules that never depend on a fixed latency, so interconnects can add register slices on any channel without changing behaviour.

    PCI Express and other links

    Nothing assumes a fixed link latency: requests are matched to completions by tags (Part 6) and flow control uses credits, so links of any length behave the same, only slower.

    What breaks

    A block on an elastic interconnect has valid/ready ports but, inside, samples a read response a fixed number of cycles after issuing the request, because "the memory always answers in six cycles" (illustrative). Months later, physical design adds two register slices on the response path. The block now reads stale data twice in every few thousand requests, when its fixed-cycle sample happens to catch an old value still sitting on the bus. The handshake ports made the block look elastic; its internal assumption made it not. The fix was to act only when the response's valid is seen, and to add a check in its testbench that randomises response latency.

    Why this matters

    Principal lens. Latency insensitivity is a design discipline you choose at the architecture stage, because retrofitting it is expensive. Ask for it on every interface that physical design might need to re-pipeline, which is nearly all of them in a large SoC. Then budget the two things it does not give for free: the latency added along critical round trips, and throughput around feedback loops, where the round trip, not the handshake, sets the rate. Verification should randomise latency on every elastic channel; a block that passes only at the nominal latency is not latency-insensitive.

    Interview

    Asked: "How do you add a pipeline stage to a path without changing the design's behaviour?"

    Weak answer: Delay all the other signals by a cycle too.

    Strong answer: Make the interfaces latency-insensitive: every block waits for valid data and holds outputs until accepted, so results depend only on the sequence of values, not on when they arrive. Then a register slice or skid buffer can be added to any channel; latency grows, behaviour doesn't. Throughput holds if stages accept while emptying, except around feedback loops, where added latency lowers the rate to items in flight over loop latency.

    Follow-ups: (1) What breaks latency insensitivity inside a block that has valid/ready ports? (2) Why do feedback loops lose throughput when stages are added?

    Check yourself In the model, why does the first word arrive one cycle later per stage?

    Each elastic stage holds a word for one cycle on its way through, so N stages add N cycles before the first word reaches the sink.

    Check yourself A loop keeps 2 items in flight and its round trip is 10 cycles. What is its best throughput, and what happens if two stages are added?

    2/10 = 0.2 items per cycle. With two more stages the round trip is 12 cycles: 2/12 ≈ 0.17. More items in flight (more buffering or credits) would restore it.

    Check yourself Why is a block with valid/ready ports not automatically latency-insensitive?

    If it internally assumes a fixed latency (for example sampling a response a set number of cycles after a request), changing a path's latency still breaks it.

    Key takeaways

    • A latency-insensitive system's results depend on the sequence of values, not on when they arrive, so stages can be added anywhere.

    • The valid/ready contract provides it if every block truly waits for data; theory (Carloni et al., SELF) shows such blocks compose.

    • Latency always grows; throughput holds on straight paths but drops around loops unless more items are kept in flight.

    Keep going

    Next: Protocols Compared: Handshakes. How ten protocols answer "is the receiver ready?", side by side.

    Related topics

    • Part 3 overview: Handshakes

    • Lesson 4.5: The Credit Loop and Buffer Sizing

    Previous

    48 / 234

    Next: Protocols Compared: Handshakes

    Up next

    Up nextProtocols Compared: Handshakes

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Valid-Only Streams
    • Wait States and Stretching
    • The Valid/Ready Handshake