# Инференс LLM за один проход: архитектура Jev, KV-кэш и алгоритм RLCD

Источник: https://www.youtube.com/watch?v=aZglGvBPQGs
Канал: freeCodeCamp.org
Опубликовано: 27.10.2026

---

Современные языковые модели традиционно решают задачи классификации и принятия решений через авторегрессионную генерацию структурированного текста (например, JSON), что создает значительные накладные расходы по задержке и вычислительным ресурсам. Инженер и автор обучающих курсов Вивек Кальянаранган в материале для платформы freeCodeCamp детально разбирает концепцию Jev — архитектурного подхода, заменяющего многоэтапный декодинг одиночным прямым проходом (prefill-only) с фиксацией детерминированных вероятностей. В рамках курса рассматриваются реализации на базе как энкодеров, так и декодеров, тонкости построения кастомных масок внимания для параллельной обработки вопросов, а также алгоритмы обучения с подкреплением RLCD и GRPO.

## ⚡ Что такое Jev: отказ от авторегрессии в пользу Prefill-Only
[[JUMP:01:12]]

Классический цикл инференса большой языковой модели состоит из двух фундаментально разных фаз: префилла (prefill) и декодинга (decode) [01:31]. В фазе префилла модель параллельно обрабатывает весь входящий промпт; данная операция ограничена преимущественно вычислительной мощностью процессора или видеокарты (compute-bound) [01:43]. В фазе декодинга модель генерирует ответ авторегрессионно — строго по одному токену за шаг, возвращая сгенерированный токен обратно на вход сети [01:56]. 

По словам Вивека Кальянарангана, если контекст задачи составляет 500 токенов, для получения ответа модель сначала прогоняет 500 токенов, затем 501, 502 и так далее, пока не упрется в стоп-токен [02:08]. При решении прикладных инженерных задач — маршрутизации тикетов, классификации транзакций или скоринге рисков — такой подход приводит к двум фундаментальным проблемам:

*   Неконтролируемая и высокая задержка (latency), растущая линейно с каждым сгенерированным токеном [02:33].
*   Необходимость парсинга свободного текста или структуры JSON, что требует валидации схемы и порождает риск синтаксических сбоев [02:46].

Идея системы Jev заключается в полном исключении фазы декодинга [02:21]. Вся задача формулируется таким образом, чтобы модель выдавала готовое решение за один прямой проход (single forward pass) в рамках фазы префилла [02:33]. Это гарантирует строго ограниченную задержку и математически предсказуемый вывод в виде вероятностного распределения по заранее определенным категориям [03:00].

## 🛠️ Интерфейсы решений и требования к стеку
[[JUMP:03:26]]

Архитектура Jev предоставляет разработчику унифицированный API для трех ключевых типов логических запросов [03:26]:

*   **Choice (Выбор):** номинальная классификация среди взаимоисключающих ортогональных категорий без естественного порядка (например: направления «север», «юг», «запад», «восток» или варианты действий) [03:53].
*   **Score (Оценка):** порядковая (ординальная) классификация, где между классами существует градация (например: эмоциональный тон клиента — «спокойный», «раздраженный», «в ярости») [04:18].
*   **Boolean / Null (Логический тип):** бинарные вопросы формата «да/нет», возвращающие вероятность истинности утверждения [04:45].

Кальянаранган подчеркивает, что все три интерфейса на уровне базовой реализации могут опираться на единый расчет функции Softmax для распределения логитов, однако их калибровка и математическая оценка при обучении существенно различаются [05:11].

Для воспроизведения рассматриваемых пайплайнов не требуется кластер GPU — весь инференс можно выполнять на CPU [06:42]. Ключевыми требованиями являются уверенное владение PyTorch, понимание механизма внимания (Self-Attention), структуры KV-кэша, а также концепций логитов и Softmax [05:36]. В качестве основы для разбора используются открытые предобученные модели из экосистемы Hugging Face [08:24].

## 🐢 Базовый уровень: почему посимвольный авторегрессионный декодинг неэффективен
[[JUMP:08:00]]

