Порт microgpt.py (Karpathy) на Ruby; локальная копия оригинала с сохранением функциональной идентичности: один файл, минимум зависимостей, обучение и инференс маленькой GPT на символьном уровне (например, на списке имён).
- Один файл — вся логика в
microgpt.rb: автоград (классValue), токенизация, архитектура GPT (1 слой, RMSNorm, multi-head attention, MLP), оптимизатор Adam, инференс с temperature. - Практически без внешних зависимостей — только стандартная библиотека Ruby (
open-uri,net/httpдля загрузки датасета при отсутствииinput.txt). - Учебный проект — демонстрация идеи «всё остальное — лишь эффективность» на Ruby.
- Ruby 2.6+ (проверялось на 4.0.1).
- В каталоге с скриптом при первом запуске автоматически скачивается names.txt в
input.txt, если файла нет.
ruby microgpt.rbОжидаемое время выполнения на Mac Studio M1 Max 64 Gb RAM: около 1 минуты (1000 шагов обучения + 20 сэмплов инференса).
num docs: 32033
vocab size: 27
num params: 4192
step 1 / 1000 | loss 3.3430
step 2 / 1000 | loss 3.5257
step 3 / 1000 | loss 3.3301
...
step 998 / 1000 | loss 2.1192
step 999 / 1000 | loss 2.2051
step 1000 / 1000 | loss 2.1348
--- inference (new, hallucinated names) ---
sample 1: kelial
sample 2: airin
sample 3: kalia
sample 4: jaruda
sample 5: aelen
sample 6: reyla
sample 7: arar
sample 8: calelin
sample 9: dakeyr
sample 10: kaenan
sample 11: avasia
sample 12: narin
sample 13: aleyn
sample 14: arin
sample 15: alya
sample 16: kala
sample 17: arele
sample 18: hayn
sample 19: aulire
sample 20: kara
Числа (loss, количество документов, параметров, сэмплы) могут немного отличаться из-за различий в реализации случайности и округления, но поведение и порядок величин совпадают с оригиналом.
| Аспект | Python | Ruby |
|---|---|---|
| Случайные числа | random.gauss(0, std) в стандартной библиотеке |
Нет Random#gauss → реализован Box–Muller в функции gaussian(mean, std). |
| Взвешенная выборка | random.choices(range(n), weights=...) |
Нет в стандартной библиотеке → функция weighted_choice(weights) (линейный поиск по кумулятивной сумме). |
Суммы по Value |
sum(sequence) с началом 0 и Value.__radd__ |
Явный reduce(Value.new(0)) { |a, b| a + b }, т.к. Float#* не переопределён и порядок float * value не вызывает Value#*. |
| Порядок операндов | (1/n) * loss |
loss * (1.0/n) — чтобы всегда вызывался Value#* и не срабатывал встроенный Float#*. |
| Глобальное состояние | gpt() читает state_dict, n_layer и т.д. из глобальной области |
В Ruby gpt(..., state_dict, n_layer, head_dim, n_head) — эти величины передаются аргументами. |
| Загрузка файла | urllib.request.urlretrieve |
URI.open(uri).read + File.write. |
| Срезы | q[hs:hs+head_dim] |
q[hs, head_dim] (длина, а не конец диапазона). |
Логика обучения (forward, backward, Adam, формат логов) и инференса (temperature, BOS, генерация до BOS или до block_size) совпадает с оригиналом.
- Совместимость вывода с Python — фиксировать один и тот же seed и при необходимости привести к одному формату генерации случайных (например, свой минимальный RNG с тем же алгоритмом), чтобы при одинаковом
input.txtполучать идентичные сэмплы и loss. - Производительность — вынести горячие участки в нативный код (C/Rust extension или Numo/NMatrix-подобные операции), если понадобится ускорение без смены языка.
- Читаемость и тесты — разбить на модули/классы (Tokenizer, Model, Optimizer), добавить unit-тесты на
Value(backward для простых выражений) и интеграционный тест «обучение 1–2 шага + один сэмпл». - Расширение модели | — несколько слоёв (
n_layer > 1), опция GeLU вместо ReLU, сохранение/загрузка весов в файл. - CLI — аргументы: путь к
input.txt, число шагов, temperature, количество сэмплов, seed, возможно--no-download(не скачивать датасет). - Без сетевого доступа — режим «только локальный
input.txt» безopen-uriиnet/httpдля изолированных окружений. - Документация — короткие комментарии к шагам (токенизация, RMSNorm, attention, Adam) прямо в коде для учебного использования.
Оригинал: microgpt.py. Идея и алгоритм: @karpathy.
Пост на Хабре: https://habr.com/ru/articles/996404/