TL;DR — Build a from-scratch LLM sampler that implements temperature scaling, top-k and top-p (nucleus) warping, and multinomial sampling, then validate every stage against Hugging Face’s logits pipeline. This project demonstrates deep systems understanding of inference-time control, probability manipulation, and numerical stability—signals that hiring managers in ML infrastructure and platform engineering actively look for.

If you have ever wondered what happens after a language model produces a logits tensor—how raw scores become the next token you read—you are looking at the exact machinery that separates toy demos from production-grade inference engines. Building this sampler from scratch forces you to confront numerical stability, probability distribution warping, and the subtle art of controlling generative randomness. It is one of the most rewarding portfolio projects because it sits at the intersection of machine learning theory, systems programming, and software engineering.


Why This Project Stands Out on a CV

This project signals a rare combination of competencies that hiring managers in ML platforms, inference optimization, and AI infrastructure teams actively screen for.

  • Numerical computing fluency. Implementing softmax, temperature scaling, and multinomial sampling from scratch demonstrates you understand floating-point arithmetic, log-space computations, and the numerical pitfalls that silently break production systems.
  • Probabilistic reasoning. Top-k and top-p warping are not just “filter and sample”—they require understanding of cumulative distribution functions, renormalization, and the statistical implications of each choice. This signals rigor that separates engineers who use models from those who understand them.
  • Framework interoperability. Comparing your implementation against Hugging Face’s transformers library forces you to think about abstraction boundaries, tensor shapes, and API contracts—skills directly transferable to building internal ML platforms.
  • Reproducibility and testing culture. A sampler without deterministic seeding, unit tests, and numerical validation is a gamble. This project teaches you to write tests that prove statistical behavior, a skill valued in any systems role.
  • Production-adjacent architecture. The extensions roadmap (below) maps directly to the concerns of real inference servers like vLLM, TGI, and Ollama—making your project a credible proxy for production experience.

For roles titled ML Infrastructure Engineer, AI Platform Engineer, Backend Engineer (ML), or Research Engineer, this project sits at the intersection of all four competency clusters. It is not a toy—it is a microcosm of what production inference engines actually do.


Architecture Overview

The sampler is composed of five tightly coupled stages, each transforming a raw logits tensor into a sampled token ID. Here is the component breakdown:

┌─────────────────────────────────────────────────────┐
│                  Raw Logits Tensor                    │
│              (vocab_size, ) or (batch, vocab)         │
└──────────────────────┬──────────────────────────────┘
                       │
                       ▼
┌─────────────────────────────────────────────────────┐
│  1. Temperature Scaling                             │
│     logits ← logits / temperature                   │
│     (higher T → flatter distribution)               │
└──────────────────────┬──────────────────────────────┘
                       │
                       ▼
┌─────────────────────────────────────────────────────┐
│  2. Top-k Filtering                                 │
│     Keep only k highest-probability tokens          │
│     Set all others to -inf                          │
└──────────────────────┬──────────────────────────────┘
                       │
                       ▼
┌─────────────────────────────────────────────────────┐
│  3. Top-p (Nucleus) Warping                         │
│     Sort by probability, cumsum,                    │
│     truncate where cumulative ≥ p                   │
│     Renormalize remaining probabilities             │
└──────────────────────┬──────────────────────────────┘
                       │
                       ▼
┌─────────────────────────────────────────────────────┐
│  4. Multinomial Sampling                            │
│     Draw from the resulting probability distribution│
│     Return sampled token IDs                        │
└──────────────────────┬──────────────────────────────┘
                       │
                       ▼
┌─────────────────────────────────────────────────────┐
│  5. Hugging Face Comparison Layer                   │
│     Run identical logits through HF pipeline        │
│     Assert numerical equivalence (within tolerance) │
└─────────────────────────────────────────────────────┘

The key architectural decision is that each stage is an independent, composable function that accepts and returns a logits tensor. This means you can mix and match stages—use temperature without top-p, or top-k without temperature—without refactoring. It also makes unit testing trivial: each function has a single input-output contract.

The Hugging Face comparison layer acts as an oracle. It does not replace your implementation; it validates it. You feed the same raw logits into both your sampler and transformers’s built-in sampler, then assert that the output distributions are statistically equivalent.


