Zero to Hero, лекция 9: воспроизводим GPT-2 (124M)
Подробный конспект четырёхчасовой лекции Карпатого «Let's reproduce GPT-2 (124M)» по этапам репозитория build-nanogpt: модель GPT-2 с нуля, ускорение обучения в 11 раз, гиперпараметры GPT-3, накопление градиентов, DDP на 8 GPU, FineWeb-Edu, HellaSwag — и как повторить это на 4× RTX 4080.
Коротко
Заголовок раздела «Коротко»- Цель — с пустого файла написать GPT-2 на 124 млн параметров, обучить её с нуля и обогнать опубликованный чекпоинт OpenAI. Сначала Карпатый загружает в свой класс веса OpenAI: если они встали, архитектура верна. Потом стирает веса и обучает модель сам.
- Вторая часть — инженерия скорости на GPU A100: TF32, bfloat16,
torch.compile, FlashAttention, «красивые» числа. Один шаг обучения ускоряется с 1000 мс до 93 мс, примерно в 11 раз, без изменения математики. - Третья часть — гиперпараметры из статьи о GPT-3: AdamW, клиппинг градиента, разогрев и косинус, weight decay, батч в 0,5 млн токенов через накопление градиентовнакопление градиентовEN gradient accumulationГрадиенты складываются (+=), а не перезаписываются. Внутри графа так учитываются все пути к узлу; на уровне обучения — шаг оптимизатора делают раз в несколько микробатчей. и параллелизм по даннымпараллелизм по даннымEN data parallelism (DDP) на 8 GPU.
- Данные — 10 млрд токенов FineWeb-Edu, оценка — loss на валидации и тест HellaSwag. За ≈2 часа на 8× A100 модель обгоняет GPT-2 124M по HellaSwag; за ночной прогон на 40 млрд токенов почти догоняет GPT-3 124M.
- Каждый коммит репозитория karpathy/build-nanogpt (MIT) — один шаг лекции. Их можно проходить по очереди.
Как смотреть вместе с курсом
Заголовок раздела «Как смотреть вместе с курсом»Карпатый сразу предупреждает: он предполагает, что вы видели «GPT с нуля» и лекцию о токенизаторе. В курсе это уроки 4.6.2 «GPT на PyTorch» и 4.6.3 «Цикл обучения LLM»: первый раздел лекции почти целиком повторяет первый урок, а третий — второй. Раздел 2 («сделаем быстро») — это ступень 5: смешанная точность, FlashAttention, распределённое обучение и практика на нескольких GPU.
Удобно разбить четыре часа на четыре вечера по разделам лекции. Первый раздел можно смотреть уже после модуля 4.6. Остальные — во время ступени 5, держа рядом свою машину с 4 GPU (см. раздел «На вашей машине» в конце).
Карта лекции
Заголовок раздела «Карта лекции»Таймкоды и названия глав — из описания видео.
| Время | Глава в оригинале | О чём |
|---|---|---|
| 0:00 | intro: Let’s reproduce GPT-2 (124M) | цель: обогнать GPT-2 124M |
| 3:39 | exploring the GPT-2 (124M) OpenAI checkpoint | веса OpenAI через Hugging Face |
| 13:47 | SECTION 1: implementing the GPT-2 nn.Module | свой класс GPT |
| 28:08 | loading the huggingface/GPT-2 parameters | загрузка весов в свой класс |
| 31:00 | implementing the forward pass to get logits | прямой проход |
| 33:31 | sampling init, prefix tokens, tokenization | подготовка к генерации |
| 37:02 | sampling loop | top-k-сэмплирование |
| 41:47 | sample, auto-detect the device | CPU, CUDA, MPS |
| 45:50 | let’s train: data batches (B,T) → logits (B,T,C) | батчи из текста |
| 52:53 | cross entropy loss | loss на старте ≈ 10,8 |
| 56:42 | optimization loop: overfit a single batch | переобучение одного батча |
| 1:02:00 | data loader lite | простой загрузчик данных |
| 1:06:14 | parameter sharing wte and lm_head | связанные веса |
| 1:13:47 | model initialization: std 0.02, residual init | инициализация |
| 1:22:18 | SECTION 2: Let’s make it fast. GPUs, mixed precision, 1000ms | базовая скорость |
| 1:28:14 | Tensor Cores, timing the code, TF32 precision, 333ms | TF32 |
| 1:39:38 | float16, gradient scalers, bfloat16, 300ms | bfloat16 |
| 1:48:15 | torch.compile, Python overhead, kernel fusion, 130ms | компиляция |
| 2:00:18 | flash attention, 96ms | FlashAttention |
| 2:06:54 | nice/ugly numbers. vocab size 50257 → 50304, 93ms | «красивые» размеры |
| 2:14:55 | SECTION 3: hyperpamaters, AdamW, gradient clipping | гиперпараметры GPT-3 |
| 2:21:06 | learning rate scheduler: warmup + cosine decay | расписание LR |
| 2:26:21 | batch size schedule, weight decay, FusedAdamW, 90ms | weight decay |
| 2:34:09 | gradient accumulation | батч 0,5 млн токенов |
| 2:46:52 | distributed data parallel (DDP) | 8 GPU |
| 3:10:21 | datasets used in GPT-2, GPT-3, FineWeb (EDU) | данные |
| 3:23:10 | validation data split, validation loss, sampling revive | валидация |
| 3:28:23 | evaluation: HellaSwag, starting the run | HellaSwag, запуск |
| 3:43:05 | SECTION 4: results in the morning! GPT-2, GPT-3 repro | результаты |
| 3:56:21 | shoutout to llm.c, equivalent but faster code in raw C/CUDA | llm.c |
| 3:59:39 | summary, phew, build-nanogpt github repo | итоги |
Код ниже — из итогового train_gpt2.py репозитория build-nanogpt (лицензия MIT), комментарии переведены и дополнены.
Вступление: что именно воспроизводим (0:00–13:47)
Заголовок раздела «Вступление: что именно воспроизводим (0:00–13:47)»У GPT-2 (2019) была «мини-серия» из четырёх моделей: от 124M до 1558M параметров. Самую большую обычно и называют GPT-2, а мы берём самую маленькую: 12 слоёв, ширина 768. (Карпатый замечает, что числа параметров в таблице статьи неверны — OpenAI признала ошибку в своём репозитории.) Тогда обучение такой модели было серьёзной задачей. Сегодня, по словам Карпатого, её можно воспроизвести примерно за час и около 10 долларов аренды облачного GPU.
Статья о GPT-2 скупа на детали обучения, поэтому гиперпараметры берутся из статьи о GPT-3: архитектура там почти та
же. Оригинальный код OpenAI написан на TensorFlow, поэтому эталон загружается через библиотеку Hugging Face
transformers (GPT2LMHeadModel.from_pretrained("gpt2") — это именно 124M; полная GPT-2 — это "gpt2-xl").
Что видно в весах:
wte— таблица эмбеддингов токенов 50257 × 768 (словарь GPT-2 из лекции о токенизаторе);wpe— таблица позиционных эмбеддингов 1024 × 768: контекст GPT-2 — 1024 токена. Если нарисовать её столбцы, видны волны, похожие на синусоиды. В оригинальном трансформере такие синусоиды задавались формулой, а GPT-2 сама выучивает их из случайных чисел. По «шершавости» кривых Карпатый делает вывод, что модель недообучена.
Генерация через pipeline с префиксом «Hello, I’m a language model,» даёт связный текст — это цель, к которой мы
придём уже со своими весами.
Раздел 1. GPT-2 своими руками (13:47–1:22:18)
Заголовок раздела «Раздел 1. GPT-2 своими руками (13:47–1:22:18)»Архитектура
Заголовок раздела «Архитектура»GPT-2 — трансформер «только декодер» (decoder-onlydecoder-only (только декодер)EN decoder-only architectureОдна стопка блоков с причинной маской, цель — следующий токен. Архитектура GPT, Llama, Qwen, DeepSeek, Kimi, GLM-4.5.): нет кодировщика и перекрёстного внимания. Ещё два отличия от «Attention Is All You Need»: LayerNorm стоит перед вниманием и MLP (pre-normPre-Norm (нормализация на входе подслоя)EN pre-normalization (Pre-LN)x + F(Norm(x)): нормализуется только вход ветки, остаточный путь чистый. Стандарт с GPT-2 (2019); требует финальной нормализации перед LM-головой.), и после последнего блока добавлен ещё один LayerNorm. Карпатый объясняет, почему pre-norm лучше: остаточный поток (residual stream) остаётся «чистой трубой» от выхода до входа, а сложение раздаёт градиент обеим ветвям поровну.
Ещё одна его формулировка, которую стоит запомнить: внимание — это операция общения токенов, «reduce» (взвешенная сумма по позициям), а MLP работает с каждым токеном отдельно, это «map». Трансформер — многократно повторённый map-reduce.
class Block(nn.Module): 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 = MLP(config)
def forward(self, x): x = x + self.attn(self.ln_1(x)) # общение токенов (reduce) x = x + self.mlp(self.ln_2(x)) # «размышление» каждого токена отдельно (map) return x
@dataclassclass GPTConfig: block_size: int = 1024 # максимальная длина последовательности vocab_size: int = 50257 # число токенов: 50 000 слияний BPE + 256 байтовых токенов + 1 токен <|endoftext|> n_layer: int = 12 # число слоёв n_head: int = 12 # число голов внимания n_embd: int = 768 # размерность эмбеддинговMLP — два линейных слоя с GELU между ними, причём в приближённой tanh-версии (nn.GELU(approximate='tanh')). Это
исторический артефакт: когда GELU придумали, точная функция медленно считалась в TensorFlow. Сегодня причин для
приближения нет, но GPT-2 использовала его, и мы повторяем. Внимание написано одним модулем: все головы считаются
параллельно, как дополнительное «батчевое» измерение. Имена слоёв (c_attn, c_proj, ln_1…) нарочно совпадают с
Hugging Face, чтобы веса загрузились один к одному. Тонкость загрузки: часть матриц OpenAI хранила в модуле Conv1D,
их приходится транспонировать.
В итоге модель — меньше 100 строк против примерно 2000 строк в modeling_gpt2.py у Hugging Face.
Генерация и устройство
Заголовок раздела «Генерация и устройство»Прямой проход: индексы токенов размера → эмбеддинги токенов и позиций (позиции одинаковы для всех строк
батча, срабатывает broadcasting) → 12 блоков → финальный LayerNorm → lm_head → логитылогитEN logitСтрого — логарифм шансов log(p / (1 − p)). В нейросетях логитами называют любые выходы до softmax или сигмоиды.
размера . Для генерации берутся логиты последней позиции, softmax, и
top-k-сэмплированиеtop-k сэмплированиеEN top-k samplingСэмплирование только из k самых вероятных токенов (остальные получают вероятность 0). с — так по умолчанию делает pipeline Hugging Face.
Точно совпасть с генерациями pipeline не удалось, но тот же код на модели Hugging Face даёт те же результаты, что и
на своей, — значит, веса загружены верно.
Код сам выбирает устройство: CUDA, если есть видеокарта NVIDIA, иначе MPS на Mac с Apple Silicon, иначе CPU. Первый раздел, по словам Карпатого, вполне можно пройти и на CPU.
Обучение на «Крошечном Шекспире»
Заголовок раздела «Обучение на «Крошечном Шекспире»»Батчи из длинной строки токенов: берём токенов, входы — все, кроме последнего, цели — все, кроме
первого, и .view(B, T). Лишний токен нужен, чтобы у последней позиции тоже была цель.
buf = tokens[pos : pos + B*T + 1]x = buf[:-1].view(B, T) # входыy = buf[1:].view(B, T) # цели: тот же текст, сдвинутый на один токенПроверка loss на старте. Если модель ещё ничего не знает, она должна давать всем 50 257 токенам примерно равные вероятности, и кросс-энтропияперекрёстная энтропияEN cross-entropy должна быть около . Получилось ≈ 11 — инициализация разумная. Читается так: «модель одинаково не уверена во всех токенах».
Дальше — классическая проверка: переобучитьпереобучениеEN overfitting один батч до почти нулевого loss
(AdamW, lr = 3e-4). Затем DataLoaderLite идёт по всему файлу: в Шекспире ≈ 338 тысяч токенов (миллион символов,
сжатие токенизатора примерно 3 к 1), за 50 шагов по батчам 4 × 32 loss падает примерно до 6,6. Быстрый выигрыш, по
словам Карпатого, — модель учится не предсказывать токены, которых в тексте вообще нет.
Связанные веса и инициализация
Заголовок раздела «Связанные веса и инициализация»Связывание весов (1:06:14). Матрицы wte (вход) и lm_head (выход) в чекпоинте OpenAI — это буквально один и
тот же тензор (совпадает даже указатель на память). Схема пришла из «Attention Is All You Need». Это
связывание весовсвязывание весов (общие эмбеддинги входа и выхода)EN weight tying (tied embeddings)Одна и та же матрица служит эмбеддингом токенов и LM head. Её градиент — сумма вкладов от обоих мест. В Hugging Face — флаг tie_word_embeddings. экономит млн параметров — около
30% модели — и даёт полезное предположение: похожие токены похожи и на входе, и на выходе.
self.transformer.wte.weight = self.lm_head.weight # один тензор используется дваждыИнициализация (1:13:47). Из кода OpenAI: веса — нормальное распределение со стандартным отклонением 0,02, смещения — нули. 0,02 примерно соответствует правилу для ширин GPT-2 (, ). Плюс поправка из статьи: веса слоёв, которые пишут в остаточный поток, умножаются на , где — число таких слоёв. Почему: если 100 раз прибавить к нулевому вектору случайный шум со стандартным отклонением 1, стандартное отклонение суммы вырастет до . Множитель возвращает его к 1. В каждом блоке в поток пишут двое — внимание и MLP, поэтому :
def _init_weights(self, module): if isinstance(module, nn.Linear): std = 0.02 if hasattr(module, 'NANOGPT_SCALE_INIT'): # c_proj внимания и MLP std *= (2 * self.config.n_layer) ** -0.5 # компенсируем рост дисперсии в потоке torch.nn.init.normal_(module.weight, mean=0.0, std=std) if module.bias is not None: torch.nn.init.zeros_(module.bias) elif isinstance(module, nn.Embedding): torch.nn.init.normal_(module.weight, mean=0.0, std=0.02)Раздел 2. Сделаем быстро (1:22:18–2:14:55)
Заголовок раздела «Раздел 2. Сделаем быстро (1:22:18–2:14:55)»Первое правило, по Карпатому: знать своё железо и выжимать из него всё. У него машина с 8 GPU A100 SXM 80 ГБ. Пока работаем на одной, батч 16 × 1024, время одного шага:
| Шаг | Что сделали | мс/шаг |
|---|---|---|
| Базовая линия | всё в float32 | 1000 |
| TF32 | torch.set_float32_matmul_precision('high') | 333 |
| bfloat16 | torch.autocast(device_type, dtype=torch.bfloat16) | 300 |
| Компиляция | model = torch.compile(model) | 130 |
| FlashAttention | F.scaled_dot_product_attention(q, k, v, is_causal=True) | 96 |
| «Красивые» числа | vocab_size 50257 → 50304 | 93 |
| Fused AdamW | torch.optim.AdamW(..., fused=True) (из раздела 3) | 90 |
Время — из названий глав видео. Пройдём по шагам.
TF32 (1:28:14). Тензорные ядратензорное ядроEN Tensor CoreБлок GPU, который за одну инструкцию умножает маленькие матрицы и прибавляет результат: D = AB + C. Работает в форматах TF32, BF16, FP16, FP8, INT8. У RTX 4080 их 304 (по 4 на SM), пик в BF16 — 97.5 TFLOPS против 48.7 у FP32 на CUDA-ядрах. A100 умеют умножать матрицы в формате TF32: те же 8 бит порядка, что у float32, но мантисса урезана до 10 бит. По спецификации это даёт 156 TFLOPS вместо 19,5 — обещано ускорение в 8 раз. Получили только в 3: большая часть времени уходит не на арифметику, а на перекладывание данных в памяти. Метрика, которой Карпатый советует верить, — токены в секунду: она не зависит от размера батча.
bfloat16 (1:39:38). float16 требует масштабирования градиентов (gradient scaler): у него узкий диапазон порядка.
bfloat16BF16 (bfloat16)EN bfloat16 (brain floating point)16-битный формат: 1 бит знака, 8 бит порядка (как у FP32), 7 бит мантиссы. Диапазон как у FP32, точность — 2–3 значащие цифры. Стандарт обучения LLM. сохраняет диапазон float32 ценой точности мантиссы, поэтому scaler не нужен. Через
autocast в bfloat16 переходят в основном умножения матриц, а чувствительные операции (softmax, нормализации, loss)
остаются в float32. Подробно — в уроке 5.4.1.
torch.compile (1:48:15). Компилятор видит всю модель и убирает две вещи: накладные расходы интерпретатора Python и лишние походы в память GPU. Вместо того чтобы для каждой операции GELU отдельно читать тензор из памяти и писать обратно, он сливает их в одно ядрослияние операций (kernel fusion)EN kernel fusionВыполнение нескольких операций одним ядром: промежуточные результаты остаются в регистрах или SRAM, в видеопамять пишется только итог. Главный способ ускорить memory-bound операции. Автоматически это делает torch.compile., которое прочитает данные один раз. Ускорение ≈ 2,3× одной строкой.
FlashAttention (2:00:18). Алгоритм делает больше арифметики, чем обычное внимание, но работает быстрее: он
никогда не записывает в медленную память матрицу внимания , а считает softmax по кускам
(онлайн-softmaxonline softmaxEN online softmaxSoftmax за один проход по данным с бегущими максимумом m и суммой ℓ: при новом максимуме старая сумма умножается на e^(m_старый − m_новый). Milakov и Gimelshein (NVIDIA, 2018); основа FlashAttention.). torch.compile сам до такого переписывания не додумается. Loss
при этом тот же — меняется только скорость. Подробно — урок 5.7.2.
«Красивые» числа (2:06:54). Ядра CUDA работают блоками, кратными степеням двойки. 50257 — «некрасивое» нечётное число, а 50304 делится на 128. Добавленные «фиктивные» токены в данных не встречаются, модель быстро выучивает, что их вероятность — ноль. Вычислений становится больше, а время падает. Итого ≈ 11× от базовой линии.
Раздел 3. Гиперпараметры, накопление градиентов, DDP (2:14:55–3:10:21)
Заголовок раздела «Раздел 3. Гиперпараметры, накопление градиентов, DDP (2:14:55–3:10:21)»Оптимизатор, клиппинг, расписание
Заголовок раздела «Оптимизатор, клиппинг, расписание»Из статьи о GPT-3: AdamWAdamWEN AdamW (Adam with decoupled weight decay)Adam, в котором затухание весов применяется отдельно от адаптивного шага. Оптимизатор по умолчанию для обучения LLM. с , , и
глобальный клиппингобрезка градиента (gradient clipping)EN gradient clippingЕсли норма градиента больше порога, градиент масштабируют до порога: clip_grad_norm_(params, 1.0). Стандарт в обучении LLM. нормы градиента до 1,0. Карпатый печатает норму на каждом
шаге: в начале она около 30 и быстро опускается ниже 1. Рост нормы посреди обучения — тревожный сигнал.
Расписание learning rate — линейный разогревразогрев learning rate (warmup)EN learning rate warmupПервые сотни–тысячи шагов learning rate линейно растёт от нуля до пика, пока статистики Adam не накопились. и косинусное затуханиекосинусное затухание learning rateEN cosine decay (cosine annealing)После разогрева learning rate плавно спадает от пика до минимума по половине косинусоиды. до 10% от максимума. Максимум — 6e-4, как у GPT-3 Small (по размеру это аналог GPT-2 124M).
max_lr = 6e-4min_lr = max_lr * 0.1warmup_steps = 715max_steps = 19073 # 19 073 шага ≈ 1 эпоха: 10 млрд токенов при батче 0,5 млн токеновdef get_lr(it): # 1) линейный разогрев в течение warmup_steps шагов if it < warmup_steps: return max_lr * (it+1) / warmup_steps # 2) после конца расписания — минимальный learning rate if it > max_steps: return min_lr # 3) между ними — косинусное затухание до минимального learning rate decay_ratio = (it - warmup_steps) / (max_steps - warmup_steps) assert 0 <= decay_ratio <= 1 coeff = 0.5 * (1.0 + math.cos(math.pi * decay_ratio)) # coeff идёт от 1 к 0 return min_lr + coeff * (max_lr - min_lr)Постепенное увеличение батча из статьи о GPT-3 Карпатый пропускает. Объяснение: в начале обучения все примеры дают почти одинаковый градиент («эти токены бывают, эти — нет»), так что это скорее ускорение системы, чем улучшение алгоритма, а арифметику оно усложняет.
Weight decay (2:26:21). 0,1, как у GPT-3, и только для двумерных тензоров — матриц линейных слоёв и
эмбеддингов. Смещения и параметры LayerNorm не штрафуются. У модели 50 тензоров с decay и 98 без него, но во вторых
всего около ста тысяч чисел. Плюс fused=True — обновление всех параметров одним ядром вместо цикла: 93 → 90 мс.
Подробнее о группах параметров — в уроке 4.6.3.
Накопление градиентов (2:34:09)
Заголовок раздела «Накопление градиентов (2:34:09)»У GPT-3 Small батч — 0,5 млн токенов. При длине 1024 это ≈ 488 строк, и в память GPU они не влезут. Выход — накопление: делаем много проходов вперёд-назад на маленьких микробатчах, градиенты складываются, и только потом один шаг оптимизатора. Берём токенов. С микробатчем 16 × 1024 = 16 384 токена это 32 микрошага, и шаг обучения длится около 3 секунд.
Главный подвох: F.cross_entropy усредняет loss по батчу, а накопление градиентов суммирует. Если ничего не
сделать, градиент окажется в 32 раза больше. Карпатый показывает это на игрушечной сети с MSE и 4 примерами: батч из
4 и четыре шага по 1 дают разные градиенты, пока loss не поделить на 4.
for micro_step in range(grad_accum_steps): x, y = train_loader.next_batch() x, y = x.to(device), y.to(device) with torch.autocast(device_type=device_type, dtype=torch.bfloat16): logits, loss = model(x, y) # градиенты при backward() складываются, а это СУММА в целевой функции; # нам нужно СРЕДНЕЕ, поэтому масштабируем loss loss = loss / grad_accum_steps loss_accum += loss.detach() loss.backward()Приятное следствие: размер микробатча B становится чисто «скоростной» настройкой. Результат обучения от него не
зависит, лишь бы влезло в память.
DDP: восемь GPU (2:46:52)
Заголовок раздела «DDP: восемь GPU (2:46:52)»torchrun --standalone --nproc_per_node=8 train_gpt2.py запускает 8 копий скрипта. Каждая получает переменные
окружения RANK, LOCAL_RANK, WORLD_SIZE и работает на своей видеокарте.
flowchart LR D["Поток токенов"] --> R0["GPU 0: свои B×T токенов"] D --> R1["GPU 1: следующие B×T"] D --> R7["... GPU 7"] R0 --> A["all-reduce: среднее градиентов"] R1 --> A R7 --> A A --> O["одинаковый шаг AdamW на всех GPU"]
Что пришлось изменить:
- печатать и сохранять только из главного процесса (
master_process = ddp_rank == 0); DataLoaderLiteначинает чтение со сдвигомB * T * process_rankи шагает наB * T * num_processes, чтобы процессы не читали одни и те же токены;- обернуть модель в
DDP(model, device_ids=[ddp_local_rank]). Усреднять градиенты нужно только на последнем микрошаге накопления, поэтому флагmodel.require_backward_grad_syncвключается лишь там (в итоговом коде строка перенесена выше, до прямого прохода: по заметкам в README флаг используют и forward, и backward); - усреднить между процессами печатаемый loss:
dist.all_reduce(loss_accum, op=dist.ReduceOp.AVG).
При 8 GPU и микробатче 16 накопление сокращается с 32 до 4 шагов, скорость — около 1,5 млн токенов в секунду. Числа немного отличаются от прогона на одной GPU из-за того, как загрузчики «заворачивают» данные в конце файла. На маленьком батче, где этого эффекта нет, они совпадают.
Данные: от WebText к FineWeb-Edu (3:10:21–3:23:10)
Заголовок раздела «Данные: от WebText к FineWeb-Edu (3:10:21–3:23:10)»- GPT-2: набор WebText не опубликован. Это страницы по внешним ссылкам с Reddit, набравшим не меньше 3 кармы: 45 млн ссылок, 40 ГБ текста. Есть попытка воспроизведения — OpenWebText.
- GPT-3: смесь, где большая часть — отфильтрованный Common Crawl, плюс WebText2, книги и Википедия. Тоже не опубликован. Сырой Common Crawl, по словам Карпатого, «гораздо хуже, чем вы думаете»: реклама, таблицы, биржевые котировки. Поэтому все его тщательно фильтруют.
- Открытые аналоги: RedPajama (и очищенный SlimPajama), FineWeb — 15 трлн токенов отфильтрованного Common Crawl, и его образовательная часть FineWeb-Edu (1,3 трлн токенов «образовательных» и 5,4 трлн «высокообразовательных»). Образовательность размечала большая языковая модель — по словам Карпатого, Llama 3 70B.
Берётся выборка sample-10BT — 10 млрд токенов. Скрипт fineweb.py скачивает её через библиотеку datasets,
токенизирует (каждый документ начинается с <|endoftext|> = 50256), хранит токены как uint16 (все номера меньше
65 535) и режет на 100 шардов по 100 млн токенов. Шард 000 — валидация, остальные — обучение. Подробнее о том, как
готовят такие корпуса, — в уроке 5.1.2.
Числа запуска: шага (одна эпоха); разогрев в GPT-3 — 375 млн токенов, то есть шагов (Карпатый считает это очень осторожным: хватило бы и около 100). На A100 80 ГБ влезает микробатч 64 × 1024, и с 8 GPU накопление вообще не нужно: 330 мс на шаг, ≈1,7 часа на эпоху.
Валидация и HellaSwag (3:23:10–3:43:05)
Заголовок раздела «Валидация и HellaSwag (3:23:10–3:43:05)»Раз в несколько сотен шагов (в итоговом коде — каждые 250) модель считает loss на валидационном шарде и генерирует продолжения фразы «Hello, I’m a language model,». Для генерации заведён отдельный генератор случайных чисел, чтобы не сбивать случайность обучения.
HellaSwag — тест на здравый смысл: контекст и четыре продолжения, одно
правильное. Маленькая модель не умеет отвечать «вариант B», поэтому используется «стиль дополнения»: каждое из
четырёх продолжений подставляется после контекста, считается средний loss на токенах продолжения, и ответом
считается вариант с наименьшим loss. В валидационной части 10 042 примера, случайное угадывание даёт 25%. В
hellaswag.py записано, что GPT-2 124M в таком стиле набирает 29,55% (acc_norm) — это число и надо побить. Тест
старый, поэтому его примеры могли просочиться в обучающие данные. Карпатый говорит об этом прямо.
Одна шероховатость: с torch.compile генерация и HellaSwag в его версии PyTorch падали, поэтому в итоговом коде
use_compile = False.
Раздел 4. Результаты наутро (3:43:05–3:59:39)
Заголовок раздела «Раздел 4. Результаты наутро (3:43:05–3:59:39)»10 млрд токенов (≈2 часа на 8× A100). Loss на валидации FineWeb-Edu опустился ниже, чем у чекпоинта GPT-2 124M на той же валидации (сравнение не вполне честное: GPT-2 училась на другом распределении). HellaSwag вырос с 25% и превысил GPT-2 124M. Карпатый отмечает: GPT-2, по его словам, училась на 100 млрд токенов, а здесь хватило 10.
40 млрд токенов (4 эпохи, ночь, ≈8 часов). HellaSwag 33,24% — почти уровень GPT-3 124M (33,7%), обученной на 300 млрд токенов. Генерации заметно связнее.
Почему вышло эффективнее, Карпатый честно не знает и перечисляет гипотезы: FineWeb-Edu — только английский, почти без кода и математики, и ёмкость модели не тратится на них; данные сейчас чище (дедупликация, фильтрация качества); не исключено, что HellaSwag просочился в обучающий набор.
Что осталось недоделанным:
- на графике loss — странная периодичность. Похоже, выборка FineWeb-Edu не перемешана, а загрузчик проходит токены строго по порядку. Нужно перемешивать документы и шарды в каждой эпохе;
- максимальный learning rate, судя по экспериментам других людей, можно поднять почти в 3 раза — гиперпараметры GPT-3 консервативны;
- для точного соответствия GPT-3 нужна длина контекста 2048 при микробатче 32 (те же 0,5 млн токенов на шаг);
- сохранение чекпоинтов (каждые 5000 шагов) — для продолжения обучения надо сохранять ещё и состояние оптимизатора и генераторов случайных чисел;
- это только претрейн: модель «видит сны» о документах интернета. Чтобы с ней разговаривать, нужен SFTобучение с учителем на инструкциях (SFT)EN supervised fine-tuning (SFT) — тот же цикл обучения на диалогах (урок 6.1.2).
llm.c (3:56:21). Тот же GPT-2 на чистом C/CUDA. train_gpt2.py из llm.c — это эталон на PyTorch, а
train_gpt2.cu — быстрая версия. В демонстрации на одной GPU loss и нормы совпадают шаг в шаг, а скорость — около
223 тысяч токенов в секунду против 185 тысяч у PyTorch.
Исправления после видео (README build-nanogpt)
Заголовок раздела «Исправления после видео (README build-nanogpt)»В README репозитория перечислены исправления, сделанные после записи:
- забытый после перехода на FlashAttention буфер маски
biasудалён; - старые версии PyTorch плохо переводят
uint16вlong, поэтому вload_tokensмассив сначала переводится вint32средствами NumPy; - в
torch.autocastнадо передавать тип устройства ("cuda"), а не само устройство ("cuda:3"); model.require_backward_grad_syncиспользуется и прямым, и обратным проходом, поэтому строку перенесли выше.
Для «боевого» обучения похожих моделей README советует litGPT и TinyLlama.
🖥 На вашей машине (4×GPU)
Заголовок раздела «🖥 На вашей машине (4×GPU)»Всё, что ниже, — наши рекомендации, а не текст лекции. Код build-nanogpt не требует NVLink: DDP усредняет градиенты через NCCL, а у вас это пойдёт по PCIe 4.0.
- Шекспир на одной GPU. Ранние коммиты (до
switch to FineWeb EDU) работают сinput.txtиз репозитория и отрабатывают за минуты. Начните с них:git log --oneline --reverse, затемgit checkout <коммит>. - Данные.
pip install datasets tiktoken numpy tqdmиpython fineweb.py. Скрипт скачает выборку с Hugging Face и запишет 100 шардов по 100 млн токенов в форматеuint16— это около 20 ГБ на диске (2 байта на токен), плюс место под скачанный набор. По словам Карпатого, обработка занимает около получаса на его машине. - Запуск на 4 GPU. В
train_gpt2.pyуменьшите микробатч:B = 64рассчитан на 80 ГБ. На карте с 32 ГБ начните сB = 16и пробуйте 32.total_batch_size = 524288должен делиться наB * T * 4: приB = 16накопление составит 8 микрошагов, приB = 32— 4. Запуск:Окно терминала torchrun --standalone --nproc_per_node=4 train_gpt2.py - Оценка времени. Скрипт печатает
tok/sec. Время эпохи — токенов, делённое на эту скорость. Например, при 300 тысячах токенов в секунду (условное число — измерьте своё) эпоха займёт с ≈ 9 часов. Учтите, что в итоговом кодеuse_compile = False: если включить компиляцию, станет быстрее, но придётся отключить HellaSwag и генерацию. - PCIe вместо NVLink. Градиенты 124 млн параметров усредняются один раз за шаг оптимизатора, а при накоплении
градиентов этот шаг длится секунды. Поэтому связь между картами здесь — не главное узкое место. Проверить можно,
сравнив
tok/secна 1 и на 4 GPU: в идеале рост почти в 4 раза.
Что сделать руками
Заголовок раздела «Что сделать руками»- Пройдите коммиты build-nanogpt по порядку (их больше 40) и для каждого найдите место в лекции по карте выше. Самые
важные:
add forward(),calculate the loss,little loop crushes a little batch,weight tie,gpt-2 initialization, все коммиты раздела 2,add gradient accumulation,add DistributedDataParallel training. - Проверьте loss на старте: случайная модель должна давать ≈ 10,8. Если у вас 15 или 50, что-то не так с инициализацией.
- Повторите опыт с MSE из ноутбука
play.ipynb: батч из 4 против четырёх шагов накопления, с делением loss на 4 и без него. - Повторите таблицу ускорений на своей RTX 4080: замерьте мс/шаг для float32, TF32, bf16,
torch.compile, FlashAttention и 50304. Совпадёт ли порядок выигрышей с A100? - Прогоните хотя бы часть обучения на FineWeb-Edu и нарисуйте кривые train/val/HellaSwag ячейкой из
play.ipynb.
Словарик
Заголовок раздела «Словарик»| EN | RU |
|---|---|
| checkpoint | чекпоинт, сохранённые веса |
| decoder-only transformer | трансформер «только декодер» |
| pre-norm, residual stream | нормализация перед блоком, остаточный поток |
| weight tying | связывание весов |
| logits | логиты |
| top-k sampling | top-k-сэмплирование |
| Tensor Cores, TF32, bfloat16 | тензорные ядра, форматы TF32 и bfloat16 |
| mixed precision, autocast | смешанная точность, автоприведение типов |
| kernel fusion | слияние ядер |
| FlashAttention, online softmax | FlashAttention, онлайн-softmax |
| warmup, cosine decay | разогрев, косинусное затухание |
| weight decay | штраф на веса (weight decay) |
| gradient clipping | обрезка (клиппинг) градиента |
| gradient accumulation, micro-batch | накопление градиентов, микробатч |
| Distributed Data Parallel (DDP), all-reduce | параллелизм по данным, all-reduce |
| shard | шард, кусок набора данных |
| validation loss | loss на валидации |
| tokens per second | токены в секунду (пропускная способность) |