Для демонстрации недостатков стандартного подхода автор разбирает сценарий антифрод-мониторинга [08:49]. Модели передается контекст: с банковской карты, оканчивающейся на 4412, совершено 9 транзакций за 3 минуты в четырех странах, при этом владелец заявляет, что физическая карта находится у него в кармане [09:02]. 

В стандартной парадигме разработчик отправляет промпт через интерфейс, аналогичный OpenAI API или библиотеке Pydantic, ожидая JSON следующего формата [09:14]:

```json
{
  "priority": "high",
  "decision": "fraud",
  "escalate": true
}
```

В демонстрационном коде используется квантованная модель Qwen с 3,6 млрд параметров [08:24]. Вектор входных идентификаторов промпта (включая системные инструкции и открывающую фигурную скобку `{`) составляет порядка 130 токенов [11:53], [15:21].

На каждом шаге авторегрессионного цикла модель вычисляет тензор логитов размерностью `[1, 130, 152000]`, где 152 000 — объем словаря (vocabulary size) [16:41]. Чтобы получить один токен, скрипт:

1. Берет срез последнего токена `[-1, :]` [16:54].
2. Применяет к вектору логитов функцию Softmax [17:19].
3. Находит индекс с максимальной вероятностью через операцию `argmax` [17:33].
4. Добавляет новый токен в конец последовательности, обновляет KV-кэш и повторяет прямой проход для вычисления следующего токена [16:28], [18:58].

Модели требуются десятки итераций только для генерации служебных символов: кавычек, двоеточий, переносов строк и названий ключей (`"priority"`, `"decision"`) [18:29], [20:26]. На накладные расходы форматирования тратится львиная доля времени, хотя прикладной системе требовались лишь три конкретных значения [20:39].

## 🧩 Архитектура на базе Encoder-Only: маскирование и эмбеддинги запросов
[[JUMP:21:24]]

Первая альтернатива авторегрессии — использование энкодерных моделей (семейства BERT или аналогичных маскированных языковых моделей, MLM) [21:45]. Поскольку энкодер обладает двунаправленным вниманием (bidirectional attention), каждый токен видит контекст как слева, так и справа от себя [24:21].

Вивек Кальянаранган предлагает нестандартную структуру компоновки входной последовательности: вопрос ставится в самое начало, а исходный контекст (state) помещается в конец [22:25]:

```text
[CLS] Вопрос [SEP] [MASK] Вариант_A [MASK] Вариант_B [SEP] Контекст [SEP]
```

Причина такого порядка заключается в архитектурной особенности механизмов внимания, которые исторически присваивают больший вес начальным позициям последовательности [22:50]. Перед каждым возможным вариантом ответа внедряется служебный токен маски `[MASK]` [23:16].

Главное ограничение энкодерного подхода связано с его двунаправленной природой [24:21]. В энкодере невозможно переиспользовать кэш ключей и значений (KV-cache) для неизменного контекста: любое изменение вопроса или вариантов ответов в начале последовательности полностью пересчитывает веса внимания для каждого последующего токена [24:34]. Поэтому при наличии 50 разных вопросов к одному документу энкодер вынужден заново прогонять весь контекст 50 раз [24:46].

## 💻 Реализация Encoder-подхода в PyTorch: от пулинга до линейных голов
[[JUMP:25:26]]

В практическом разборе скрипта `encoding.py` демонстрируется обработка тикета службы поддержки: клиент жалуется на двойное списание за один заказ и угрожает чарджбэком через банк [26:19]. Проверяется гипотеза: требует ли клиент возврат средств (`"no"`, `"yes"`) [26:58].

Векторизованная последовательность содержит 54 токена, а скрытое состояние (hidden state) последнего слоя имеет размерность `[54, 768]` [30:42]. Для извлечения решения выполняются следующие математические преобразования:

