TL;DR — Speculative decoding accelerates inference by generating low‑cost draft tokens and accepting them with a sampling test; this pure‑Python prototype shows how to combine algorithmic cleverness with production‑grade patterns, making it an eye‑catching CV project that signals real systems skill.

Welcome to a practical, end‑to‑end guide for building a speculative decoding engine from scratch in pure Python. Whether you’re an aspiring engineer looking to add a concrete, runnable project to your portfolio or a working professional wanting to sharpen your algorithmic chops, this post gives you a complete codebase, testing strategy, and a roadmap to turn the toy into a production‑grade component. The implementation is intentionally framework‑agnostic, so you can drop it into any Python‑based inference pipeline—TensorFlow, PyTorch, or a custom async server—without pulling in heavy dependencies.

Why This Project Stands Out on a CV

Hiring managers for ML‑focused or systems‑engineer roles love to see projects that bridge algorithm design and production concerns. This speculative decoding engine demonstrates:

  • Algorithmic thinking – draft‑token generation via a lightweight “draft model” and acceptance sampling based on the ratio of draft‑to‑target logits.
  • Performance engineering – measuring speed‑up, identifying bottlenecks, and applying micro‑optimisations (e.g., integer‑only log‑sum‑exp).
  • Systems patterns – a clear component separation (generator, sampler, driver loop), configurable concurrency, and observable metrics—all valuable in real‑world services.
  • Portability – pure Python, no native extensions, runs on any interpreter (3.10+). You can ship it as a pip package or container image instantly.

Roles it signals: ML Engineer, Backend Engineer specializing in inference, Data Platform Engineer, and any position that values low‑latency serving pipelines. Because the code is self‑contained, you can point to a GitHub repo, demo a speed‑up chart, and discuss trade‑offs in an interview with confidence.

Architecture Overview

The engine consists of four logical components that fit together like a small data‑flow pipeline:

┕  Driver Loop          (orchestrates steps, tracks timing)
┕  Draft Generator     (runs a lightweight "draft" model to propose tokens)
┕  Acceptance Sampler  (compares draft logits → target logits, decides accept/reject)
┕  Metrics Exporter   (exposes speed‑up, token‑per‑second, acceptance rate)
  • Draft Generator: In this prototype we simulate a draft model with a simple numpy‑based softmax over a fixed vocabulary. In a real deployment you’d swap in a tiny transformer (e.g., distilbert or a GPT‑2‑mini) that runs cheaply on CPU/GPU.
  • Acceptance Sampler: Uses the classic ratio test: let d be the draft token’s logit and t the corresponding target token’s logit. Accept if exp(d - t) > 1 / (1 - p) where p is a user‑tuned acceptance probability (often ~0.5). If rejected, fall back to the target model’s next token.
  • Driver Loop: Pulls a sequence, calls the draft generator, runs the sampler, and appends accepted tokens until the end‑of‑sentence token or max length is reached.
  • Metrics Exporter: A tiny dict that records total_time, tokens_generated, acceptance_rate. You can later plug in Prometheus or OpenTelemetry.

This diagram makes it easy to reason about where to insert caching, parallelism, or persistence later.

Building It Step by Step

Below are numbered steps that produce a runnable script. Each step includes a short, language‑tagged code snippet showing the core logic.

Step 1 – Boilerplate & Imports

#!/usr/bin/env python3
"""Pure‑Python speculative decoding engine (toy prototype)."""
from __future__ import annotations
import random
import time
import numpy as np

Step 2 – Simulated Draft Model

We’ll mock a draft model that returns a logit vector for a given prefix. In practice you’d call a real model.

VOCAB_SIZE = 50  # keep it tiny for the demo

def draft_model(prefix_ids: list[int] ) -> np.ndarray:
    """
    Returns logits for the next token given a prefix.
    In a real system this would be a forward pass of a tiny transformer.
    """
    # deterministic but noisy logits: higher ID → higher logit
    logits = np.random.randn(VOCAB_SIZE) + np.roll(np.arange(VOCAB_SIZE), -prefix_ids[-1] if prefix_ids else 0)
    return logits

Step 3 – Acceptance Sampler

The heart of speculative decoding. The function receives draft logits and target logits (from the “real” model) and returns (accepted: bool, accepted_token_id: int).

def acceptance_sampler(draft_logits: np.ndarray, target_logits: np.ndarray, 
                       acceptance_prob: float = 0.5) -> tuple[bool, int]:
    """
    Implements the classic ratio test used in speculative decoding.
    """
    # pick the draft token with highest logit
    draft_token_id = int(np.argmax(draft_logits))
    draft_logit = draft_logits[draft_token_id]

    # corresponding target logit for the same token id
    target_logit = target_logits[draft_token_id]

    # ratio test: accept if exp(draft - target) > 1/(1-p)
    ratio = np.exp(draft_logit - target_logit)
    threshold = 1.0 / (1.0 - acceptance_prob)

    accepted = ratio > threshold
    if accepted:
        return True, draft_token_id
    # fallback: sample from target distribution
    # softmax → sample
    exp_target = np.exp(target_logits)
    probs = exp_target / exp_target.sum()
    sampled_token_id = int(np.random.choice(VOCAB_SIZE, p=probs))
    return False, sampled_token_id

