Search Tech Journey

Find topics, journeys and posts

back to blog
mladvanced 120m read

DL S069 · Speculative Decoding — 2–3× Free Speedup

The trick where a tiny draft model proposes k tokens and the big model verifies them in one forward pass. Provably lossless, 2–3× decode speedup, and now standard in every serious inference stack.

🧠SoftwareM11 · Efficient inference + serving· Session 069 of 130 120 min

🎯 Understand speculative decoding well enough to implement it in ~80 lines, and to predict when it will help vs hurt.

Series: Deep Learning & LLMs From Scratch — 80 sessions · Session 69 / 80 · Module M11 · ~2 hours

The story

Here's the puzzle. Decode is memory-bound (S066): you read 14 GB of weights to produce one 2-byte token. Ridiculous. If only you could produce more tokens per memory read.

You can. The trick, from Leviathan et al. (Google, 2022) and simultaneously Chen et al. (DeepMind, 2023): use a small draft model (say Llama-3.2-1B) to generate the next kk tokens greedily/sampled. Then run the big target model (Llama-3-70B) once on all kk tokens in parallel — a single forward pass — and accept as many drafted tokens as agree with what the target would have produced. On acceptance, you got kk tokens for one big-model forward pass. On rejection at position jj, you keep the first j1j-1 tokens and take the target's token at jj (always at least one token per iteration, so it's never worse than plain decode).

The kicker: this is provably lossless. Under proper rejection sampling, the output distribution is exactly the target's. You are not trading quality for speed. You are trading "some wasted small-model compute" for "much less wasted big-model memory bandwidth". And on real workloads it's 2–3× faster.

Every serious inference stack — vLLM, TensorRT-LLM, TGI, MLC, llama.cpp — ships speculative decoding now. Understanding it changes how you think about all of decode.

The TA who guesses, the professor who verifies
🌍 Real world
💻 Code world

The 2022 idea in one image

Imagine a professor grading exam papers. Each paper takes 60 seconds. There are 100 papers. That's 100 minutes. Now imagine a TA who guesses each paper's grade in 3 seconds — mostly right, occasionally wrong. Professor's new workflow: TA scribbles guesses on all 100 papers (5 minutes). Professor reads them, agrees on 80%, corrects the other 20 in one sitting. Total: 5 minutes + one glance per paper (much less than 60s each because the answer is already there to verify) ≈ 20 minutes. 5× speedup, same grades.

That's speculative decoding. The TA is the draft model. The professor is the target model. The "verify all at once" trick works because reading 100 papers in a row is not 100× slower than reading one — the professor's eyes are already open. Same for a GPU: one forward pass on a 5-token sequence is not 5× slower than one forward pass on a 1-token sequence. It's memory-bound; you pay the weight-read cost once either way. The extra 4 tokens are essentially free compute.

Leviathan's original paper reported 2–3× speedups on T5. In 2024–2025, with tree-based drafts (Medusa, EAGLE), the same idea hits 3–5×. It might be the single highest-ROI inference trick invented in the last five years.

You will be able to
  • Explain the speculative decoding loop in 4 steps: draft, target-forward, rejection-sample, accept/repair.
  • Prove (informally) why the accept/reject rule preserves the target distribution.
  • Implement greedy and sampled speculative decoding in ~80 lines of PyTorch.
  • Predict acceptance rate for a given (draft, target) pair from vocab overlap intuition.
  • Compare vanilla speculative vs Medusa vs EAGLE and pick the right one.
  • Diagnose why speculative decoding sometimes *slows* generation and fix it.

Prerequisites

  • Session 066 — why decode is memory-bound (the whole reason speculative works).
  • Session 053 — softmax, temperature, top-p.
  • Session 068 — distillation is one way to get a good draft model.


1 · The loop, in words

Target model MtM_t (big, slow). Draft model MdM_d (small, fast). Speculation length kk (typically 4–8).