*   **Пулинг запроса (Query Mean Pooling):** определяются границы токенов самого вопроса (с 1 по 9 позицию, исключая токен `[CLS]`) [32:14]. Для этих 8 токенов вычисляется среднее арифметическое их скрытых состояний (mean pooling), что формирует итоговый вектор запроса размерностью `[768]` [33:06].
*   **Извлечение векторов опций:** фиксируются скрытые состояния на позициях токенов `[MASK]`, расположенных непосредственно перед вариантами ответов (например, позиции 10 и 12) [33:57], [34:34]. В результате формируется матрица опций размерностью `[2, 768]` [36:21].
*   **Скалярное произведение (Dot Product):** вычисляется проекция вектора запроса на векторы опций [36:35]. Полученные скаляры нормализуются с помощью Softmax, превращаясь в вероятности классов [36:48], [39:42].

Кальянаранган отмечает, что «сырая» предобученная модель BERT без дообучения выдает смещенные вероятности (например, 30% на «yes» вместо уверенного срабатывания), так как внутренние эмбеддинги не калиброваны под вероятностный вывод [40:09]. Для адаптации энкодера в продакшене автор выделяет три пути [37:42]:

1.  **Линейная голова (Linear Head):** добавление слоя `Linear(768, 1)` поверх скрытого состояния маски для прямого предсказания скалярного логита [37:56].
2.  **Частичный Fine-Tuning:** заморозка основы и обучение только классификатора [38:24].
3.  **Полное дообучение (Full Backbone Fine-Tuning):** сквозная оптимизация всех слоев трансформера [38:37].

## 🔄 Архитектура Decoder-Only: кэширование контекста и упаковка вопросов
[[JUMP:40:42]]

Большинство современных LLM относятся к типу Decoder-Only (семейства Qwen, Llama, Gemma). В отличие от энкодера, каузальное (причинно-следственное) внимание позволяет зафиксировать контекст задачи в самом начале последовательности и закэшировать его в KV-кэш один раз [41:02].

Паттерн работы с декодером строится по принципу автодополнения (auto-complete) [42:21]:

```text
[Контекст документа] Вопрос: <текст вопроса>? Ответ:
```

После двоеточия модель должна сгенерировать ровно один токен [42:34]. При расчете распределения первого токена отсекаются все 152 000 логитов словаря, кроме тех индексов, которые соответствуют целевым вариантам (например, идентификаторы слов `"yes"` и `"no"`) [43:00]. К оставшимся логитам применяется Softmax [43:15]. 

Модели декодеров способны поддерживать обработку сотен и тысяч вопросов к одному документу без повторного вычисления базового контекста [41:55]. Если длина контекста модели составляет 32 000 токенов, а сам документ занимает 1 000 токенов, оставшиеся 31 000 позиций можно утилизировать под параллельные цепочки вопросов [41:41].

## 🎛️ Пользовательская причинная маска внимания и переиспользование KV-кэша
[[JUMP:46:22]]

При попытке упаковать несколько вопросов в один контекст возникает фундаментальная проблема утечки внимания (context pollution) [50:10]. Если вопрос №2 видит токены вопроса №1, итоговое решение модели начинает зависеть от порядка следования вопросов [50:22]. 

Кальянаранган обращает внимание на архитектурный изъян некоторых альтернативных решений (например, открытой реализации Clef от Cloudflare), где вопросы видят друг друга [50:36]. В Jev применяется строгая изоляция с помощью кастомной 2D-маски видимости [51:15]:

*   Контекст видит только самого себя по стандартной каузальной маске (токены 0–37) [1:00:48].
*   Вопрос №1 видит весь базовый контекст и свои собственные токены, но не видит последующие вопросы [1:01:43].
*   Вопрос №2 видит базовый контекст и свои собственные токены, но его видимость токенов вопроса №1 принудительно зануляется (`False`) [51:02], [1:01:30].

В коде `decoder.py` для 38 токенов контекста формируется KV-кэш: 28 слоев, 8 голо Compartment (KV-heads), скрытая размерность 128 [54:41], [55:20]. Далее формируются тензоры сегментов (segments), разделяющие границы вопросов, и через операцию внешнего сравнения `seg == seg.T` создается маска принадлежности к одному вопросу [58:13]. Объединение логического `И` между базовой каузальной маской (`arange >= arange.T`) и маской сегментов, дополненное полным доступом к токенам контекста, дает результирующую матрицу видимости размером `[35, 73]` для 35 токенов, формирующих вопросы [57:34], [59:45].

