⚡ Andrej Karpathy 出品

NanoChat 完整教程

仅需 $100 即可训练你自己的 ChatGPT

🚀 端到端训练 💻 8XH100 GPU 📦 单文件实现 ⚡ 4小时完成

🎯 NanoChat 是什么?

NanoChat 是由AI领域知名专家 Andrej Karpathy 开发的完整LLM训练框架。它将从分词、预训练、微调、评估到Web部署的整个流程浓缩在一个 干净、最小化、可hack的代码库中。

最大的亮点:仅需约$100和4小时,你就能在单个8XH100节点上 训练出属于自己的ChatGPT!

✅ 核心特点

  • 端到端:覆盖完整LLM生命周期
  • 代码简洁:~8K行代码,45个文件
  • 依赖极少:uv.lock仅2004行
  • 性能优秀:可达GPT-2级别
  • 成本可控:$100起步,$1000达到高性能

🎓 学习价值

  • • 理解完整LLM训练流程
  • • 掌握Tokenization原理
  • • 学习预训练+微调技巧
  • • 了解RLHF强化学习
  • • 实践模型评估方法

📊 项目规模

333K
字符数
8.3K
代码行数
45
文件数
~83K
Token数
29K
GitHub Stars

🚀 快速开始指南

1

环境准备

硬件需求:

  • 推荐:8XH100 80GB GPU(Lambda Labs租用约$100)
  • 兼容:8XA100 80GB(稍慢但可用)
  • 最低:单GPU(需调整batch_size,训练时间x8)
# 克隆仓库
git clone https://github.com/karpathy/nanochat.git
cd nanochat

# 安装依赖(使用uv包管理器,快速且现代化)
pip install uv
uv sync

# 或使用传统pip
pip install -e .
2

一键运行完整流程

NanoChat提供了speedrun.sh脚本, 一键完成从数据准备到模型部署的全部流程(约4小时):

# 运行完整训练流程(speedrun模式,$100预算)
bash speedrun.sh

# 流程包括:
# 1. 下载训练数据(FineWeb)
# 2. 预训练(Base Model)
# 3. 中期训练(Mid Training)
# 4. 监督微调(SFT)
# 5. 强化学习(RL/RLHF)
# 6. 模型评估
# 7. 启动Web服务

预期结果:

  • ✓ 总训练时间:约3小时51分钟
  • ✓ 总成本:约$100(Lambda Labs 8XH100)
  • ✓ 模型性能:4e19 FLOPs(类似幼儿园水平😊)
  • ✓ 自动生成report.md评估报告
3

与你的LLM对话

训练完成后,启动Web界面与模型交互:

# 启动Web服务
python -m scripts.chat_web

# 输出示例:
# * Running on http://0.0.0.0:8000
# 访问:http://your-server-ip:8000

💬 测试对话:

  • • "讲个故事吧" - 测试创意生成
  • • "你是谁?" - 观察模型幻觉
  • • "为什么天空是蓝色的?" - 测试知识问答
  • • "写一首诗" - 测试文学创作
4

查看评估报告

# 查看完整评估报告
cat report.md

# 报告包含:
# - 各阶段训练指标
# - 多个benchmark评估结果
# - 训练时长和成本统计

示例评估表格:

Metric BASE MID SFT RL
CORE 0.2219 - - -
GSM8K - 0.0250 0.0455 0.0758
HumanEval - 0.0671 0.0854 -

🏗️ 架构设计

完整训练流水线

数据准备
下载FineWeb数据集 → 使用RustBPE分词器处理
预训练
Base Model训练 → 学习语言基础(语法、词汇、常识)
中期训练
Mid Training → 在高质量数据上继续训练
监督微调
SFT → 学习对话格式(SmolTalk数据集)
强化学习
RLHF → 通过人类反馈优化回答质量
评估部署
多维度评估 → Web服务部署 → 实际使用

📁 项目结构