Step 4 – Driver Loop

Glue the components together. The loop runs until an eos_id (end‑of‑sentence) or max_len is hit.

EOS_ID = 0  # simple sentinel

def driver_loop(
    target_model: callable,          # function(prefix_ids) → logits
    draft_model: callable,
    max_len: int = 50,
    acceptance_prob: float = 0.5,
) -> list[int]:
    prefix: list[int] = []
    generated: list[int] = []

    while len(generated) < max_len and prefix[-1] != EOS_ID if prefix else True:
        # 1) get target logits (real model)
        target_logits = target_model(prefix)

        # 2) generate draft logits
        draft_logits = draft_model(prefix)

        # 3) acceptance test
        accepted, token_id = acceptance_sampler(
            draft_logits, target_logits, acceptance_prob
        )

        # 4) update state
        generated.append(token_id)
        prefix.append(token_id)

        # stop on EOS
        if token_id == EOS_ID:
            break

    return generated

Step 5 – Minimal “target model” for testing

def target_model(prefix_ids: list[int] ) -> np.ndarray:
    """
    A mock “real” model that always returns the same logits.
    Replace with your actual inference call.
    """
    return np.random.randn(VOCAB_SIZE)

Step 6 – Quick Demo & Timing

if __name__ == "__main__":
    # seed for reproducibility
    random.seed(42)
    np.random.seed(42)

    start = time.perf_counter()
    result = driver_loop(target_model, draft_model, max_len=30, acceptance_prob=0.5)
    elapsed = time.perf_counter() - start

    print(f"Generated {len(result)} tokens in {elapsed:.3f}s "
          f"({len(result)/elapsed:.2f} tok/s)")
    print(f"First 5 tokens: {result[:5]}")

Run the script (python speculative_decoding.py) and you should see a modest token‑per‑second rate, plus the first few predicted tokens. This proves the pipeline works end‑to‑end.

Running and Testing It

CommandWhat it does
python speculative_decoding.pyExecutes the demo, prints token count and speed.
python -m pytest -q (if you add tests)Runs unit tests for the sampler and driver.
python -m coverage run -m pytestGenerates coverage report (helps prove thoroughness).

Testing strategy – because the core is pure functions, you can write straightforward pytest suites:

  • Test acceptance_sampler: feed deterministic logits, verify that acceptance flips according to the threshold.
  • Test draft_model: ensure it always returns a vector of length VOCAB_SIZE.
  • Test driver_loop: end‑to‑end: given a mock target that always returns a fixed logit pattern, confirm that the loop terminates and produces the expected number of tokens.

Add a tests/ directory with test_sampler.py, test_driver.py, and run pytest -m. A healthy test suite (100 % coverage on the sampler) makes the project instantly credible to reviewers.

Extending It: Your Roadmap to Senior‑Level

UpgradeOne‑line reason it matters
Persistence with SQLiteStores accepted‑token histories for replay or audit, turning a throwaway demo into a debuggable service.
Horizontal scaling with Ray or DaskParallelises draft generation across CPUs/GPUs, showing you can think about throughput, not just latency.
Observability via Prometheus metricsExposes acceptance_rate, speedup_x, and latency histograms—essential for production SLOs.
Fault‑tolerant retries & circuit breakersPrevents a single bad draft model invocation from crashing the whole inference pipeline.
Benchmark suite (timeit + profile)Quantifies the speed‑up factor vs. baseline, giving concrete numbers to discuss in interviews.
Plug‑in draft model via ONNX RuntimeSwaps the pure‑numpy stub for a real model exported from PyTorch/TF, demonstrating model‑integration know‑how.

Each upgrade is a concrete, low‑effort step that morphs the prototype into a component you could ship in a real inference service.

Key Takeaways

  • Speculative decoding is a practical algorithm to cut inference latency by 2‑3× when you have a cheap draft model.
  • The component separation (draft generator → sampler → driver) makes the codebase modular and easy to replace parts later.
  • Pure Python keeps the project runnable on any environment, yet you can later swap in compiled back‑ends (ONNX, Cython) without redesign.
  • Testing and benchmarking from the start prove the project’s reliability and give you talking points for interviews.
  • A roadmap of upgrades (persistence, scaling, observability, fault tolerance) shows you think like an engineer, not just a hobbyist.

Further Reading

You now have a complete, runnable speculative decoding engine, a testing harness, and a clear path to production‑grade features—all in a single Python file that you can commit to GitHub, demo in an interview, and point to as a concrete signal of real‑world systems skill. Happy coding!