A C++ implementation of a GPT-like large language model from scratch, built with LibTorch (PyTorch C++ API). This project is a learning exercise to understand transformer architecture, attention mechanisms (including modern variants), tokenization, KV caching, data pipelines, and full pre-training loops—all in C++.
- Attention Mechanism : Self-attention with learned Q, K, V weight matrices (scaled dot-product attention)
- Multi-Head Causal Attention : Multi-head self-attention with causal masking (GPT-2 style)
- KV Cache : Key-value caching for fast iterative text generation (
MultiHeadAttentionV2,GptModelV2) - Transformer Blocks : Full GPT-2 style block (LayerNorm → Attention → shortcut, LayerNorm → FeedForward → shortcut)
- GELU Activation : GPT-2 style GELU activation in the feed-forward network
- Text Generation : Greedy text generation with and without KV cache (
generate_text_simple/generateTextSimpleCached) - GPT Dataset V1 : Custom dataset loader for text-based training data (
the-verdict.txt) - Tokenizer : OpenAI's tiktoken C++ port for BPE (Byte-Pair Encoding) tokenization
- Embeddings : Token + position embedding layer
- Group Query Attention (GQA) :
GroupQueryAttentionV1— query heads split inton_kv_groups; each group ofgroupSize = numHeads / numKvGroupsquery heads shares a single K/V head. Reduces KV-cache memory footprint and attention bandwidth during decoding at a small accuracy cost vs full MHA. - Multi-Head Latent Attention (MLA) :
MultiHeadLatentV1/gpt2mhl— latent (low-rank) compression of the K/V projections (DeepSeek-style) to shrink KV-cache memory even further than GQA.
- Data Preparation :
PreparedData(dataPreparation.hpp/cpp) — tokenizer loading, text↔token-ID conversion, batch encoding, and sliding-window GPT dataset construction. - DataLoader : Custom
createDataLoaderV2withSequentialSamplermatching Python'sDataLoader(shuffle=False), producing[batch, seq_len]input/target batches (targets = inputs shifted by 1). - Cross-Entropy Loss :
cal_loss_batch/total_loss_loader— batch loss and full-loader loss evaluation (mirrors PyTorch'scalc_loss_batch/calc_loss_loader). - Training Loop :
train_model_simple— AdamW optimizer (lr=4e-4,weight_decay=0.1), epoch/step tracking, periodic train/val loss evaluation, CUDA cache clearing, and text-sample generation during training. - Sampling :
generate_with_temperature— top-k filtering + temperature-scaled softmax +multinomialsampling, with optional EOS-token early stopping.
- FLOPs Analysis :
FlopsAnalysis.cpp— estimates floating-point operations for attention layers. - Memory Estimation :
memoryEstimateMHA_GQA.cpp— compares KV-cache memory footprint of full Multi-Head Attention vs Group Query Attention. - Unit Tests : doctest — all components are tested through
TEST_CASEmacros (nomain(); doctest provides the entry point).
The model mirrors the GPT-2 architecture (configurable via config struct, default = GPT-2 124M):
Input Tokens [batch, seq_len]
│
▼
┌─────────────────────────────────┐
│ Token Embedding + Position │
│ Embedding + Dropout │
└─────────────────────────────────┘
│
▼ (repeated n_layers times)
┌─────────────────────────────────┐
│ Transformer Block │
│ ┌───────────────────────────┐ │
│ │ LayerNorm1 │ │
│ │ Multi-Head Causal Attn │ │ ← MHA / GQA / MLA variants
│ │ + shortcut │ │
│ └───────────────────────────┘ │
│ ┌───────────────────────────┐ │
│ │ LayerNorm2 │ │
│ │ GELU Feed-Forward │ │
│ │ + shortcut │ │
│ └───────────────────────────┘ │
└─────────────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ Final LayerNorm │
│ Output Head [vocab_size] │
└─────────────────────────────────┘
| Module | File | Purpose |
|---|---|---|
GptModelV2 |
GptModelV2.hpp/cpp |
Full GPT-2 style model with token+position embedding, stacked transformer blocks, final norm, output head, and KV-cache lifecycle (reset_kv_cache) |
TransformBlockV2 |
TransformBlockV2.hpp/cpp |
One transformer block: LayerNorm→Attention→dropout→shortcut, then LayerNorm→FeedForward→dropout→shortcut |
MultiHeadAttentionV2 |
MultiHeadAttentionV2.hpp/cpp |
Multi-head causal self-attention with an optional sliding-window KV cache (cache_k, cache_v, current_ptr) |
FeedForward |
FeedForward.hpp/cpp |
Two-layer MLP (emb_dim → 4×emb_dim → emb_dim) with GELU |
LayerNormV2 |
LayerNormV2.hpp/cpp |
GPT-2 style learnable LayerNorm with affine parameters |
GELU |
GELU.hpp/cpp |
GPT-2 style GELU activation (tanh approximation) |
| Module | File | Purpose |
|---|---|---|
GroupQueryAttentionV1 |
GroupQueryAttentionV1.hpp/cpp |
GQA attention layer; projects Q to numHeads×head_dim but K/V to numKvGroups×head_dim, then shares each K/V head across a group of query heads via repeat_interleave. Maintains a growing KV cache. |
Gpt2ModelV3 |
Gpt2ModelV3.hpp/cpp |
GPT-2 model variant using GQA attention with KV cache lifecycle |
TransformerBlockV4 |
TransformerBlockV4.hpp/cpp |
Transformer block with GQA attention |
FeedForwardV3 |
FeedForwardV3.hpp/cpp |
GELU feed-forward network |
LayerNormV3 |
LayerNormV3.hpp/cpp |
Learnable LayerNorm |
GELUV3 |
GELUV3.hpp/cpp |
GPT-2 style GELU |
| Module | File | Purpose |
|---|---|---|
gpt2mhl |
gpt2mhl.hpp/cpp |
End-to-end GPT-2 model with Multi-Head Latent Attention; supports KV cache, loss, generation (generateTextSimpleCachedV2), and pre-training |
MultiHeadLatentV1 |
MultiHeadLatentV1.hpp/cpp |
Latent (low-rank) K/V compression attention layer to minimize KV-cache memory |
TransformerBlockV5 |
TransformerBlockV5.hpp/cpp |
Transformer block using the latent attention layer |
| Module | File | Purpose |
|---|---|---|
PreparedData |
include/Gpt2Untrained/dataPreparation.hpp, src/Gpt2Untrained/dataPreparation.cpp |
Tokenizer loading, text↔token-ID conversion, batch encoding |
GPTDatasetV1 |
include/basics/GPTDatasetV1.h |
Sliding-window dataset producing input/target pairs (target = input shifted by 1) |
preTrainingOnunlabled.cpp |
src/TrainedGpt/preTrainingOnunlabled.cpp |
Full pre-training driver: data loaders, cross-entropy loss, AdamW training loop, loss evaluation, temperature/top-k sampling |
The test kv_caching in kv_cache.cpp implements a C++ port of generate_text_simple_cached:
torch::Tensor generateTextSimpleCached(
GptModelV2& model,
torch::Tensor idx,
int max_new_tokens,
int context_size = 0,
bool use_cache = true
);How it works:
- Prefill phase — The input prompt is processed in chunks of
kv_window_size, filling the cache incrementally (handles prompts longer than the window). - max_generable cap — The number of new tokens is limited to
ctx_len - input_lengthbecause of the fixed-size (learned) position embedding. - Cached decode loop — Each step feeds only the single newly-predicted token
[batch, 1]into the model withuse_cache=true. The attention layers re-read the growingcache_k/cache_vinstead of recomputing keys/values for the entire context — this is the standard optimization used by inference engines like llama.cpp and vLLM.
The KV cache itself (in MultiHeadAttentionV2) maintains:
cache_k,cache_v— buffers of shape[batch, heads, window_size, head_dim]current_ptr— how many valid slots are filled- Overflow handling — when a new chunk doesn't fit, the oldest tokens are discarded (sliding-window behavior)
- Offset causal mask — when serving a partial attention matrix (cached path), the mask is offset by
K - num_tokensso newer query rows can attend to all past (cached) keys correctly
| Variant | KV Heads | KV-cache memory | Notes |
|---|---|---|---|
| Multi-Head Attention (MHA) | numHeads |
numHeads × head_dim per token |
Baseline, full expressiveness |
| Group Query Attention (GQA) | numKvGroups |
numKvGroups × head_dim per token |
Greatly reduced cache; small accuracy cost. Groups of query heads share one K/V head. |
| Multi-Head Latent Attention (MLA) | latent (compressed) | low-rank compressed per token | Even smaller cache than GQA via latent K/V compression |
Pre-training is driven from src/TrainedGpt/preTrainingOnunlabled.cpp using the gpt2mhl (Multi-Head Latent Attention) model. The pipeline mirrors the classic GPT-2 "from scratch" training recipe:
- Data split — the corpus (e.g.
the-verdict.txt) is split into train/validation sets (configurable ratio, e.g. 90/10 or 70/30). - DataLoaders —
createDataLoaderV2builds[batch, seq_len]batches with a sliding-window stride; targets are inputs shifted by one token. - Loss —
cal_loss_batchcomputes cross-entropy on the flattened logits vs targets;total_loss_loaderaverages over a loader. - Optimizer — AdamW (
lr = 4e-4,weight_decay = 0.1); gradients zeroed withset_to_none=trueto free memory; CUDA cache emptied periodically. - Evaluation — every
eval_freqsteps the model is evaluated on both train and validation loaders, and a sample is generated to visually track progress.
generate_with_temperature supports:
- Greedy decoding (default)
- Top-k filtering — keep only the
top_kmost likely logits before softmax - Temperature scaling —
logits / temperaturebefore softmax for diversity control - EOS early stop — stops generation when an optional end-of-sequence token is sampled
The randomly-initialized model must be pre-trained before it produces coherent text. You can download the GPT-2 small (124M) pretrained weights (PyTorch format) trained from scratch:
https://huggingface.co/rasbt/gpt2-from-scratch-pytorch/resolve/main/gpt2-small-124M.pth
Save it as models/gpt2-small-124M.pth (or your preferred path) and load it into the model with torch::load before inference/training:
torch::load(model, "models/gpt2-small-124M.pth");Note: These weights are in PyTorch
.pthformat. When loading into the C++ model, ensure the state-dict keys and shapes match the architecture (MHA vs GQA vs MLA variants have different KV projection shapes).
| Dependency | Version / Notes |
|---|---|
| CMake | ≥ 3.20 |
| C++ Compiler | GCC ≥ 10, Clang ≥ 12 (C++20) |
| LibTorch | 2.x (download from pytorch.org) |
| vcpkg | For fmt, pcre2, curl |
| CUDA Toolkit | ≥ 11.x (for GPU acceleration) |
| libutfcpp-dev | UTF-8 library for tiktoken (sudo apt install libutfcpp-dev) |
| libpcre2-dev | Regex library for tiktoken (sudo apt install libpcre2-dev) |
LibTorch is excluded from git (see .gitignore) because it's several gigabytes. You need to download it manually:
# 1. Go to https://pytorch.org/get-started/locally/
# Select: Linux → LibTorch → C++ / Java → (stable / CUDA)
# 2. Download and extract into external/
cd external/
wget https://download.pytorch.org/libtorch/cu121/libtorch-cxx11-abi-shared-with-deps-2.1.0%2Bcu121.zip
unzip libtorch-cxx11-abi-shared-with-deps-2.1.0+cu121.zip
# Now external/libtorch/ exists
# 3. Verify the directory structure:
ls external/libtorch/
# → include/ lib/ share/# Install vcpkg first if you haven't already
git clone https://github.com/microsoft/vcpkg.git ~/vcpkg
~/vcpkg/bootstrap-vcpkg.sh
# Install required packages
~/vcpkg/vcpkg install fmt pcre2 curl
# Build the project
mkdir -p build && cd build
cmake .. -DCMAKE_TOOLCHAIN_FILE=$HOME/vcpkg/scripts/buildsystems/vcpkg.cmake
make -j$(nproc)- Open the project in CLion
- Go to Settings → Build, Execution, Deployment → CMake
- Set CMake options:
-DCMAKE_TOOLCHAIN_FILE=/home/youruser/vcpkg/scripts/buildsystems/vcpkg.cmake - Build (Ctrl+F9) and run (Shift+F10)
The C++ port of OpenAI's tiktoken (tikteken.cpp) relies on two system libraries that are not managed by CMake's find_package by default:
Tiktoken uses PCRE2 for regex-based token splitting (e.g., gpt2 / cl100k regex patterns).
The Problem: The original CMakeLists tried find_package(PCRE2 CONFIG) which fails unless PCRE2 was built with CMake exports.
The Fix: Use pkg-config to locate PCRE2:
find_package(PkgConfig REQUIRED)
pkg_check_modules(PCRE2 REQUIRED libpcre2-8)
if(NOT TARGET PCRE2::8BIT)
add_library(PCRE2::8BIT INTERFACE IMPORTED)
target_include_directories(PCRE2::8BIT INTERFACE ${PCRE2_INCLUDE_DIRS})
target_link_libraries(PCRE2::8BIT INTERFACE ${PCRE2_LIBRARIES})
endif()Install PCRE2 with:
sudo apt install libpcre2-dev
# or with vcpkg: vcpkg install pcre2Tiktoken needs UTF-8 decoding/encoding for handling Unicode characters in tokenized text.
The Problem: find_package(utf8cpp) fails because different distros package it under different CMake target names.
The Fix: Manually create an imported target pointing to the system include path:
if(NOT TARGET utf8cpp::utf8cpp)
add_library(utf8cpp::utf8cpp INTERFACE IMPORTED)
target_include_directories(utf8cpp::utf8cpp INTERFACE /usr/include/utf8cpp)
endif()Install utf8cpp with:
sudo apt install libutfcpp-devThe tiktoken C++ library is included as a git submodule so its version is pinned:
# When cloning for the first time:
git clone --recurse-submodules https://github.com/Moin2002-tech/LargeLanguageModelcpp.git
# If you already cloned without --recurse-submodules:
git submodule update --init --recursiveThe top-level CMakeLists.txt adds it with:
add_subdirectory(external/tiktoken)
target_link_libraries(LargeLanguageModelcpp PRIVATE tiktoken)- Linear projections:
Q = X @ W_q,K = X @ W_k,V = X @ W_v - Score computation:
scores = Q @ K^T - Scale & Softmax:
weights = softmax(scores / sqrt(d_out), dim=1) - Context vector:
output = weights @ V
cd build && make
./LargeLanguageModelcppThis runs the doctest test suite which prints attention scores, softmax outputs, context vectors, the full 6×6 attention score matrix, and the pre-training/loss-evaluation diagnostics.
MIT — feel free to use, modify, and learn from this code.