Mini inference engine (KV cache + sampling + quantization)
Serve a model in a compact engine: implement the KV cache, sampling, and INT4 quantization end to end.
Goal
Build a small but real LLM inference server. It must support:
- Continuous batching.
- A KV cache (PagedAttention-style block allocator is the stretch goal).
- Standard sampling (greedy, top-k, top-p, min-p, temperature, frequency penalty).
- Streaming SSE responses to multiple concurrent clients.
- Optional INT4 / FP8 weight loading.
- An OpenAI-compatible
/v1/chat/completionsendpoint.
This is the project that proves you can do systems work, not just notebook code. Inference is where most of the production cost lives.
Time: 3-5 weeks part-time.
Prerequisites
02_transformer/04_build_gpt_from_scratch.md05_inference/02_kv_cache.md05_inference/01_sampling_decoding.md
Tech stack
- PyTorch 2.x (
F.scaled_dot_product_attention) - FastAPI +
uvicorn - A model loader for HF safetensors weights (no need to use the HF model class; you'll load weights into your own).
bitsandbytesorawq-pyfor quantized weights (optional)
Architecture
flowchart LR
CLIENT -->|/v1/chat/completions| API[FastAPI server]
API --> SCHED[Scheduler queue]
SCHED -->|micro-batch| ENG[Engine: forward + sample]
ENG --> KV[KV cache pool]
ENG --> WEIGHTS[(model weights)]
ENG -->|tokens| SCHED
SCHED --> STREAM[SSE streamer]
STREAM --> CLIENTStep-by-step
1. Reload a real model into your own architecture
Pick a small, well-known model (gpt2, Qwen2.5-0.5B, Llama-3.2-1B). Load its safetensors weights into the modern decoder you built in Project 2 (or write a fresh one for this model). Verify outputs match HF's model.generate(do_sample=False) exactly on a fixed prompt — bit-exact agreement on the first 32 tokens is the gold-standard sanity check.
2. KV cache
Implement a KVCache per request:
- Pre-allocate
(L, h_kv, max_len, d_h)per request. - Append on each step.
- Provide a
clear()method.
(Stretch: a paged allocator with 16-token blocks shared across requests.)
3. Sampling module
Implement, configurable per request:
temperature(withT=0→ argmax)top_ktop_pmin_pfrequency_penaltypresence_penaltyseed(deterministic generation)
Match OpenAI parameter names where possible.
4. Continuous batching scheduler
Maintain three queues:
pending— requests waiting for prefill.running— requests in decode.done— finished; awaiting return.
Each iteration:
- Move new requests into
running, prefilling their prompts. - Run one forward step for the batched
runningrequests. - Sample one token per request; append.
- Stream tokens to clients via
asyncio.Queues. - Move EOS/maxlen requests to
done.
Cap concurrency on KV memory budget.
5. FastAPI server
Endpoints:
POST /v1/chat/completions— OpenAI-compatible chat. Supportsstream: true(SSE).POST /v1/completions— legacy completion endpoint.GET /v1/models— list loaded models.GET /metrics— Prometheus metrics (request count, latency, tokens/s).
Test with the official openai Python SDK pointing at your server's URL.
6. Quantization (stretch)
Add a path to load AWQ-quantized weights (or just bitsandbytes 4-bit linear layers). Verify quality with a small perplexity check.
7. Speculative decoding (stretch)
Plug in a small draft model (Lesson 5.3). Show ~2× decode speedup at batch=1.
8. Benchmark
Compare to vLLM and TGI on:
- Tokens/s at batch=1.
- Tokens/s at batch=8.
- Time-to-first-token under load.
- Memory footprint at
T=8k.
Be honest: vLLM will likely beat your engine. Document by how much and why (PagedAttention, fused kernels, optimised CUDA).
Acceptance criteria
- Loads at least one real HF model and matches its outputs bit-exact at temperature 0 for the first 32 tokens.
- Implements KV cache; per-token cost roughly constant for a single request as T grows (no recompute).
- Implements all sampling controls listed above.
- Continuous batching of ≥ 8 concurrent requests.
- OpenAI-compatible JSON over HTTP, with SSE streaming.
- Prometheus
/metricswithtokens_generated_total,request_latency_seconds,kv_cache_used_bytes. - README with benchmark numbers vs vLLM (small model is fine).
Stretch goals
- PagedAttention-style block KV pool with prefix cache sharing.
- AWQ INT4 weight loading.
- Speculative decoding with a draft model.
- Tensor parallel across 2 GPUs.
- Tool-call streaming (
tool_callsfield deltas). - Support JSON-grammar sampling via
outlinesintegration. - Add a Triton or CUDA fused kernel for one operation (e.g., RMSNorm).
Common pitfalls
- Forgetting to detach KV from the autograd graph → memory blow-up.
- Mismatched sampling RNG across requests in a batch → bad determinism.
- Holding the GIL in CPU-bound Python sampling → throughput cap. Move to a separate process or use Triton/Numba for sampling.
- Streaming protocol bugs (SSE headers,
Content-Type: text/event-stream,data:prefix,[DONE]terminator). - Off-by-one between prompt length and KV-cache write index.
Story / portfolio
- Title: "Building vLLM (a worse one) to learn how vLLM works."
- Diagram: scheduler + KV manager.
- Benchmarks: tokens/s vs batch size; latency CDF; KV memory vs context length.
- Honest comparison: "vLLM is 1.7× faster because they fuse
qkv_proj + RoPE + attentionand use PagedAttention; here is what I would build next to close that gap." - Demo:
curla streaming chat completion + a tiny web UI.
This is the systems project. With Projects 2 and 5 in your portfolio, you can credibly apply for "ML systems" or "inference platform" roles — among the highest-paying tracks in the industry.