При вызове прямого прохода модели в PyTorch явно передаются предрассчитанный KV-кэш, позиционные индексы токенов (от 38 до 73 для корректной работы RoPE) и сгенерированная кастомная маска внимания [1:02:59], [1:03:13].

## 🎯 Извлечение логитов, коллизии токенов и калибровка решений
[[JUMP:1:03:52]]

После выполнения прямого прохода извлекаются логиты точек принятия решений (decision points) — позиций, соответствующих последнему токену перед предполагаемым ответом [1:04:19], [1:06:30]:

*   Для первого вопроса точка принятия решения находилась на 18-м относительном токене; из словаря были выбраны токены с ID `902` («no») и `9834` («yes») [1:05:01], [1:05:52]. Вероятность того, что клиент просит возврат, составила 95% [1:06:56].
*   Для второго вопроса (определение ответственного отдела: «billing» против «technical») точка решения находилась на 34-м токене [1:05:01]. Модель выдала ID `30531` («billing») с уверенностью 97% и ID `10916` («technical») с вероятностью 2,5% [1:05:52], [1:07:22].

### Проблема коллизии мультитокенов (Multi-Token Collision)
[[JUMP:1:09:12]]

Если варианты ответов начинаются с одного и того же токена, наивный выбор вероятности по первому токену дает сбой [1:09:31]. Например, в паре вариантов `"fraud"` и `"fraud review"` первый токен идентичен, и декодер не сможет разделить их вероятности за один шаг, выдав распределение 50/50 [45:03], [45:16].

Кальянаранган предлагает три способа решения коллизий [45:28], [1:09:44]:

*   **Перефразирование:** замена пересекающихся терминов на уникальные синонимы (например, `"fraud"` и `"review"`) [45:28].
*   **Индексация через алиасы:** формулирование задачи как Multiple Choice с выбором букв `A`, `B`, `C`, `D` [45:41], [1:10:09].
*   **Span Pooling:** агрегация скрытых состояний по всей длине фразы варианта ответа с последующей классификацией через линейный слой [44:31], [45:54].

В энкодерах данная проблема отсутствует в принципе, поскольку токен маски привязан к конкретной позиции в предложении [1:10:24].

## 🎓 Обучение моделей: дистилляция «учитель — ученик» и GRPO
[[JUMP:1:11:15]]

Хотя предобученный декодер способен решать задачи zero-shot, для достижения высокой калибровки вероятностей требуется специальный пайплайн обучения. В основе лежит схема дистилляции знаний «учитель — ученик» (Teacher-Student Distillation) [1:11:17].

В роли учителя выступает более мощная закрытая модель или размеченный датасет, формирующий целевое распределение вероятностей (например: 0.7 для «billing», 0.2 для «technical», 0.1 для «account») [1:11:42]. Обучаемая компактная модель (студент) генерирует логиты (например: +1.0, +0.8, +0.2), которые после Softmax дают распределение [0.44, 0.36, 0.19] [1:12:07]. Несмотря на сохранение рангового порядка, распределение студента нуждается в подтягивании к учителю [1:12:22].

Для оптимизации используется адаптация алгоритма GRPO (Group Relative Policy Optimization) из области обучения с подкреплением [1:12:35]. Поскольку в парадигме prefill-only нет генерации длинных текстовых цепочек (траекторий), альтернативные траектории синтезируются искусственно [1:13:27]:

1.  К базовым логитам студента добавляются различные случайные шумы (пертурбации) [1:13:40].
2.  Для каждого возмущенного вектора рассчитывается вознаграждение (reward) на основе близости к распределению учителя [1:14:34].
3.  Вычисляется среднее вознаграждение группы $\mu$ и стандартное отклонение $\sigma$ [1:15:24].
4.  Для каждой траектории вычисляется преимущество (Advantage) по формуле [1:15:24]:
$$A_i = \frac{R_i - \mu}{\sigma}$$

5.  Траектории с положительным преимуществом ($A_i > 0$) поощряются градиентным спуском, смещая веса студента в сторону учителя [1:14:46], [1:15:51].

