Building tinyGPT: A Transformer Inference Engine From Scratch in C++

Kartik Sirohi  ·  Learning & Systems

Back in April, I set out with a simple goal: build and understand a Transformer language model end to end — not just training it, but everything that happens after, all the way down to the metal.

Where it started

The spark came from contributing to llama.cpp, where I implemented a SIMD-vectorized version of ggml_vec_dot_q4_1_q8_1 — a quantized dot-product kernel used during inference. That PR taught me a lot about one optimized operation. But it also left me with a bigger question: what happens around that one kernel? How does a model actually get from "trained weights" to "text streaming out token by token," and what does it take to make that fast?

I decided the only real way to find out was to build the whole stack myself — training included.

Phase 1: Training the model

I implemented a decoder-only Transformer in PyTorch and trained a ~30M parameter GPT on the TinyStories dataset, on Kaggle, for 2 epochs. Nothing exotic here — the interesting part of this project was never the training run, it was what came after.

Final architecture

Parameter Value
Parameters ~30M
\(d_{\text{model}}\) 256
Heads 8
Blocks 6
\(d_k\) (per head) 32
Vocabulary 50,257 (GPT-2 BPE)
Max sequence length 128

Once training was done, I exported the weights and made a deliberate choice: no torch.jit, no ONNX, no existing runtime. I wanted to write the inference engine myself, in C++, from the ground up.

Phase 2: Writing an inference engine from nothing

The engine grew in distinct stages, each one solving a specific bottleneck I hit in the previous one.

Results

I benchmarked the final engine against a naive PyTorch implementation, on both MPS and CPU, on an Apple M1 MacBook Air (mean of 5 runs):

Metric PyTorch (MPS) PyTorch (CPU) Custom C++ Engine
Time-to-First-Token 910.29 ms 13.53 ms 14.08 ms
Avg time/token 87.00 ms 12.20 ms 8.11 ms
Throughput 11.49 tok/s 81.96 tok/s 120.94 tok/s

The MPS comparison looks dramatic, but the more honest and interesting number is the CPU-vs-CPU one: even against PyTorch's own CPU path, the custom engine wins on every metric, purely from tiling, SIMD, KV caching, and threading — no exotic tricks, just doing the well-known things correctly and stacking them.

What actually mattered

If I had to rank the four optimizations by impact:

  1. KV caching gave the single biggest algorithmic win — it changes the complexity class of generation, not just the constant factor.
  2. Tiled matmul mattered because, at this model size, matrix multiplication dominates the per-token cost, and cache misses were the silent killer in the naive version.
  3. SIMD compounds on top of tiling — once the memory access pattern is cache-friendly, vectorizing the arithmetic gives another significant improvement.
  4. OpenMP was the smallest but easiest win, and only paid off once the per-operation cost was high enough to amortize threading overhead — which is why the adaptive threshold mattered.

The broader lesson: inference engineering isn't really about writing a forward pass. It's about not doing work you don't have to do (KV caching), respecting the memory hierarchy (tiling), and using the hardware you actually have (SIMD + threads). None of these ideas are novel—they're standard techniques used in mature inference systems such as llama.cpp and vLLM—but implementing them myself, and watching the profiler numbers change as each optimization landed, made the "why" click in a way that simply reading papers never did.

Where it's limited (on purpose)

This is a learning project, not a production runtime, and it's honest about that:

What's next

The natural next step is weight quantization (starting with INT8 and eventually mixed precision), followed by exploring multi-GPU inference to better understand tensor parallelism, communication costs, and memory-bandwidth tradeoffs at larger scales.

Note: AI assistance was used for debugging, error handling, and some boilerplate during development. The model implementation, training pipeline, inference engine architecture, and optimization strategy are my own work.

Project Resources

  1. Code Repository: github.com/sirohikartik/tinygpt