Repeat until EOS:

  1. Draft: starting from the current context, autoregressively sample kk tokens from MdM_d: x~1,,x~k\tilde{x}_1, \ldots, \tilde{x}_k, recording draft probabilities qi()q_i(\cdot) at each step.
  2. Target forward pass (parallel): feed the context + x~1,,x~k\tilde{x}_1, \ldots, \tilde{x}_k into MtM_t as one sequence. This is one forward pass, same cost as producing a single token in vanilla decode (memory-bound: you read weights once). Extract target probabilities pi()p_i(\cdot) at positions 1..k+11..k+1.
  3. Rejection-sample sequentially for i=1ki = 1 \ldots k:
    • Accept x~i\tilde{x}_i with probability min(1,pi(x~i)qi(x~i))\min\left(1, \frac{p_i(\tilde{x}_i)}{q_i(\tilde{x}_i)}\right).
    • On acceptance: continue to i+1i+1.
    • On rejection at ii: sample a replacement from the corrected residual pi(x)max(0,pi(x)qi(x))p'_i(x) \propto \max(0, p_i(x) - q_i(x)), break.
  4. Free token: if all kk tokens were accepted, sample one more from pk+1()p_{k+1}(\cdot) — that's already there in the target's parallel pass output.

At the end of one iteration: you've produced between 1 and k+1k+1 new tokens for one big-model forward pass. Amortized, expected tokens per big pass is 1+iαi1 + \sum_i \alpha_i where αi\alpha_i is per-position acceptance probability.


2 · Why it's lossless

The claim: tokens accepted by this scheme are distributed exactly as if you'd sampled from pip_i directly.

Sketch: consider a single position. Probability of emitting token xx under the scheme is:

  • (draft produces xx) × (accept it) + (draft produces something else, gets rejected) × (residual sample lands on xx)
  • =q(x)min(1,p(x)/q(x))+yxq(y)(1min(1,p(y)/q(y)))max(0,p(x)q(x))zmax(0,p(z)q(z))= q(x) \cdot \min(1, p(x)/q(x)) + \sum_{y \ne x} q(y) \cdot (1 - \min(1, p(y)/q(y))) \cdot \frac{\max(0, p(x) - q(x))}{\sum_z \max(0, p(z) - q(z))}

Grind through — comes out to exactly p(x)p(x). The residual construction is the entire trick: it exactly cancels the bias introduced by the draft. Same principle as rejection sampling in classical MCMC.


3 · Expected speedup

Let α\alpha be the per-token acceptance probability (assume constant for intuition). Expected tokens per iteration: E[tokens]=n=0kαn=1αk+11αE[\text{tokens}] = \sum_{n=0}^{k} \alpha^n = \frac{1 - \alpha^{k+1}}{1 - \alpha}

For α=0.7,k=5\alpha = 0.7, k = 5: E3.03E \approx 3.03 tokens per big-model pass. So ~3× throughput on decode, minus overhead of the draft passes and the extra tokens in the target's forward.

Overhead calibration: target parallel forward of k+1k+1 tokens vs 1 token. On memory-bound decode, this is nearly free — you're re-reading the same weights either way. On prefill (compute-bound), the extra tokens cost real FLOPs; that's why speculative decoding is a decode optimization, not a prefill one.

