Вы пишете промпт, жмёте Enter, и через секунду LLM выдаёт связный ответ. Магия? Нет, математика. Конкретные матрицы, которые множатся на ваших GPU или NPU. Если вы инженер, который деплоит эти модели в прод, поверхностное понимание архитектуры — прямой путь к пожарам: модель не влезает в VRAM, latency прыгает, а на нагрузке падает точность.
В этой статье мы препарируем типичную 12B LLM на примере Gemma 4 12B — от токенизации до момента, когда последний токен уходит в ответ. Без воды, с реальными формами тензоров и привязкой к железу. Готовьтесь считать flops и смотреть на память.
Важно: Все численные параметры (d_model, количество слоёв, head_dim) соответствуют реальной конфигурации Gemma 4 12B на момент 20.06.2026. Я не использую абстрактные цифры — только те, что вы увидите в config.json модели.
Этап 1: Токенизация — как LLM жуёт текст
Первый шаг — превратить строку в последовательность чисел. Gemma 4 12B использует SentencePiece с размером словаря 256k токенов. Это не просто разбивка по пробелам: модель учится на слоги, морфемы и даже части слов.
Возьмём фразу: "Привет, как дела?". Токенизатор разобьёт её на что-то вроде ["▁Привет", ",", "▁как", "▁дела", "?"]. Каждый токен получает целочисленный ID.
После токенизации у нас есть массив [batch_size, seq_len] — скажем, [1, 128] для контекстного окна в 128 токенов. Дальше в игру вступает embedding.
Этап 2: Embedding и позиционные кодировки
Каждый ID проецируется в плотный вектор размерности d_model. В Gemma 4 12B d_model = 3584. Значит, из матрицы [vocab_size, d_model] мы извлекаем срез размером [seq_len, d_model].
# Псевдокод для иллюстрации
embedding = nn.Embedding(vocab_size=256000, d_model=3584)
input_ids = torch.randint(0, 256000, (1, 128))
x = embedding(input_ids) # shape: (1, 128, 3584)
Gemma 4 использует RoPE (Rotary Position Embedding). Это не отдельный вектор, а поворот матриц Q и K внутри attention. Код RoPE встроен прямо в вычисление внимания, поэтому на уровне тензоров вы его не увидите — он меняет сдвиг фаз частот.
Типичная ошибка: некоторые инженеры пытаются прибавить позиционный embedding к слою нормализации перед attention, потому что так было в оригинальном Transformer. В Gemma 4 это сломает модель. RoPE работает только в attention — не путайте.
Этап 3: Attention — сердце трансформера
Теперь самое интересное: multi-head self-attention. Gemma 4 12B имеет 28 голов внимания и 40 слоёв. Размерность каждой головы: head_dim = d_model / n_heads = 3584 / 28 = 128.
1 Формируем Q, K, V
Из входного тензора (batch, seq, d_model) мы получаем три проекции через линейные слои без bias. Каждый слой nn.Linear(d_model, d_model). Итоговые Q, K, V имеют ту же форму (batch, seq, d_model). Затем мы разделяем на головы: (batch, n_heads, seq, head_dim).
# Реальное преобразование
W_q = nn.Linear(3584, 3584, bias=False)
Q = W_q(x).view(batch, seq, 28, 128).transpose(1, 2) # (1, 28, 128, 128)
2 Вычисление внимания
Для каждой головы: scores = Q @ K^T / sqrt(head_dim). В нашем случае sqrt(128) ≈ 11.31. Матрица scores имеет форму (batch, n_heads, seq, seq). Для последовательности в 4096 токенов это 28 * 4096 * 4096 ≈ 470 млн элементов — уже 1.8 GB в float16.
Здесь кроется причина, почему при длинных контекстах модель начинает "забывать" — softmax, применённый к таким огромным матрицам, приводит к насыщению. В Gemma 4 26B эта проблема вызывала дрейф тензоров, что мы разбирали в статье Gemma 4 26B ломается в полёте. В 12B версии таких эффектов меньше, но flash attention — обязательное требование для деплоя.
После softmax получаем attention weights, умножаем на V: output = weights @ V. Обратно собираем головы в один тензор (batch, seq, d_model) через линейный слой W_o.
Этап 4: Feed-Forward Network (FFN)
После attention идёт FFN. В Gemma 4 12B используется SwiGLU — комбинация двух линейных слоёв и Swish-активации. Размер промежуточного слоя: d_ff = 28672 (обычно 8 * d_model). Матрицы: W1: (3584, 28672), W2: (28672, 3584), W3: (3584, 28672) (для ворот).
# Псевдокод FFN с SwiGLU
hidden = F.silu( x @ W1 ) * ( x @ W3 ) # (batch, seq, 28672)
output = hidden @ W2 # (batch, seq, 3584)
Этот блок занимает ~70% параметров модели. Если вы квантуете модель до 4 бит, именно здесь вы получаете наибольший выигрыш в размере. В статье Как запустить многомодальную Gemma 4 локально я подробно разбирал, как Q4_0 меняет профиль памяти.
Этап 5: LayerNorm и Residual
Каждый блок (attention + FFN) окружён LayerNorm и skip-connection. Gemma 4 использует RMS LayerNorm — без центрирования, только нормализация по корню среднего квадрата. Параметр eps = 1e-6. Форма нормализации — вдоль последней оси (d_model).
x = x + attention(rms_norm(x))
x = x + ffn(rms_norm(x))
Это стандарт для всех современных LLM. Интересный нюанс: порядок Pre-LN (нормализация перед слоем) даёт более стабильное обучение, но для инференса это не имеет значения — вычисления те же.
Этап 6: Выходной слой и дискретизация
После последнего 40-го слоя идёт финальный LayerNorm, затем линейный слой lm_head: (3584, vocab_size=256000). Получаем логиты (batch, seq, vocab_size). Для генерации каждого нового токена берём последний логит последовательности и пропускаем через softmax. Затем выбираем токен с максимальной вероятностью (жадная декодировка) или сэмплируем.
Здесь часто возникают проблемы с производительностью: softmax над 256k классами занимает ~3-5% времени инференса. В средах с батчингом это ощутимо. Некоторые фреймворки, как Стратум, предлагают замену на логарифмический softmax, но я бы советовал сначала измерить — часто bottleneck в другом месте.
Этап 7: Инференс — от одного токена до ответа
LLM генерирует автогрессивно: один токен за раз. На каждом шаге он принимает предыдущий токен и обновляет KV-кэш. KV cache — это матрицы Key и Value из всех слоёв, которые мы сохраняем после первого forward pass. Его размер: n_layers * 2 * batch * n_heads * seq_len * head_dim * dtype_size.
Для Gemma 4 12B с seq_len=4096 в float16: 40 * 2 * 1 * 28 * 4096 * 128 * 2 = 40 * 2 * 4096 * 28 * 128 * 2 ≈ 2.35 GB. На 8K контексте — уже 4.7 GB. На 32K — 18.8 GB. Теперь понятно, почему длинные контексты такие дорогие?
Провал в проде: Один клиент деплоил Gemma 4 12B с контекстом 32K на одной RTX 4090 (24 GB). Модель в FP16 занимала ~24 GB, плюс KV cache — сразу OOM. Пришлось срезать контекст до 8K и использовать 8-битный KV cache. Если вам нужно больше — смотрите на решение Склеиваем Gemma и DeepSeek, где мы объединяем модели для снижения нагрузки.
На этапе продакшена мы не можем позволить себе ждать по 2 секунды на токен. Здесь вступают батчинг и continuous batching. vLLM и TensorRT-LLM собирают несколько запросов в один тензор, разрезают контексты до минимальной длины и выполняют forward на всём батче. Gemma 4 12B поддерживает Flash Attention 2, что даёт прирост ~3x по сравнению с наивной реализацией.
Аппаратные ограничения и квантование
Модель в FP16 весит 24 GB. На потребительских GPU (RTX 4090 24 GB) это впритык. На корпоративном А100 (80 GB) — комфортно, но дорого. Чтобы влезть на одну 4090, люди квантуют до 4 бит. Сейчас лучший вариант — IQ4_NL в llama.cpp, который сохраняет больше 95% качества.
Недавно я экспериментировал с multi-token prediction для Gemma 4. В статье Как настроить MTP для Gemma 4 31B я показал, как драфтер ускоряет генерацию в 2-3 раза. Для 12B это не так критично, но задел на будущее.
Ещё один важный фактор — bandwidth memory. Карта H100 с HBM3 выдаёт 3.35 TB/s, в то время как RTX 4090 — 1 TB/s. Разница в три раза напрямую влияет на latency. Поэтому для продакшена я рекомендую H100 или как минимум 3090 с NVLink.
Подводные камни в продакшене
- Tool calling — Gemma 4 12B имеет проблемы с вызовом функций. Подробнее в статье Почему tool calling сломан. Решение — дообучать модель на синтетических вызовах.
- Средоточность (sycophancy) — модель склонна поддакивать пользователю. В статье про sycophancy мы описали fine-tuning с penalisation.
- Хаос в чатах — когда LLM используется как ассистент разработчика, контекст разрастается. Методология организации описана в Процесс разработки с LLM.
- Абблация — если вам нужно понять, какие головы внимания важны, используйте инструменты из обзора инструментов абблации.
Собираем всё воедино: от тензора до ответа
Давайте проследим путь одного токена через всю модель:
- Токенизация: строка → ID →
[1, 1] - Embedding:
[1, 1, 3584] - 40 слоёв трансформера: каждый содержит attention (с RoPE) + SwiGLU FFN + остаточные связи + RMSNorm. Тензор не меняет формы.
- Финальный RMSNorm + lm_head:
[1, 1, 256000] - Softmax + выбор токена:
int - Повторяем с шага 2 для нового токена, обновляем KV cache.
На практике весь этот pipeline занимает ~10-30 мс на один токен на H100 (в зависимости от батча). На RTX 4090 в квантованном виде — ~30-70 мс. Этого достаточно для чата, но недостаточно для real-time транскрибации. Если вам нужно быстрее — смотрите в сторону специфических железок (Groq, Cerebras) или используйте гибридные схемы.
Последний совет: не верьте benchmarks. Каждая модель ведёт себя по-разному под нагрузкой. Gemma 4 12B, например, показывает отличную latency на коротких промптах, но на длинных контекстах начинает тупить из-за softmax saturation. Лучший способ проверить — запустить тесты с реальным трафиком. Возьмите инфраструктуру из статьи Ghetto MLOps и адаптируйте под себя.
Теперь, когда вы знаете, что происходит под капотом, вы сможете принимать осознанные решения: стоит ли гнаться за 8K контекстом, какой формат квантования выбрать и где ставить баннер "Не хватает памяти". LLM — это просто линейная алгебра с порогом входа. Не дайте себя запутать buzzwords.