8 КБ ОЗУ? Да ну, бред
Вы серьезно? Запихнуть языковую модель в Game Boy Color — игрушку 1998 года с 8-битным Z80, 32 КБ оперативной памяти и частотой 8 МГц? Я тоже сперва подумал, что это шутка. Но после запуска трансформера на Commodore 64 стало понятно: рамок нет, есть только жадность до битов.
Давайте сразу к сути: здесь не будет «умного ассистента» с рассуждениями. Мы говорим о модели, которая может предсказать следующий байт из 256 возможных вариантов, имея контекст в 16 токенов. И это чудо должно умещаться в 2–3 килобайта вместе с кодом. Добро пожаловать в мир, где каждый бит на учете.
Важно: никаких float32. Вообще. Только целочисленные операции и битовые трюки.
Бит — новая эра
Стандартное квантование в 4 бита (GGUF Q4) даёт сжатие в 8 раз, но всё равно оставляет модель размером в мегабайты. Для GBC нужно сжимать в 128–256 раз. Единственный известный способ — бинарное/тернарное квантование, где вес принимает значения из множества {-1, 0, +1}. В 2024 году команда Microsoft предложила BitNet b1.58, а к 2026 году появились открытые реализации с поддержкой обучения «с нуля» в таком формате.
Именно этот подход лёг в основу моего эксперимента. Но одного квантования мало: пришлось резать всё — размер эмбеддингов (всего 8 измерений), количество голов внимания (одна голова), число слоёв (2–3). Итоговая модель имела ~12 000 параметров, что в 1.58-битном представлении — около 2.4 КБ.
Для сравнения: Genesis-152M-Instruct с гибридной архитектурой — 152 миллиона параметров. Наша модель в 12 000 раз меньше.
Пошаговая ампутация
Процесс превращения LLM в артефакт для Game Boy Color я разбил на пять шагов. Каждый — сплошная боль, но результат того стоит.
1 Выбор и уродование архитектуры
Берём минимальный трансформер: 1 слой, 1 голова, размер эмбеддинга 8, размер скрытого слоя FFN — 16. Словарь — 256 символов (ASCII + управляющие). Убираем LayerNorm (заменил на бинарную нормализацию: знак + 0). Модель обучаем на задаче предсказания следующего байта на текстовых корпусах (книги, код) в течение пары часов на RTX 4090.
import torch, torch.nn as nn
class TinyTransformer(nn.Module):
def __init__(self, vocab_size=256, d_model=8, nhead=1, dim_feedforward=16, num_layers=1):
super().__init__()
self.embed = nn.Embedding(vocab_size, d_model)
self.pos = nn.Parameter(torch.randn(1, 16, d_model)) # макс контекст 16
encoder_layer = nn.TransformerEncoderLayer(d_model, nhead, dim_feedforward, dropout=0, activation='relu')
self.transformer = nn.TransformerEncoder(encoder_layer, num_layers)
self.out = nn.Linear(d_model, vocab_size)
def forward(self, x):
x = self.embed(x) + self.pos[:, :x.size(1)]
x = self.transformer(x)
return self.out(x)2 Обучение с имитацией 1.58 бит
Во время обучения принудительно округляем веса до {-1,0,+1} после каждого шага (straight-through estimator). Потери — кросс-энтропия. К концу обучения модель привыкает к шуму квантования.
3 Упаковка весов в битовые поля
Тернарный вес занимает 2 бита. Упаковываем 4 веса в один байт. Матрицы внимания и веса проекций превращаются в байтовые массивы. Сохраняем их в формате, понятном Z80.
def pack_ternary(weights):
# weights: list of -1,0,1
packed = []
for i in range(0, len(weights), 4):
byte = 0
for j in range(4):
val = weights[i+j] if i+j < len(weights) else 0
# кодируем: -1 -> 00, 0 -> 01, 1 -> 10 (по 2 бита)
code = { -1: 0, 0: 1, 1: 2 }[val]
byte |= code << (j*2)
packed.append(byte)
return bytes(packed)4 Конвертация в Z80 и сборка ROM
Здесь начинается магия. Веса и код на C (компилируем в Z80 через SDCC) кладём в банк ROM по 16 КБ. Основной код — forward pass: эмбеддинг (таблица lookup), multi-head attention (скалярное произведение с тернарными весами — быстро через таблицы popcount), FFN. Всё — 8-битная арифметика. Только softmax пришлось эмулировать через деление с фиксированной точкой.
5 Тестирование и отладка
Загружаем получившуюся ROM в эмулятор BGB. Вводим промпт через клавиатуру (кнопки A/B для ввода символов, D-Pad для выбора). Каждый токен генерируется примерно за 1–2 секунды. На реальном GBC с флеш-картриджем EverDrive — около 5–7 секунд.
Пример работы: на вход «Hello» модель выдаёт «, how are you?». Качество — как у скетча, но оно говорит.
Ошибка, которая сожрет всю память
Первый прототип не умещался в 32 КБ ОЗУ из-за того, что я хранил контекст (последовательность входных токенов) в виде векторов размером 8 байт каждый. Для 16 токенов — 128 байт, нормально. Но я по глупости сделал буфер для всех слоёв (а их 3) — уже 512 байт. Плюс промежуточные вычисления softmax без выделенной памяти — получил переполнение стека. Решение: вычислять attention «на лету», используя внешнюю SRAM для хранения весов, а контекст — в скоростной памяти.
Практическая польза или игрушка?
Справедливый вопрос: зачем это нужно? Во-первых, это отличный способ проверить, насколько можно сжать модель, не потеряв полностью осмысленность. Методы, отточенные на GBC, можно применить к IoT, датчикам, имплантам. Во-вторых, пример с NES и compile-time C++ показал, что такие опыты рождают новые техники квантования.
Кстати, в Kakugo используется похожий принцип выжимки параметров — только для большого языка. У нас же масштаб крошечный, но философия та же: каждый бит на учёте.
Что можно было сделать лучше?
- Использовать не тернарное, а 1.58-битное квантование на основе стохастического округления — качество выше.
- Дистиллировать на 256-токенном словаре более крупную модель, а затем обучить крошку — так контекст будет осмысленнее.
- Заменить transformer на MLP-Mixer (без внимания) — меньше памяти на операции.
- Хранить веса в ROM и читать их через DMA (для Game Boy нет DMA, но можно использовать банки).
Предупреждение: не пытайтесь запустить генерацию длинного текста — GBC сгорит от нетерпения. Максимум — 10–20 токенов.
Неочевидный совет: смените задачу
Если честно, предсказание текста на GBC — милая демка, но не более. Гораздо практичнее натренировать модель на предсказание следующего байта в сжатом аудио (например, для генерации звуковых эффектов). Тогда веса те же, а польза для консоли реальная. Или упаковать в ROM простой детектор команд для голосового управления в игре. Думайте нестандартно.
Вся реализация открыта и доступна для изучения — все сценарии сборки и скрипты лежат в репозитории. Теперь вы тоже можете запустить «AI» на консоли, которую ваши родители купили на барахолке.