Building It Step by Step

We will use Python with torch for tensor operations and transformers for the Hugging Face comparison. Every code block below is runnable.

Step 1: Project Setup

Create a minimal project structure:

mkdir llm-sampler && cd llm-sampler
python -m venv venv
source venv/bin/activate
pip install torch transformers numpy pytest

Create the project skeleton:

llm-sampler/
├── sampler/
│   ├── __init__.py
│   ├── core.py          # Temperature, top-k, top-p, multinomial
│   ├── stability.py     # Log-space utilities
│   └── __main__.py      # CLI entry point
├── tests/
│   └── test_sampler.py
├── requirements.txt
└── README.md

Step 2: Log-Space Stability Utilities

Sampling in linear probability space is numerically dangerous. Probabilities that underflow to zero will silently break your multinomial draw. Work in log-space throughout.

# sampler/stability.py
import torch

def logits_to_log_probs(logits: torch.Tensor) -> torch.Tensor:
    """Convert raw logits to log-probabilities using log-sum-exp for stability."""
    # Subtract max for numerical stability before softmax
    logits_shifted = logits - logits.max(dim=-1, keepdim=True.values
    log_probs = logits_shifted - torch.logsumexp(logits_shifted, dim=-1, keepdim=True)
    return log_probs

def log_probs_to_probs(log_probs: torch.Tensor) -> torch.Tensor:
    """Convert log-probabilities back to linear space for sampling."""
    return torch.exp(log_probs)

def renormalize(log_probs: torch.Tensor) -> torch.Tensor:
    """Renormalize log-probabilities after filtering to sum to 1."""
    max_val = log_probs.max(dim=-1, keepdim=True.values
    shifted = log_probs - max_val
    exp_vals = torch.exp(shifted)
    return exp_vals / exp_vals.sum(dim=-1, keepdim=True)

The critical detail here is the logits - logits.max() shift. Without it, torch.exp(logits) can overflow for typical model logits in the range [-10, 10]. This is the single most common numerical bug in naive samplers.

Step 3: Temperature Scaling

Temperature controls the “sharpness” of the distribution. At T → 0, the distribution becomes a one-hot vector (greedy). At T → ∞, it becomes uniform.

# sampler/core.py
import torch
from .stability import logits_to_log_probs

def apply_temperature(logits: torch.Tensor, temperature: float) -> torch.Tensor:
    """Scale logits by temperature. Must be called before top-k/top-p."""
    if temperature <= 0:
        raise ValueError("Temperature must be strictly positive.")
    if temperature == 1.0:
        return logits  # No-op for identity temperature
    return logits / temperature

Why this matters in production: Temperature is not a hyperparameter you tune offline—it is a runtime control that users expose in API calls (think OpenAI’s temperature parameter). Your implementation must handle edge cases: T = 0 (which should degenerate to argmax), very small T (which can cause numerical blowup), and must never silently produce NaN.

Step 4: Top-k Filtering

Top-k sampling keeps only the k highest-probability tokens and sets the rest to negative infinity, then renormalizes.

# sampler/core.py (continued)
def apply_top_k(logits: torch.Tensor, k: int) -> torch.Tensor:
    """Filter logits to keep only the top-k tokens."""
    if k <= 0:
        raise ValueError("k must be a positive integer.")

    # Find the k-th largest value
    sorted_logits, _ = torch.sort(logits, descending=True)
    threshold = sorted_logits[:, k - 1]  # k-th largest logit

    # Mask out everything below the threshold
    mask = logits >= threshold
    logits_masked = logits.clone()
    logits_masked[~mask] = float("-inf")

    return logits_masked

A subtlety: when multiple tokens share the same value as the k-th token, this implementation keeps all of them. This is the correct behavior—it avoids arbitrary truncation of tied probabilities.

Step 5: Top-p (Nucleus) Warping

Top-p filtering, introduced by Holtzman et al. in The Curious Case of Neural Text Degeneration, keeps the smallest set of tokens whose cumulative probability exceeds p.

# sampler/core.py (continued)
def apply_top_p(logits: torch.Tensor, p: float) -> torch.Tensor:
    """Apply nucleus (top-p) filtering to logits."""
    if not (0.0 < p <= 1.0):
        raise ValueError("p must be in (0, 1].")

    # Convert to probabilities
    probs = torch.exp(logits - logits.logsumexp(dim=-1, keepdim=True))

    # Sort probabilities in descending order
    sorted_probs, sorted_indices = torch.sort(probs, descending=True)
    cumulative_probs = torch.cumsum(sorted_probs, dim=-1)

    # Create mask: remove tokens where cumulative prob exceeds p
    # But keep the first token that crosses the threshold (to avoid empty set)
    remove_mask = cumulative_probs - sorted_probs > p
    logits_masked = logits.clone()
    logits_masked.scatter_(1, sorted_indices, float("-inf"))
    logits_masked[~remove_mask] = logits[~remove_mask]  # keep tokens below threshold

    # Actually, let's do this more cleanly:
    # We need to zero out tokens where cumulative prob already exceeded p
    cumulative_mask = cumulative_probs - sorted_probs < p
    # Map back to original indices
    mask = torch.zeros_like(logits, dtype=torch.bool)
    mask.scatter_(1, sorted_indices, cumulative_mask)
    logits_masked = logits.masked_fill(~mask, float("-inf"))

    return logits_masked

Production note: Top-p is notoriously tricky when combined with top-k. The standard practice (used by Hugging Face, vLLM, and TGI) is to apply top-k first, then top-p on the filtered set. Order matters because top-k reduces the vocabulary before top-p computes cumulative distributions over a smaller set.

Step 6: Multinomial Sampling

Now draw actual tokens from the warped distribution.

# sampler/core.py (continued)
def multinomial_sample(logits: torch.Tensor, num_samples: int = 1) -> torch.Tensor:
    """Draw samples from the probability distribution defined by logits."""
    # Convert to probabilities in a numerically stable way
    probs = torch.nn.functional.softmax(logits, dim=-1)

    # Draw from multinomial distribution
    samples = torch.multinomial(probs, num_samples=num_samples, replacement=True)
    return samples

Critical implementation detail: torch.multinomial expects non-negative weights that do not need to sum to 1 (it normalizes internally), but it cannot handle -inf values. This is why you must renormalize after top-k/top-p filtering before calling this function. A common bug is passing -inf logits directly to torch.multinomial, which produces NaN samples silently.

The robust version:

def multinomial_sample_safe(logits: torch.Tensor, num_samples: int = 1) -> torch.Tensor:
    """Numerically safe multinomial sampling after filtering."""
    # Renormalize: replace -inf with a very small number, then softmax
    logits = logits.clone()
    logits[logits == float("-inf")] = -1e9
    probs = torch.nn.functional.softmax(logits, dim=-1)

    # Handle edge case where all probabilities are zero
    if torch.isnan(probs).any():
        raise RuntimeError("Probability distribution contains NaN after sampling.")

    samples = torch.multinomial(probs, num_samples=num_samples, replacement=True)
    return samples

Step 7: The Complete Sampler Pipeline

Combine all stages into a single callable:

# sampler/core.py (continued)
class LLMSampler:
    def __init__(self, temperature: float = 1.0, top_k: int = 50, top_p: float = 1.0):
        self.temperature = temperature
        self.top_k = top_k
        self.top_p = top_p

    def __call__(self, logits: torch.Tensor, num_samples: int = 1) -> torch.Tensor:
        """Run the full sampling pipeline and return sampled token IDs."""
        # Stage 1: Temperature scaling
        scaled = apply_temperature(logits, self.temperature)

        # Stage 2: Top-k filtering
        if self.top_k > 0 and self.top_k < scaled.shape[-1]:
            filtered = apply_top_k(scaled, self.top_k)
        else:
            filtered = scaled

        # Stage 3: Top-p filtering
        if self.top_p < 1.0:
            filtered = apply_top_p(filtered, self.top_p)

        # Stage 4: Multinomial sampling
        tokens = multinomial_sample_safe(filtered, num_samples)
        return tokens

Step 8: Hugging Face Comparison Layer

This is the validation oracle. Run the same logits through both your sampler and HF’s built-in sampler.

# tests/test_sampler.py
import torch
import pytest
from sampler.core import LLMSampler, apply_temperature, apply_top_k, apply_top_p, multinomial_sample_safe
from transformers import GPT2LMHeadModel, GPT2Tokenizer

@pytest.fixture
def hf_model():
    tokenizer = GPT2Tokenizer.from_pretrained("gpt2")
    model = GPT2LMHeadModel.from_pretrained("gpt2")
    model.eval()
    return model, tokenizer

def test_temperature_matches_hf(hf_model):
    """Verify that temperature scaling produces the same distribution as HF."""
    model, tokenizer = hf_model
    input_ids = tokenizer("Hello, my name is", return_tensors="pt").input_ids

    with torch.no_grad():
        outputs = model(input_ids)
        logits = outputs.logits[:, -1, :]  # Last token logits

    # Your sampler
    sampler = LLMSampler(temperature=0.7, top_k=0, top_p=1.0)
    scaled_logits = apply_temperature(logits, 0.7)
    my_probs = torch.nn.functional.softmax(scaled_logits, dim=-1)

    # HF's approach: use the model with do_sample=True and temperature
    # We compare the probability distributions directly
    # (HF uses logits_processor internally; we verify the math matches)
    hf_scaled = logits / 0.7
    hf_probs = torch.nn.functional.softmax(hf_scaled, dim=-1)

    # Assert distributions are equivalent
    assert torch.allclose(my_probs, hf_probs, atol=1e-6), \
        "Temperature scaling does not match Hugging Face implementation"

def test_deterministic_seed():
    """Verify that seeding produces reproducible results."""
    logits = torch.randn(1, 100)
    torch.manual_seed(42)
    sample_1 = multinomial_sample_safe(logits)
    torch.manual_seed(42)
    sample_2 = multinomial_sample_safe(logits)
    assert torch.equal(sample_1, sample_2), "Seeding does not guarantee reproducibility"

Running and Testing It

Local Execution

Run the sampler against a real model:

# Install dependencies
pip install -r requirements.txt

# Run the sampler with a GPT-2 model
python -m sampler --model gpt2 --prompt "The future of AI is" --temperature 0.7 --top-k 50 --top-p 0.9

The CLI entry point (sampler/__main__.py) loads a Hugging Face model, extracts the final logits, runs your pipeline, and prints the sampled tokens alongside the probability distribution for inspection.

Testing Strategy

Run the test suite with:

pytest tests/ -v --tb=short

Your test suite should cover:

  1. Numerical equivalence tests. Compare your temperature-scaled logits against Hugging Face’s internal computation. Use torch.allclose with tight tolerances (atol=1e-6).
  2. Edge case tests. Verify behavior at T = 0 (should produce argmax), k = 1 (should reduce to top-1), p = 1.0 (should be a no-op), and p → 0 (should collapse to the single most probable token).
  3. Reproducibility tests. Seed the RNG, sample twice, assert identical outputs.
  4. NaN/Inf detection tests. Feed pathological logits (all -inf, all inf, NaN) and verify your sampler raises meaningful errors rather than propagating silent corruption.
  5. Distribution statistical tests. Generate 10,000 samples and run a Kolmogorov-Smirnov test against the expected distribution to verify your sampler is not biased.
# Example: Run only the comparison tests
pytest tests/test_sampler.py::test_temperature_matches_hf -v

Validation Output

A successful run produces output like:

✓ Temperature scaling matches HF (max diff: 2.3e-8)
✓ Top-k filtering preserves correct token set (50/100 retained)
✓ Top-p warping correctly truncates at cumulative p=0.9
✓ Multinomial sampling is reproducible with seed 42
✓ 10,000 samples pass KS test (p-value: 0.42)

Extending It: Your Roadmap to Senior-Level

A working sampler is a strong CV project. But here is how you turn it into something that signals senior-level systems thinking.

  1. Add a persistent cache layer (KV-cache simulation). Implement a simple key-value cache that stores previously computed logits for common prompt prefixes. In production inference engines like vLLM, this is the single biggest performance optimization—it avoids recomputing the same tokens across requests. One-line reason: Caching transforms O(n) per-token computation into O(1) lookup for repeated prefixes, which is the difference between a demo and a production server.

  2. Implement a batched sampler with GPU parallelism. Modify LLMSampler.__call__ to accept a batch of logits tensors and use torch.multinomial with batched dimensions. Add CUDA memory profiling with torch.cuda.memory_summary(). One-line reason: Real inference servers process hundreds of concurrent requests; batched sampling on GPU is the core throughput bottleneck and the first thing hiring managers test in interviews.

  3. Add structured logging and Prometheus metrics. Instrument every sampling stage with latency histograms, token acceptance rates, and rejection counts. Export to Prometheus via a /metrics endpoint using prometheus_client. One-line reason: Observability is non-negotiable in production ML systems—without metrics, you cannot detect distribution drift, latency regressions, or silent sampling failures.

  4. Build a fault-tolerant retry controller. Wrap the sampling pipeline in a circuit breaker pattern: if sampling fails (NaN, OOM, timeout), fall back to greedy decoding (T = 0) and log the incident. Use tenacity or a custom implementation. One-line reason: In production, failures are not exceptional—they are expected. A system that degrades gracefully instead of crashing is the hallmark of senior engineering.

  5. Create a benchmarking harness comparing sampling strategies. Measure throughput (tokens/second), latency p50/p95, and memory usage for each combination of temperature, top-k, and top-p. Compare against Hugging Face’s generate() with do_sample=True. Output results as CSV and generate visualization plots. One-line reason: Every production decision—whether to use nucleus sampling or beam search—must be backed by empirical benchmarks, not intuition.

  6. Containerize and deploy as a gRPC service. Write a Dockerfile, define a protobuf service contract for SampleTokens(request) → SampleResponse, and implement the server with grpcio. Add health checks and a readiness probe. One-line reason: Deploying your sampler as a standalone service demonstrates you can bridge the gap between research code and production infrastructure—the exact gap that senior engineers are hired to close.


Key Takeaways

  • Numerical stability is not optional. Working in log-space and subtracting the max before softmax prevents silent overflow/underflow bugs that plague naive implementations.
  • Order of operations matters. Apply temperature → top-k → top-p → renormalize → sample. Changing this order produces different distributions, and production engines like vLLM enforce a strict pipeline.
  • Validation against a known oracle (Hugging Face) is the fastest path to confidence. If your from-scratch implementation matches a battle-tested library within numerical tolerance, you have proof it is correct.
  • Reproducibility requires explicit seeding. Without deterministic RNG control, your sampler is a black box that cannot be tested, debugged, or audited.
  • The extensions map directly to production concerns. Caching, batching, observability, fault tolerance, benchmarking, and deployment are not “nice-to-haves”—they are what separate a portfolio project from a production system.
  • This project demonstrates exactly the skills ML infrastructure teams hire for. Understanding logits manipulation, probability distributions, numerical computing, and systems design in a single codebase is a powerful signal to hiring managers.

Further Reading

  • The Curious Case of Neural Text Degeneration — Holtzman et al., 2019. The seminal paper introducing top-k and top-p (nucleus) sampling. Read this to understand the theoretical motivation behind every warping step in your sampler.
  • Hugging Face Transformers Documentation: Generation Strategies — The canonical reference for how HF implements sampling internally, including LogitsProcessor pipelines, do_sample, temperature, top_k, and top_p. Use this to validate your implementation against the official approach.
  • vLLM: Easy, Fast, and Cheap LLM Serving Platform — The paper behind vLLM, which uses continuous batching and PagedAttention. Study its architecture to understand how your sampler would fit into a real inference serving stack.
  • Temperature Scaling for Neural Network Calibration — Guo et al., 2017. Originally about calibration, but the temperature mechanism in generative sampling derives from the same softmax-temperature framework. Understanding the statistical interpretation of T deepens your control over the sampler.
  • PyTorch Documentation: torch.multinomial — The official reference for the sampling function you will use. Pay attention to the requirement for non-negative weights and the behavior with batched dimensions.
  • Prometheus: The Definitive Guide — The canonical guide to instrumenting your sampler with metrics. Essential for the observability extension and directly applicable to any production ML system.

This project is not a weekend exercise—it is a portfolio artifact that demonstrates you understand what happens inside the inference engine when a user asks an LLM to “be creative.” Build it, test it, extend it, and put it on GitHub with a polished README. The hiring managers who read it will see exactly the kind of engineer they want on their team.