仅需 $100 即可训练你自己的 ChatGPT
NanoChat 是由AI领域知名专家 Andrej Karpathy 开发的完整LLM训练框架。它将从分词、预训练、微调、评估到Web部署的整个流程浓缩在一个 干净、最小化、可hack的代码库中。
最大的亮点:仅需约$100和4小时,你就能在单个8XH100节点上 训练出属于自己的ChatGPT!
硬件需求:
# 克隆仓库 git clone https://github.com/karpathy/nanochat.git cd nanochat # 安装依赖(使用uv包管理器,快速且现代化) pip install uv uv sync # 或使用传统pip pip install -e .
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服务
预期结果:
训练完成后,启动Web界面与模型交互:
# 启动Web服务 python -m scripts.chat_web # 输出示例: # * Running on http://0.0.0.0:8000 # 访问:http://your-server-ip:8000
💬 测试对话:
# 查看完整评估报告 cat report.md # 报告包含: # - 各阶段训练指标 # - 多个benchmark评估结果 # - 训练时长和成本统计
示例评估表格:
| Metric | BASE | MID | SFT | RL |
|---|---|---|---|---|
| CORE | 0.2219 | - | - | - |
| GSM8K | - | 0.0250 | 0.0455 | 0.0758 |
| HumanEval | - | 0.0671 | 0.0854 | - |
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对话界面
标准的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
关键设计:
# 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
优化要点:
使用Rust实现的BPE(Byte Pair Encoding)分词器,性能优异:
为什么选Rust?
BPE原理
# 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!"
目标:学习语言的基础知识和模式
# 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 # 训练步数
过滤后的教育类内容,质量高
参数×20=tokens,tokens×4.8=chars
关键技巧:
学习率调度
Warmup + Cosine Decay
梯度裁剪
防止梯度爆炸,clip_norm=1.0
混合精度
BF16加速训练,节省显存
目标:在更高质量数据上继续训练
torchrun --standalone --nproc_per_node=8 \ -m scripts.mid_train \ --device_batch_size=32 \ --max_steps=2000 # 较少步数,避免过拟合
为什么需要中期训练?
目标:学习对话格式和交互模式
# SmolTalk数据集格式
{
"messages": [
{"role": "user", "content": "你好"},
{"role": "assistant", "content": "你好!有什么我可以帮助你的吗?"}
]
}
# 只计算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()
目标:通过奖励信号优化输出质量
⚠️ 注意
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服务 python -m scripts.chat_web --model_path ./models/final.pt # 服务配置 # - Host: 0.0.0.0(允许外部访问) # - Port: 8000 # - 自动加载最新checkpoint
界面截图示例:
# 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和训练数据,性能显著提升
从32→16→8→4→2→1
代码自动补偿,将并行变串行
牺牲速度换显存,可节省50%
从1024→512→256
# 使用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}
NanoChat是学习LLM的最佳起点 - 代码简洁、流程完整、成本可控
$100起步
人人可负担
4小时完成
快速迭代
8K行代码
易于理解
端到端
完整流程
"The best ChatGPT that $100 can buy" - Andrej Karpathy