## 📐 Функция потерь RLCD: комбинация Log Loss, Spherical Score и RPS
[[JUMP:1:16:54]]

Фреймворк RLCD (Reinforcement Learning for Calibrated Decisions) опирается на комплексную функцию награды, оценивающую не просто точность совпадения меток, а математическую честность вероятностного распределения [1:16:54].

Награда $R$ складывается из трех ключевых метрик [1:17:01], [1:21:39]:

*   **Log Score (Логарифмический скор):** логарифм вероятностей студента, взвешенный по вероятностям учителя [1:18:04]:
$$\text{LogScore} = \sum p_{\text{teacher}} \cdot \log(p_{\text{student}})$$
Метрика отвечает на вопрос: выделил ли студент высокую вероятность там же, где ее выделил учитель [1:17:13].

*   **Spherical Score (Сферический скор):** косинусоподобная метрика совпадения векторов вероятностей, нормализующая коллинеарность направлений предсказаний учителя и студента [1:17:26], [1:18:33].
*   **Ranked Probability Score (RPS):** специализированная метрика для ординальных шкал (интерфейс Score) [1:19:37].

Вивек поясняет необходимость RPS наглядным примером: клиент находится в состоянии «в ярости» [1:20:18]. Если модель выбирает между предсказаниями «раздражен» и «спокойный», стандартный Log Loss накажет обе ошибки одинаково [1:20:57]. Однако с точки зрения калибровки ответ «раздражен» находится ближе к истине, чем «спокойный» [1:20:31]. Метрика RPS вычисляет среднеквадратичную разность между кумулятивными суммами распределений (CDF) студента и учителя, штрафуя далекие промахи сильнее близких [1:21:09]:

$$\text{RPS} = \frac{1}{K-1} \sum_{k=1}^{K-1} \left( \sum_{j=1}^k p_{\text{student}, j} - \sum_{j=1}^k p_{\text{teacher}, j} \right)^2$$

Итоговая функция потерь объединяет классическую кросс-энтропию и RL-компонент [1:22:05]:
$$\mathcal{L} = \mathcal{L}_{\text{CE}} - \left( \text{LogScore} + 0.75 \cdot \text{SphericalScore} - \text{RPS} \right)$$

Оптимизацию можно проводить как на уровне легковесных адаптеров LoRA или единичных классификационных голов, так и через Full Fine-Tuning базовой сети [1:22:56].

## ⚖️ Encoder против Decoder: инженерные компромиссы и выбор архитектуры
[[JUMP:1:23:17]]

В финальной части курса Вивек Кальянаранган сопоставляет сильные и слабые стороны рассмотренных подходов, формируя матрицу архитектурных компромиссов для инженера [1:23:23]:

*   **Кэширование контекста:** абсолютное преимущество у Decoder-Only [1:23:35]. Контекст читается один раз и оседает в памяти KV-кэша, после чего к нему можно адресовать неограниченное число дополнительных вопросов без нарастания вычислительной сложности [1:23:48]. Энкодер вынужден заново пересчитывать всю матрицу внимания на каждый запрос [1:23:35].
*   **Длина контекста:** декодеры масштабируются значительно лучше при работе с большими документами (10K+ токенов) [1:24:01].
*   **Плотность параметров и качество представлений:** энкодеры выигрывают за счет двунаправленного внимания [1:24:14]. Модель BERT на 400 млн параметров способна демонстрировать уровень качества извлечения признаков, сопоставимый с декодером на 2–3 млрд параметров [1:25:05].
*   **Мультитокенные опции:** внедрение токенов маски `[MASK]` в энкодере полностью исключает коллизии префиксов, тогда как в декодере разработчику приходится контролировать однотокенность ответов или применять алиасы [1:24:27], [1:24:39].
*   **Сложность пайплайна:** энкодер требует обучения проекционных линейных слоев практически с нуля, в то время как декодер обеспечивает приемлемое качество классификации прямо «из коробки» в режиме zero-shot [1:06:56], [1:23:09].

Выбор между энкодером и декодером сводится к балансу между доступной видеопамятью, требованиями к задержке и количеством вопросов, приходящихся на один неизменный фрагмент входных данных [1:25:18].