nanochat

repository·master·Indexed 13 days ago

https://github.com/karpathy/nanochat

A minimal, hackable experimental harness for training LLMs on a single GPU node. It covers the full lifecycle from tokenization and pretraining to finetuning and inference, featuring a compute-optimal scaling system controlled by a single '--depth' parameter. Version 0.1.0.

Tokens
18.3K
Snippets
55
Records
75
Agent score
99%

What's inside nanochat

  1. Understand the nanochat file structure

    master

    The nanochat repository is organized into several functional directories that separate core logic, training scripts, evaluation tasks, and execution scripts:

    • nanochat/: The core library containing the model architecture (gpt.py), inference engine (engine.py), optimizer (optim.py), tokenizer (tokenizer.py), and data utilities (dataloader.py, dataset.py).
    • scripts/: High-level entry points for users, including chat_cli.py for interactive chat, base_train.py for pretraining, chat_sft.py for supervised fine-tuning, and infer_bench.py for performance benchmarking.
    • tasks/: Definitions for evaluation datasets and task mixtures (e.g., arc.py, gsm8k.py, mmlu.py).
    • runs/: Shell scripts for reproducible experiments, such as speedrun.sh for fast training or runcpu.sh for CPU/MPS execution.
    • tests/: Unit tests for core components like the inference engine, optimizer, and tokenizer.
  2. Muon Optimizer Features and Implementation Details

    master

    The Muon optimizer in nanochat includes several advanced features for orthogonalization and variance reduction:

    1. Polar Express Orthogonalization: Uses the "Polar Express Sign Method" for orthogonalization instead of standard Newton-Schulz iteration. This is the default method.
    2. NorMuon Variance Reduction: Implements per-neuron/column adaptive learning rates. It maintains a second_momentum_buffer (shape [rows, 1] or [1, cols]) to normalize updates based on a running variance estimate ($\beta_2=0.95$). This has negligible memory overhead (~$1/\max(rows, cols)$ per parameter).
    3. Cautious Weight Decay: (See Configure Muon Optimizer Weight Decay)
  3. Configure model complexity using --depth

    master

    nanochat is designed around a single complexity dial: --depth. This integer represents the number of layers in the GPT transformer model.

    Setting --depth automatically calculates all other hyperparameters (transformer width, number of heads, learning rate adjustments, training horizons, weight decays, etc.) to ensure the model is compute-optimal.

    • GPT-2 capability: Occurs at approximately depth 26 (d24-d26 range).
    • GPT-1 size: Approximately depth 12.

    Example for a quick 12-layer experimentation run:

    OMP_NUM_THREADS=1 torchrun --standalone --nproc_per_node=8 -m scripts.base_train -- \
        --depth=12 \
        --run="d12" \
        --model-tag="d12" \
        --core-metric-every=999999 \
        --sample-every=-1 \
        --save-every=-1
    torchrun --standalone --nproc_per_node=8 -m scripts.base_train -- --depth=12
  4. Use the BestFit-Crop BOS-aligned dataloader

    master

    The project uses a BOS-aligned dataloader to ensure every sequence in a batch starts with a Beginning-of-Sequence (BOS) token. This improves training quality by providing proper context for every document.

    Two implementation strategies exist:

    1. Original (Simple): High efficiency and 100% token utilization, but some documents may start mid-stream without context.
    2. _bos_bestfit (BestFit-Crop): The new default. It uses a bin-packing algorithm to pick the largest documents that fit into a row, reducing the number of 'unlucky' crops. While it achieves 100% utilization (no padding), it discards approximately 34% of tokens when documents do not fit perfectly into the sequence length.

    Note on Loss Scaling: Switching to a BOS-aligned dataloader will result in lower validation loss compared to non-BOS loaders. This is because the model is no longer being trained on 'confusing' tokens that lack proper context. Absolute loss values are not directly comparable across different dataloader implementations.

  5. Manage precision and dtypes via COMPUTE_DTYPE

    master

    Nanochat uses explicit dtype management instead of torch.amp.autocast to provide fine-grained control over precision.

    • Automatic Detection: COMPUTE_DTYPE is automatically determined based on hardware:
      • SM 80+ (Ampere and later): torch.bfloat16
      • Pre-Ampere: torch.float32
      • CPU/MPS: torch.float32
    • Manual Override: You can override the automatic detection by setting the NANOCHAT_DTYPE environment variable.
    • Implementation Detail: The codebase uses a custom Linear class (inheriting from nn.Linear) that casts weights to match the input dtype during the forward pass: F.linear(x, self.weight.to(dtype=x.dtype)). This replaces the need for autocast.
    export NANOCHAT_DTYPE=torch.float16
  6. Use Value Embeddings (VEs) to increase model capacity

    master

    Value Embeddings (VEs) are a highly effective way to add significant parameter capacity to a model with almost zero additional FLOP cost, as they are simply added to the Values tensor.

    Best Practices for VEs:

    • Placement: Use VEs at every layer or at alternating layers. Experiments show that alternating layers work best.
    • Capacity: Do not attempt to reduce capacity via low-rank decompositions, parameter sharing, or projections; the model performs better with high-capacity, full-rank embedding tables.
    • Gating: Using a gate to control the influence of VEs is beneficial.
    • Scaling Impact: Adding large amounts of VEs makes the model 'parameter bloated,' which shifts the optimal tokens-to-parameters ratio significantly lower (e.g., from 8 down to 4).
  7. Configure SFT training with chat_sft.py

    master

    The chat_sft.py script is used for Supervised Fine-Tuning (SFT). It has been updated to match the capabilities of base_train.py and includes several specific tuning options:

    • Optimizer Warm-start: Use --load-optimizer=1 (default) to load pretrained momentum buffers via load_optimizer_state() in checkpoint_manager.py. Note that Learning Rates (LRs) are reset to fresh SFT values after loading.
    • LR Schedule: Uses a warmup/constant/warmdown schedule. Key flags include --warmup-ratio, --warmdown-ratio, --init-lr-frac, and --final-lr-frac. A warmdown_ratio of 0.5 is recommended.
    • Data Mixture Tuning: You can configure the number of epochs for specific datasets using --mmlu-epochs and --gsm8k-epochs.
    • Evaluation: Periodic evaluation across 6 tasks is performed using --chatcore-every=N (e.g., --chatcore-every=200), with results logged to wandb.
    • Hyperparameter Inheritance: By default, SFT inherits batch sizes and LRs from the pretrained checkpoint metadata, though CLI overrides remain functional.
    # Example SFT command with custom epoch counts and evaluation frequency
    python chat_sft.py --mmlu-epochs 3 --gsm8k-epochs 4 --chatcore-every 200 --load-optimizer=1
  8. Implement Bigram Hash Embeddings (Engram-lite)

    master

    To improve model performance by offloading static N-gram patterns to a lookup table, implement an 'engram-lite' module. This approach uses a simple hash of bigrams and adds the resulting embeddings directly to the residual stream at every layer, gated by a per-layer learnable lambda ($\lambda$).

    Implementation Details:

    • Hash Function: (36313 * curr) XOR (27191 * prev) mod table_size
    • Embedding Table: Use a zero-initialized embedding table to ensure it starts as an identity mapping.
    • Injection: Add to the residual stream at every layer using a learned lambda (initial value of 0.1 is recommended over 0.0).
    • Optimal Hyperparameters:
      • Table Size: vocab_size * 5 (e.g., ~164K entries for a 32K vocab).
      • Optimizer: Use AdamW with the same learning rate as the token embeddings.

    Note: Avoid complex context-aware gating (like sigmoid dot-product gates) or restricting injection to early layers only, as simple direct addition to the residual stream has been shown to perform better.

    class BigramEmbed(nn.Module):
        def __init__(self, vocab_size, embed_dim, table_multiplier=5):
            self.embed = nn.Embedding(vocab_size * table_multiplier, embed_dim)
    
        def forward(self, idx):
            # idx is the token indices
            # h is the hashed bigram index
            h = (36313 * idx[:, 1:]) ^ (27191 * idx[:, :-1]) % (table_size - 1)
            return self.embed(h)
  9. Enable Per-Layer Residual Scalars (x0 & resid lambdas)

    master

    Nanochat includes learnable per-layer residual connections to improve model performance with minimal compute overhead. This is controlled via the --scalar_lr CLI argument (default 0.5).

    Mechanisms

    1. x0_lambdas (x0 residual connections): Blends the initial normalized embedding (x0) back into each layer: x = resid_lambdas[i] * x + x0_lambdas[i] * x0. This provides a direct path from the embedding to deep layers.
    2. resid_lambdas (residual stream scaling): A multiplicative scaling of the residual stream at each layer, initialized to 1.0.

    Learning Rate Sensitivity

    These two scalar types require different learning rates to train effectively:

    • x0_lambdas: Use the standard --scalar_lr (e.g., 0.5).
    • resid_lambdas: Require a much smaller learning rate, approximately 100x smaller than the x0_lambdas (e.g., scalar_lr * 0.01).

    Implementation Warning

    When implementing custom layers, remember that __init__ runs in a meta device context. Any tensor values set during __init__ are fake; actual values must be initialized in the init_weights() method.

    # Example CLI usage
    python train.py --scalar_lr 0.6
  10. Participate in the Time-to-GPT-2 Leaderboard

    master

    The nanochat leaderboard tracks the "time to GPT-2" metric: the wall clock time required to outperform the GPT-2 (1.6B) CORE metric (baseline: 0.256525) on an 8xH100 GPU node.

    To participate, you must:

    1. Achieve a CORE metric higher than 0.256525.
    2. Report the total_training_time in seconds (the time of training iterations, excluding evaluations and logging).
    3. Report the validation bpb (bits per byte) of your run.
    4. Ensure your improvement is principled and generalizes to other model depths.

    If you outperform the current SOTA, you can submit a Pull Request. Use git log -1 --format="%h" to get your commit hash for the submission.

  11. Prepare NVIDIA ClimbMix dataset for training

    master

    To use the NVIDIA ClimbMix dataset (which has shown significant gains over FineWeb-EDU), follow these steps to download shards and train the tokenizer:

    1. Download at least 150 data shards using the nanochat.dataset module.
    2. Train the tokenizer using scripts.tok_train.
    python -m nanochat.dataset -n 150
    python -m scripts.tok_train
  12. Use Flash Attention 3 for training and inference

    master

    Nanochat integrates Flash Attention 3 (FA3) to improve throughput. It replaces the standard PyTorch scaled_dot_product_attention (FA2).

    Key Features

    • Layout: Uses (B, T, H, D) layout directly, eliminating the need for transposes.
    • Training: Uses flash_attn.flash_attn_func(q, k, v, causal=True).
    • Inference: Uses flash_attn.flash_attn_with_kvcache() which handles all cache cases (prefill, single-token, chunk inference) in a single call.
    • GQA: Grouped Query Attention is handled automatically when n_kv_heads < n_heads.
    • Sliding Window: Supported via the window_size=(left, 0) parameter in FA3.

    KVCache Implementation

    Unlike the previous combined tensor format, the FA3 KVCache uses separate tensors for key and value caches with the shape (num_layers, B, T, H, D). Position is tracked via a cache_seqlens tensor (int32, per batch element).