Sweet spot

    4 · Code: a working speculative decoder

    The chunk everyone gets wrong is the sequential rejection sampling. Let's write it explicitly.

    import torch
    import torch.nn.functional as F
     
    @torch.no_grad()
    def draft(model, input_ids, k, temperature=1.0):
        """Autoregressive sample k tokens from `model`. Returns tokens (1, k) and probs list."""
        tokens, probs = [], []
        ctx = input_ids
        for _ in range(k):
            logits = model(ctx).logits[:, -1, :]  # (1, V)
            p = F.softmax(logits / temperature, dim=-1)
            tok = torch.multinomial(p, num_samples=1)  # (1, 1)
            tokens.append(tok)
            probs.append(p)
            ctx = torch.cat([ctx, tok], dim=1)
        return torch.cat(tokens, dim=1), probs  # (1, k), list of (1, V)
    Try itMeasure acceptance rate for a real draft/target pair on your own prompts.

    Install transformers>=4.36. Load meta-llama/Llama-3.2-1B as the assistant and meta-llama/Llama-3-8B-Instruct as the target. Generate 200 tokens on a factual prompt ("Explain photosynthesis in 3 sentences") and a creative one ("Write a haiku about compilers"). Log assistant_kwargs={"num_assistant_tokens": 5}, time both runs, and check outputs.past_key_values length vs base decode. You'll usually see 2–3× on the factual prompt and 1.2–1.5× on the creative one — that gap is the α\alpha term.

    💡 Hint · Use transformers `assisted_generation` with `assistant_model=` and count how many tokens agree.

    Now the target verification + accept/reject:

    @torch.no_grad()
    def spec_step(target, draft_model, input_ids, k=5, temperature=1.0):
        # 1) Draft k tokens
        drafted, q_list = draft(draft_model, input_ids, k, temperature)
        # 2) One target forward on context + drafted
        full = torch.cat([input_ids, drafted], dim=1)         # (1, T + k)
        logits = target(full).logits                          # (1, T + k, V)
        # Positions of interest: predictions for slots T..T+k inclusive
        T = input_ids.shape[1]
        p_all = F.softmax(logits[:, T - 1 : T + k, :] / temperature, dim=-1)  # (1, k+1, V)
     
        accepted = []
        for i in range(k):
            p_i = p_all[0, i]              # target prob at position i
            q_i = q_list[i][0]             # draft prob at position i
            x = drafted[0, i].item()
            r = torch.rand(1, device=input_ids.device).item()
            ratio = (p_i[x] / q_i[x]).clamp(max=1.0).item()
            if r < ratio:
                accepted.append(x)
            else:
                # rejection: sample from corrected residual
                residual = torch.clamp(p_i - q_i, min=0.0)
                residual = residual / residual.sum()
                new_x = torch.multinomial(residual, 1).item()
                accepted.append(new_x)
                return torch.tensor([accepted], device=input_ids.device)  # (1, i+1)
        # All k accepted: bonus token from target's k-th slot prediction
        bonus = torch.multinomial(p_all[0, k], 1).item()
        accepted.append(bonus)
        return torch.tensor([accepted], device=input_ids.device)  # (1, k+1)

    Driver:

    @torch.no_grad()
    def spec_generate(target, draft_model, prompt_ids, max_new=200, k=5):
        ids = prompt_ids
        while ids.shape[1] < prompt_ids.shape[1] + max_new:
            new = spec_step(target, draft_model, ids, k=k)
            ids = torch.cat([ids, new], dim=1)
            if (new == tokenizer.eos_token_id).any():
                break
        return ids

    Real implementations swap target(full).logits for a KV-cached version — the target only needs to process the newly appended tokens because its cache already covers input_ids. Same for the draft. In this simple version we recompute for clarity; in production this is critical for the speedup to materialize.


    5 · Draft model choice

    Options, in order of setup complexity:

    • Same-family smaller model. Llama-3-70B target ↔ Llama-3.2-1B draft. Same tokenizer (essential), similar distribution → α0.7\alpha \approx 0.7.
    • Distilled draft (S068). Train a 1B student on the target's outputs. α0.8\alpha \approx 0.8.
    • N-gram / lookup draft (Prompt Lookup Decoding, Saxena 2023). Just grep the prompt for repeated substrings. Zero extra params, α0.3\alpha \approx 0.3 but no compute cost. Surprisingly good for code, RAG, structured output.
    • Self-speculation (Medusa, Eagle). Add "prediction heads" to the target itself that predict multiple future tokens in one pass. No separate model, higher α\alpha, need to train the heads.

    Medusa

    Cai et al. 2024: attach KK extra decoder heads to the target (h1,,hKh_1, \ldots, h_K), each predicting the token kk steps ahead. Fine-tune only these heads (base frozen) on a small dataset. At inference, sample from each head to get candidates, verify with a tree-attention pass. ~2.5× speedup, no separate draft model.

    EAGLE / EAGLE-2 / EAGLE-3

    Li et al. 2024. Sharper draft model that predicts the feature vector (pre-LM-head hidden state) at the next position, then applies the frozen LM head. More faithful to the target than a naive small model. State of the art in 2024: 3× on Llama-3-70B.

    EAGLE-2 (2024) added a dynamic draft tree: instead of a fixed-depth candidate tree, expand deeper along high-confidence branches. Reported 4× speedup on Vicuna-33B.

    EAGLE-3 (Li et al., March 2025) added "training-time test" — during draft-head training, they simulate the drift that happens when the draft's own output feeds back as input, and train against that distribution. Result: acceptance rate stays high as tree depth grows. Independent benchmarks (agentnative.dev 500-prompt suite, 2026) put EAGLE-3 at 2.89× median speedup on Llama-3-70B vs Medusa-2 at 2.21× vs a standard 1B draft at 1.95× vs prompt-lookup at 1.38×.

    2025 default: EAGLE-3

    As of mid-2025, vLLM v1 ships EAGLE-3 as the default speculator for English + code workloads. Medusa remains competitive when you can't afford the EAGLE draft training run (EAGLE-3 draft heads take ~8 H100-hours per target model). The industry consensus is: use EAGLE-3 if you can, prompt-lookup if you can't train, and turn spec off entirely if acceptance is below ~0.5.

    Further reading — the speculative frontier:


    6 · When speculative decoding hurts

    War story Batching kills the speedup

    At batch size 16+, the target's forward pass is no longer memory-bound — it's compute-bound. Now the extra kk tokens in the target pass cost you real FLOPs, and speculative decoding slows you down. vLLM disables speculation above a batch threshold. Rule of thumb: speculative wins when batch < ~8 on a modern GPU.

    War story Domain shift kills acceptance

    Your draft was trained on general web text; your target is being used for Python code generation. α\alpha drops to 0.3. Now you're doing extra work for nothing. Fix: distill/fine-tune the draft on your target domain, or switch to prompt-lookup for code (repetitive substrings help).

    War story Tokenizer mismatch is silent death

    Llama draft, Mistral target — tokenizers differ. Token IDs mean different things. The scheme still runs and produces gibberish that's biased toward one vocab. Always assert draft.config.vocab_size == target.config.vocab_size and use the same tokenizer object.

    War story Greedy target doesn't need the residual trick

    If temperature=0 (greedy), the accept rule collapses to "accept if x~i==argmax  pi\tilde{x}_i == \text{argmax}\; p_i, else take argmax". No sampling. Simpler and slightly faster path. HuggingFace assisted_generation special-cases this.


    7 · Mermaid: one iteration


    8 · How to actually run this today

    Easiest path — HuggingFace assisted_generation:

    from transformers import AutoModelForCausalLM, AutoTokenizer
     
    tok = AutoTokenizer.from_pretrained("meta-llama/Llama-3-70B-Instruct")
    target = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-3-70B-Instruct", torch_dtype="auto", device_map="auto")
    draft = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-3.2-1B-Instruct", torch_dtype="auto", device_map="auto")
     
    inputs = tok("Explain speculative decoding briefly.", return_tensors="pt").to(target.device)
    out = target.generate(**inputs, assistant_model=draft, max_new_tokens=200)
    print(tok.decode(out[0], skip_special_tokens=True))

    Prompt-lookup (zero-cost draft):

    out = target.generate(**inputs, prompt_lookup_num_tokens=10, max_new_tokens=200)

    vLLM: --speculative_model meta-llama/Llama-3.2-1B-Instruct --num_speculative_tokens 5.

    TensorRT-LLM has both draft-target and Medusa/EAGLE built in.


    Recall

    1. Why is speculative decoding lossless? The acceptance rule min(1,p/q)\min(1, p/q) plus the residual-sampling on rejection max(0,pq)\propto \max(0, p - q) together reproduce exactly the target's distribution pp. Same math as rejection sampling in MCMC.

    2. Expected tokens per big-model pass for α=0.8,k=6\alpha=0.8, k=6? (10.87)/(10.8)=(10.21)/0.23.95(1 - 0.8^7)/(1 - 0.8) = (1 - 0.21)/0.2 \approx 3.95 tokens per iteration.

    3. Why does speculative decoding fail at large batch sizes? The target's k+1k+1-token parallel forward stops being memory-bound and starts costing real FLOPs. The extra work no longer amortizes over reused weight reads. Disable speculation above ~batch 8.

    4. Difference between draft-target and Medusa? Draft-target uses a separate smaller model. Medusa attaches K extra decoder heads to the target itself that predict K future tokens in parallel; no separate model.

    5. What breaks silently if the draft and target have different tokenizers? Token IDs mean different things in the two vocabularies. Accept/reject still runs but on nonsense correspondences, producing garbled output biased toward the target's vocab. Always assert same tokenizer.

    Stretch: implement prompt-lookup decoding from scratch: given a prompt, find its longest suffix that appears earlier in the prompt, and use the following few tokens as the draft. Measure acceptance rate on a code-generation task vs a chat task.

    In your own words: speculative decoding turns __________________________ into __________________________.

    Spaced review: S053 sampling strategies, S066 KV cache (memory-bound decode is the whole reason this works).

    Next session (S070): continuous batching — how servers pack multiple requests of different lengths into one GPU without wasting cycles waiting for the slowest.

    Bring back tomorrow:

    • The 4-step spec-decoding loop.
    • Why it's lossless (rejection sampling identity).
    • When it stops helping (batch > 8, tokenizer mismatch, domain drift).
    Common misconception
    ✗ What most people think

    "Speculative decoding trades quality for speed. The small draft model is guessing, so the output must be a bit worse than what the big model would have produced — that's the price of the speedup."

    ✓ What is actually true

    Correctly implemented speculative decoding is exactly distribution-preserving. The verification step accepts a drafted token with probability min(1, p_target/p_draft) and, on rejection, resamples from a specific residual distribution constructed so the overall output distribution is identical to sampling from the target model alone. It is not an approximation with a small error; it is an algebraic identity. You get the target model's exact distribution, faster.

    Why the myth is so sticky

    This one is seductive because every other inference speedup you have met genuinely is a quality trade: quantisation, pruning, distillation, cache eviction, smaller models. You have correctly learned that free lunches do not exist, and you generalise. The escape is that speculation is not buying compute reduction at all — it is buying parallelism. The target model still evaluates every token it accepts; it just evaluates several positions in one forward pass instead of one per pass, exploiting the fact that decode leaves the tensor cores idle. Nothing was approximated because nothing was skipped. The rule "faster means worse" is true for lossy compression and false for better hardware utilisation, and the misconception is failing to notice which one you are looking at.

    Prove it to yourself

    Check the identity empirically rather than trusting the claim:

    import torch
    p = torch.tensor([0.1, 0.6, 0.3])   # target distribution
    q = torch.tensor([0.5, 0.2, 0.3])   # draft distribution
    counts = torch.zeros(3)
    for _ in range(200000):
        i = torch.multinomial(q, 1).item()
        if torch.rand(1).item() < min(1.0, (p[i]/q[i]).item()):
            counts[i] += 1
        else:
            resid = (p - q).clamp(min=0); resid /= resid.sum()
            counts[torch.multinomial(resid, 1).item()] += 1
    print(counts/counts.sum())   # -> approaches p, not q

    The empirical frequencies converge to p regardless of how bad q is. A worse draft costs throughput, never correctness.

    From first principles
    Start with the question

    Why can the target model verify k drafted tokens in a single forward pass, when generating those same k tokens autoregressively would require k passes? Verification looks like it should be just as sequential.

    1. 1
      Autoregressive generation is sequential because token t+1 is an input to the computation of token t+2 — you cannot start the second until the first exists.
      forced by · the dependency is on the sampled token value, which is not known until the forward pass completes
    2. 2
      But once a candidate sequence exists, the tokens are all known. Feeding them together with a causal mask computes, in one pass, the target's next-token distribution at every prefix simultaneously.
      forced by · causal masking means position i's output depends only on positions ≤ i, which are all present in the input — so the pass is a batched evaluation of k independent conditionals
    3. 3
      That pass reads the model weights exactly once, the same as a single-token decode step, because weight bytes do not depend on sequence length.
      forced by · decode is bandwidth-bound, so the marginal cost of extra positions is nearly free until you become compute-bound
    4. 4
      The rejection-sampling rule then converts "here are k proposals and the target's true conditionals" into a sample from the target distribution, accepting the longest correct prefix.
      forced by · each accepted token was drawn from the target's exact conditional given the tokens before it — which is the definition of autoregressive sampling
    5. 5
      Acceptance is a prefix property: one rejection invalidates every token after it, because those were conditioned on a token that was not chosen.
      forced by · conditional distributions computed on a wrong prefix are answers to a question you are no longer asking
    ⇒ Therefore

    Therefore verification is parallel for exactly the reason training is parallel and generation is not: you already have the tokens. Speculation manufactures a candidate sequence cheaply so it can borrow training-style parallelism at inference time.

    And note what this predicts. First: the speedup should collapse as batch size rises, because speculation spends idle compute, and a large batch has already spent it — measure tokens/s gain at batch 1 versus batch 64 and expect the advantage to shrink toward nothing. Second: expected tokens per target pass with acceptance rate α and draft length k is (1−α^(k+1))/(1−α), which saturates in k; so raising k should show sharply diminishing returns and eventually go negative once draft cost dominates, giving an optimum you can find by sweeping. Third: because rejection kills the whole suffix, speedup should depend on acceptance rate super-linearly — a draft that agrees 80% of the time is far more than twice as useful as one that agrees 40% of the time.

    Mental modelGuess the sentence, check it in one glance

    Someone is dictating and you are transcribing. Writing one word at a time means asking "next?" after every word — the round trip dominates. Instead you guess the next several words yourself, then ask them to read your guess and point at the first word you got wrong. Their read is one glance regardless of how many words you wrote.

    Everything before their finger is verified correct and you keep it. Everything after is discarded, because it was written assuming a word that turned out wrong. Your speedup is exactly how far the finger usually travels.

    • Verification is one pass whatever the draft length, because the target reads its weights once. That single fact is the whole mechanism.
    • Acceptance is a prefix: the first rejection discards everything after it. So expected gain grows with acceptance rate faster than linearly.
    • The speedup comes from idle compute during memory-bound decode. No idle compute, no speedup — which is why it fades under heavy batching.
    • Output distribution is exactly preserved. A bad draft costs throughput, never correctness. This means you can tune acceptance aggressively without any quality risk.
    • Draft cost is on the critical path. A draft model that is too good is too slow, and the optimum is a specific small ratio of draft to target cost.
    🔔 Fires when you see

    Fire this the moment you see: a single-stream latency requirement on a large model · GPU utilisation low during generation but memory bandwidth saturated · a workload with highly predictable output such as code, structured formats, or long verbatim quotes from the prompt · someone proposing a smaller model purely for latency · a claim that a decoding speedup changed the outputs (it should not, so it is a bug) · a draft length picked as a round number rather than by sweep.

    The tradeoff

    You want lower single-stream latency. Where does the draft come from — a separate small model, the model itself, or the prompt?

    A separate small draft model from the same family
    + you gain the highest acceptance rates in general, because a model trained on the same data with the same tokeniser genuinely approximates the target's conditionals; it works across all content types rather than only on predictable text
    − you pay a second set of weights in HBM competing with your KV cache, a second serving path to maintain and version, and a hard requirement that the tokenisers match exactly — the draft's latency also sits on the critical path of every step
    pick when a suitably small same-family checkpoint exists, you have memory headroom after weights and cache, and your traffic is general-purpose rather than one narrow format
    Self-drafting — extra prediction heads or early-exit layers on the target itself
    + you gain no second model, so no extra weights, no tokeniser mismatch, and no separate deployment; the draft sees the target's own hidden states, which makes it well-calibrated to the target by construction
    − you pay needs training the additional heads, so it is not a drop-in for an arbitrary checkpoint, and it complicates the model definition and every downstream tool that expects a standard architecture
    pick when you own the training pipeline for the model you serve, or you are deploying a checkpoint that already ships with such heads — otherwise the training cost dwarfs the benefit
    Draft from the context itself — n-gram or prompt lookup, no model at all
    + you gain effectively zero draft cost and zero extra memory, implementable in a few lines, and acceptance can be remarkably high on workloads where output copies the input
    − you pay acceptance collapses to near zero on genuinely novel generation, so the benefit is entirely workload-dependent and you may add overhead for nothing on general traffic
    pick when the output substantially overlaps the input — code editing, summarisation with quotation, retrieval-augmented answers, structured rewrites — measure the overlap on real traffic before committing
    What a senior engineer actually does

    Start by asking whether you are the right shape for this at all: speculation converts idle compute into tokens, so if you are already running large batches you have nothing to convert and should not bother. Given a genuine single-stream or low-concurrency latency target, try prompt-lookup drafting first — it costs almost nothing to implement and on copy-heavy workloads it is competitive with a draft model while consuming no memory.

    If you go to a draft model, tune two things by measurement and not by intuition: acceptance rate on your own traffic, and draft length. Acceptance is the number that predicts everything, and it is workload-specific, so a rate quoted for someone else's traffic tells you nothing about yours. And instrument it in production — acceptance rate drifting downward is an early signal that your traffic distribution has moved, which is useful information well beyond decoding.


    Quick recall · click to reveal
    ★ = stretch question

    Previous: ← DL S068 · Next: DL S070 →