nanochat/
├── nanochat/           # 核心代码
│   ├── model.py       # Transformer模型
│   ├── dataset.py     # 数据加载
│   └── utils.py       # 工具函数
├── rustbpe/           # Rust实现的BPE分词器
├── scripts/           # 训练脚本
│   ├── base_train.py  # 预训练
│   ├── mid_train.py   # 中期训练
│   ├── sft_train.py   # 监督微调
│   ├── rl_train.py    # 强化学习
│   └── chat_web.py    # Web服务
├── tasks/             # 评估任务
├── speedrun.sh        # 一键运行脚本
└── pyproject.toml     # 依赖配置

🧩 核心组件

🔤

RustBPE

高性能Rust分词器,速度快、内存占用低

🧠

Transformer

标准decoder-only架构,支持可变深度

📊

评估系统

支持CORE、GSM8K、HumanEval等多个benchmark

🌐

Web界面

简洁的HTML+CSS对话界面

🔍 核心代码解读

核心1 Transformer模型实现

标准的decoder-only Transformer架构,代码极简且高效:

# nanochat/model.py (简化版)
import torch
import torch.nn as nn
from torch.nn import functional as F

class CausalSelfAttention(nn.Module):
    """因果自注意力层"""
    def __init__(self, config):
        super().__init__()
        # Q, K, V投影
        self.c_attn = nn.Linear(config.n_embd, 3 * config.n_embd)
        # 输出投影
        self.c_proj = nn.Linear(config.n_embd, config.n_embd)
        # Causal mask
        self.register_buffer("bias", torch.tril(
            torch.ones(config.block_size, config.block_size)
        ).view(1, 1, config.block_size, config.block_size))
        
    def forward(self, x):
        B, T, C = x.size()  # batch, seq_len, embedding_dim
        
        # 计算Q, K, V
        qkv = self.c_attn(x)
        q, k, v = qkv.split(self.n_embd, dim=2)
        
        # 多头注意力
        k = k.view(B, T, self.n_head, C // self.n_head).transpose(1, 2)
        q = q.view(B, T, self.n_head, C // self.n_head).transpose(1, 2)
        v = v.view(B, T, self.n_head, C // self.n_head).transpose(1, 2)
        
        # 注意力分数(Flash Attention优化)
        att = F.scaled_dot_product_attention(q, k, v, is_causal=True)
        
        # 合并多头
        y = att.transpose(1, 2).contiguous().view(B, T, C)
        return self.c_proj(y)

class Block(nn.Module):
    """Transformer Block"""
    def __init__(self, config):
        super().__init__()
        self.ln_1 = nn.LayerNorm(config.n_embd)
        self.attn = CausalSelfAttention(config)
        self.ln_2 = nn.LayerNorm(config.n_embd)
        self.mlp = nn.Sequential(
            nn.Linear(config.n_embd, 4 * config.n_embd),
            nn.GELU(),
            nn.Linear(4 * config.n_embd, config.n_embd),
        )
        
    def forward(self, x):
        # Pre-norm架构
        x = x + self.attn(self.ln_1(x))
        x = x + self.mlp(self.ln_2(x))
        return x

关键设计:

  • Pre-norm:LayerNorm在attention/MLP之前,训练更稳定
  • Flash Attention:使用PyTorch 2.0的优化实现,速度快2-3倍
  • 因果mask:确保只能看到之前的token,实现自回归生成
  • 可配置深度:通过--depth参数调整模型大小

核心2 数据加载与处理

# nanochat/dataset.py (简化版)
class DataLoader:
    def __init__(self, split, batch_size, seq_len, device):
        self.batch_size = batch_size
        self.seq_len = seq_len
        self.device = device
        
        # 加载数据shard
        self.shards = self._load_shards(split)
        self.reset()
    
    def _load_shards(self, split):
        """从磁盘加载预处理的数据文件"""
        data_dir = Path("data") / split
        shards = sorted(data_dir.glob("*.npy"))
        return shards
    
    def reset(self):
        """重置迭代器"""
        self.current_shard = 0
        self.current_position = 0
        self.tokens = np.load(self.shards[self.current_shard])
    
    def next_batch(self):
        """获取下一个batch"""
        B, T = self.batch_size, self.seq_len
        buf = self.tokens[self.current_position : self.current_position + B*T + 1]
        
        # X: 输入序列,Y: 目标序列(右移1位)
        x = torch.tensor(buf[:-1], dtype=torch.long).view(B, T)
        y = torch.tensor(buf[1:], dtype=torch.long).view(B, T)
        
        # 移动到GPU
        x, y = x.to(self.device), y.to(self.device)
        
        # 更新位置
        self.current_position += B * T
        if self.current_position + B*T + 1 > len(self.tokens):
            self._load_next_shard()
        
        return x, y

优化要点:

  • Shard分片:数据分成多个文件,避免一次加载全部到内存
  • 预分词:训练前完成分词,避免训练时重复计算
  • numpy存储:使用.npy格式,加载速度快
  • 连续采样:按顺序读取,提高缓存命中率

核心3 RustBPE 分词器

使用Rust实现的BPE(Byte Pair Encoding)分词器,性能优异:

为什么选Rust?

  • • 速度快:比Python快10-100倍
  • • 内存安全:编译时保证无内存泄漏
  • • 并行化:轻松利用多核CPU
  • • 部署方便:编译成二进制,无运行时依赖

BPE原理

  • 1. 从字符级别开始
  • 2. 统计相邻字符对频率
  • 3. 合并最高频的字符对
  • 4. 重复直到达到词表大小
# Python调用Rust分词器
from nanochat.rustbpe import RustBPE

# 加载或训练分词器
tokenizer = RustBPE(vocab_size=50257)  # GPT-2词表大小
tokenizer.train(texts, verbose=True)

# 编码
text = "Hello, world!"
tokens = tokenizer.encode(text)  # [15496, 11, 995, 0]

# 解码
decoded = tokenizer.decode(tokens)  # "Hello, world!"

⚙️ 详细训练流程

阶段1:预训练(Base Model)

目标:学习语言的基础知识和模式

训练配置

# scripts/base_train.py
torchrun --standalone --nproc_per_node=8 \
  -m scripts.base_train \
  --depth=14 \              # 模型深度
  --device_batch_size=32 \  # 每GPU的batch size
  --total_batch_size=1024 \ # 总batch size
  --max_steps=10000         # 训练步数

数据来源

  • FineWeb-edu:HuggingFace的高质量网页数据

    过滤后的教育类内容,质量高

  • 数据量:根据模型大小自动计算

    参数×20=tokens,tokens×4.8=chars

关键技巧:

学习率调度

Warmup + Cosine Decay

梯度裁剪

防止梯度爆炸,clip_norm=1.0

混合精度

BF16加速训练,节省显存

阶段2:中期训练(Mid Training)

目标:在更高质量数据上继续训练

torchrun --standalone --nproc_per_node=8 \
  -m scripts.mid_train \
  --device_batch_size=32 \
  --max_steps=2000          # 较少步数,避免过拟合

为什么需要中期训练?

  • • 使用更精选的高质量数据
  • • 弥合预训练和对话微调之间的gap
  • • 提升模型在特定领域的表现

阶段3:监督微调(SFT)

目标:学习对话格式和交互模式

训练数据格式

# SmolTalk数据集格式
{
  "messages": [
    {"role": "user", "content": "你好"},
    {"role": "assistant", "content": "你好!有什么我可以帮助你的吗?"}
  ]
}

Loss计算策略

# 只计算assistant回复的loss
mask = (labels != -100)
loss = F.cross_entropy(
    logits.view(-1, vocab_size),
    labels.view(-1),
    reduction='none'
)
loss = (loss * mask.view(-1)).sum() / mask.sum()

阶段4:强化学习(RLHF)

目标:通过奖励信号优化输出质量

⚠️ 注意

RLHF是最具挑战性的阶段,需要仔细调参。NanoChat实现了简化版的RLHF, 使用GSM8K数学题作为奖励信号。

# 简化的RLHF流程
1. 生成回答:模型对问题生成答案
2. 计算奖励:检查答案是否正确(0或1)
3. 策略梯度:使用REINFORCE算法更新参数
4. KL散度约束:防止模型偏离SFT模型太远

# 奖励函数示例(GSM8K)
def compute_reward(question, answer, ground_truth):
    predicted = extract_number(answer)
    correct = extract_number(ground_truth)
    return 1.0 if predicted == correct else 0.0

🚀 模型部署与使用

🌐 Web对话界面

# 启动Web服务
python -m scripts.chat_web --model_path ./models/final.pt

# 服务配置
# - Host: 0.0.0.0(允许外部访问)
# - Port: 8000
# - 自动加载最新checkpoint

界面截图示例:

你好!请讲一个关于AI的故事
从前,有一个小AI叫做Nano,它虽然只接受了4小时的训练, 但充满了好奇心和学习的热情...

💻 命令行推理

# Python API调用
from nanochat import load_model, generate

# 加载模型
model = load_model("./models/final.pt")

# 生成文本
prompt = "Why is the sky blue?"
response = generate(
    model,
    prompt,
    max_length=200,
    temperature=0.8,
    top_k=50
)

print(response)

⚡ 推理性能优化

KV Cache

缓存已计算的key和value,避免重复计算。 对于长文本生成,速度提升10-100倍。

INT8量化

将FP16权重量化为INT8,模型大小减半, 推理速度提升2-3倍,精度损失<1%。

批处理

多个请求批量处理,提高GPU利用率。 适合服务端部署,吞吐量提升5-10倍。

Torch.compile

PyTorch 2.0编译优化,自动融合算子。 首次编译慢,但后续推理快30-50%。

🎓 进阶优化技巧

📈 训练更大的模型

d26模型($300,12小时)

# 修改speedrun.sh
--depth=26 \
--device_batch_size=16  # 减半避免OOM

性能略超GPT-2,适合实际应用

$1000级模型(42小时)

进一步增加depth和训练数据,性能显著提升

💾 显存不足的解决方案

  • 1.
    减小batch size

    从32→16→8→4→2→1

  • 2.
    梯度累积

    代码自动补偿,将并行变串行

  • 3.
    梯度检查点

    牺牲速度换显存,可节省50%

  • 4.
    减小序列长度

    从1024→512→256

💻 在MacBook上运行

# 使用MPS加速(Apple Silicon)
python -m scripts.base_train \
  --device_type=mps \
  --depth=6 \          # 减小模型
  --device_batch_size=1 \
  --max_steps=1000     # 减少步数

⚠️ CPU/MPS训练速度很慢,仅适合学习和实验小模型

📊 自定义评估任务

# 添加新的评估任务
# tasks/my_task.py

class MyTask:
    def __init__(self):
        self.name = "my_task"
        
    def evaluate(self, model):
        # 实现评估逻辑
        correct = 0
        total = len(self.test_data)
        
        for example in self.test_data:
            pred = model.generate(example.prompt)
            if self.check(pred, example.answer):
                correct += 1
        
        return {"accuracy": correct / total}

📚 学习资源

💡 推荐学习路径

1. 运行speedrun.sh 2. 阅读核心代码 3. 修改超参数实验 4. 训练更大模型

🎯 开始你的LLM之旅

NanoChat是学习LLM的最佳起点 - 代码简洁、流程完整、成本可控

💰

$100起步

人人可负担

⏱️

4小时完成

快速迭代

📦

8K行代码

易于理解

🚀

端到端

完整流程

"The best ChatGPT that $100 can buy" - Andrej Karpathy