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
transformerslibrary 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:
- Numerical equivalence tests. Compare your temperature-scaled logits against Hugging Face’s internal computation. Use
torch.allclosewith tight tolerances (atol=1e-6). - 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).
- Reproducibility tests. Seed the RNG, sample twice, assert identical outputs.
- NaN/Inf detection tests. Feed pathological logits (all -inf, all inf, NaN) and verify your sampler raises meaningful errors rather than propagating silent corruption.
- 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.
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.
Implement a batched sampler with GPU parallelism. Modify
LLMSampler.__call__to accept a batch of logits tensors and usetorch.multinomialwith batched dimensions. Add CUDA memory profiling withtorch.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.Add structured logging and Prometheus metrics. Instrument every sampling stage with latency histograms, token acceptance rates, and rejection counts. Export to Prometheus via a
/metricsendpoint usingprometheus_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.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
tenacityor 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.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()withdo_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.Containerize and deploy as a gRPC service. Write a
Dockerfile, define a protobuf service contract forSampleTokens(request) → SampleResponse, and implement the server withgrpcio. 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
LogitsProcessorpipelines,do_sample,temperature,top_k, andtop_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.