---
title: "LLama.cpp AI LLM Engine — Как это работает"
url: "https://romankryvolapov.com/ru/llama-cpp/"
description: "Как устроен инференс в llama.cpp: загрузка GGUF, токенизация, построение вычислительного графа, KV-cache и сэмплинг следующего токена."
language: ru
updated: 2026-02-04
---
## Предисловие {#predislovie}

**Llama.cpp** — это программа (движок) для запуска больших языковых моделей (LLM) на компьютере или на мобильных устройствах — на процессоре (CPU) или видеокарте (GPU); я написал приложение [Offline AI Launcher](/ru/offline-ai-launcher/), которое позволяет использовать этот же движок на Android.\
 **Зачем это нужно:**\
 \
 \- чтобы получать ответы от нейросетевых моделей (чат, дополнение текста, код) без отправки данных в облако;\
 \- выбор языка C++ нужен для быстрой работы с памятью и железом; один и тот же код собирается под Windows, Linux, macOS и Android.

**«Веса» модели** — огромный набор чисел (миллионы или миллиарды), на которых основаны все вычисления нейросети: матрицы умножения, смещения и т.д. Их получают при обучении модели и сохраняют в файл. При инференсе движок только читает эти числа и применяет их к входным данным, не меняя сами веса. **Инференс** — это процесс «спроса» к уже обученной модели: вы даёте текст, модель по шагам выдаёт ответ.

Движок работает с форматом файлов **GGUF**. GGUF — формат, в котором лежит сохранённая модель:\
 \
 \- «веса» (числа нейросети);\
 \- метаданные (размеры, тип архитектуры);\
 \- словарь (соответствие «текст ↔ числа» для токенов).\
 По сути это один файл-контейнер, из которого движок читает всё нужное в память. Поддерживается **квантизация** (уменьшение размера модели за счёт более грубого хранения чисел) и гибридные вычисления (часть на CPU, часть на GPU). Модели в формате GGUF можно брать с [Hugging Face](https://huggingface.co/models?library=gguf&sort=trending).

**Цель статьи** — по шагам разобрать, как в llama.cpp устроен инференс: от загрузки модели до появления очередного токена в ответе.\
 **Токен** — это число, соответствующее кусочку текста (слову или части слова); модель работает только с числами, а словарь переводит «текст → токены» и обратно.\
 **Статья выстроена как сценарий:**\
 \
 \- сначала подготовка (инициализация, загрузка модели, создание контекста);\
 \- затем генерация по запросу (токенизация, батч, decode, сэмплинг).

**С какими моделями работает движок:**\
 \
 \- LLaMA (Meta);\
 \- Qwen (Alibaba);\
 \- Gemma (Google);\
 \- Mistral;\
 \- Phi и другие.\
 Отличия — в размере, длине контекста и деталях; в коде это разные гиперпараметры и тензоры весов. Общий сценарий один и тот же.

**В каких проектах используется:**\
 \
 \- [LM Studio](https://lmstudio.ai);\
 \- [Ollama](https://ollama.com);\
 \- [GPT4All](https://www.nomic.ai/gpt4all);\
 \- [Offline AI Launcher](/ru/offline-ai-launcher/) (запуск моделей на смартфоне);\
 \- KoboldCpp;\
 \- Text Generation WebUI.\
 Репозиторий: [github.com/ggml-org/llama.cpp](https://github.com/ggml-org/llama.cpp). Примеры кода в статье могут отличаться от вашей версии.

**Как читать статью:**\
 \
 \- разделы идут в порядке выполнения сценария;\
 \- в каждом разделе для действия даётся цитата из кода и объяснение, что происходит и для чего;\
 \- файлы исходников указаны в тексте;\
 \- статья рассчитана на неподготовленного читателя: все термины объясняются в глоссарии в начале статьи, все шаги снабжены цитатами кода и пояснениями.

## Глоссарий терминов {#glossarij-terminov}

**Батч (batch)** — набор токенов (или эмбеддингов), позиций и флагов логитов, обрабатываемых за один вызов decode. При Prefill в батче много токенов промпта; при Decode — один новый токен.

**Бэкенд (backend)** — «движок» вычислений: CPU или видеокарта (GPU). Планировщик распределяет узлы графа по бэкендам.

**Decode** — этап генерации по одному токену: в батче один новый токен, граф вычисляет логиты для этой позиции, K и V дописываются в KV-cache.

**Эмбеддинг (embedding)** — вектор чисел, в который превращается токен перед подачей в слои модели; одна строка матрицы эмбеддингов модели.

**EOS (end of sequence)** — специальный токен «конец вывода»; по нему приложение прекращает генерацию.

**GGUF** — формат файла модели: заголовок, метаданные (ключ–значение), данные тензоров. Поддерживается квантизация и mmap.

**Гиперпараметры (hparams)** — числа, задающие размеры модели: длина контекста, число слоёв, размер эмбеддинга, число голов внимания и т.д.

**Инференс** — процесс получения ответа от модели: подаётся промпт, модель по шагам выдаёт следующий токен.

**KV-cache** — кэш ключей и значений механизма внимания; хранит уже посчитанные K и V по всем предыдущим позициям, чтобы не пересчитывать их на каждом шаге.

**Логиты (logits)** — «сырые» оценки модели по каждому токену словаря перед softmax; по ним сэмплер выбирает следующий токен.

**Prefill** — этап обработки промпта: в батче все (или много) токенов промпта, для них считаются K и V и записываются в KV-cache.

**Сэмплинг** — выбор одного токена по логитам (жадный, случайный с temperature/top_p и т.д.).

**Тензор** — многомерный массив чисел (веса модели, эмбеддинги, ключи, значения, логиты и т.д.).

**Токен** — целое число (ID), соответствующее кусочку текста (слову или части слова); модель работает только с токенами.

**Токенайзер** — компонент словаря, превращающий текст в токены (SPM, BPE и т.д.) и обратно.

**Вычислительный граф** — список операций (матрицы, сложения, активации) и связей между ними; по нему движок выполняет все вычисления модели.

**Подбатч (ubatch)** — часть батча, которая обрабатывается за один вызов **process_ubatch**. Если промпт длиннее n_ubatch, батч разбивается на подбатчи по n_ubatch токенов; каждый подбатч прогоняется через модель по очереди.\
 **Для чего:**\
 \
 \- чтобы ограничить пиковое потребление памяти при длинном промпте.

**Планировщик (sched)** — компонент GGML, который распределяет узлы вычислительного графа по бэкендам (CPU, GPU) и выделяет под граф буферы на этих устройствах. При выполнении графа планировщик обходит узлы в топологическом порядке и запускает операции на выбранных устройствах.

## Схема процесса: все шаги по порядку {#shema-processa-vse-shagi}

Ниже — вся цепочка от запуска движка до появления очередного токена в ответе. Каждый пункт дальше в статье разбирается подробно, с цитатами кода и пояснениями.

**Подготовка (один раз при старте или смене модели):**

шаг 1: инициализация бэкенда (**llama_backend_init**) →\
 шаг 2: загрузка модели из файла (**llama_model_load_from_file** → загрузчик GGUF) →\
 шаг 3: определение типа модели (**load_arch**) →\
 шаг 4: загрузка гиперпараметров — размеры, контекст (**load_hparams**) →\
 шаг 5: загрузка словаря и токенайзера (**load_vocab**) →\
 шаг 6: загрузка весов в память или на GPU (**load_tensors**) →\
 шаг 7: создание контекста инференса — KV-cache, планировщик (**llama_new_context**).

**Генерация (для каждого сообщения и каждого нового токена в ответе):**

шаг 8: приходит текст промпта от пользователя →\
 шаг 9: токенизация — текст превращается в последовательность токенов (**llama_tokenize**) →\
 шаг 10: формирование батча — токены упаковываются для одного вызова (**llama_batch_add**, **balloc->init**) →\
 шаг 11: **llama_decode** — при необходимости токены превращаются в эмбеддинги (encode) →\
 шаг 12: батч при длинном промпте разбивается на подбатчи (**memory->init_batch**) →\
 шаг 13: каждый подбатч прогоняется через модель: построение графа → выполнение на CPU/GPU (**process_ubatch** → **build_graph** → **graph_compute**) →\
 шаг 14: логиты для последней позиции копируются в буфер контекста →\
 шаг 15: сэмплинг — по логитам выбирается один следующий токен →\
 шаг 16: токен переводится в текст и выводится (**llama_token_to_piece**); если это не EOS — новый токен добавляется в батч, переход к шагу 11; иначе цикл завершается.

## Общий процесс инференса LLM {#obshhij-process-inferensa-llm}

Выше дана схема: что за чем происходит. По смыслу процесс делится на два этапа. Первый — подготовка: инициализация бэкенда, загрузка модели из GGUF (архитектура, гиперпараметры, словарь, веса) и создание контекста. Он выполняется один раз при старте или при смене модели. Второй этап — генерация: при каждом сообщении пользователя текст превращается в токены, упаковывается в батч, прогоняется через модель (**llama_decode**), из логитов выбирается следующий токен, он переводится в текст и выводится; цикл повторяется до токена «конец вывода» (EOS) или лимита. Этот цикл повторяется для каждого нового сообщения и для каждого нового токена в ответе. Ниже каждый шаг из схемы разбирается по отдельности: что именно вызывается в коде, что происходит и что может быть неочевидно неподготовленному читателю.

## Структура репозитория и основные файлы {#struktura-repozitoriya-i-osnovnye-fajly}

В репозитории llama.cpp основные части движка разнесены по папкам и файлам.\
 **Для чего так сделано:**\
 \
 \- чтобы разнести ответственность: загрузка файла, модель, словарь, контекст и батч — в разных файлах;\
 \- так проще искать код и отлаживать.\
 Ниже перечислены файлы, которые прямо относятся к загрузке модели и инференсу; для каждого указано, что в нём лежит и зачем это нужно.

**Точка входа и загрузка модели:**

\- **src/llama.cpp** — здесь живут функции **llama_backend_init**, **llama_model_load_from_file**, **llama_model_load** (статическая).\
 **Для чего:**\
 \
это «входная дверь» в движок: приложение вызывает эти функции, чтобы инициализировать библиотеку и загрузить модель; здесь же создаётся загрузчик и по очереди вызываются **load_arch**, **load_hparams**, **load_vocab**, **load_tensors**.

\- **src/llama-model-loader.cpp** — класс **llama_model_loader**: открытие GGUF-файла, построение индекса тензоров (**weights_map**), методы **get_tensor**, **load_tensor_data**.\
 **Для чего:**\
 \
загрузчик нужен, чтобы по имени тензора знать, где в файле лежат его данные и как их прочитать или отобразить в память (mmap); без него нельзя по шагам загружать архитектуру, гиперпараметры, словарь и веса.

**Модель и словарь:**

\- **src/llama-model.cpp** — класс **llama_model**, методы **load_arch**, **load_hparams**, **load_vocab**, **load_tensors**, создание тензоров и назначение буферов.\
 **Для чего:**\
 \
объект модели хранит всё, что прочитано из файла: тип архитектуры, гиперпараметры, словарь и сами веса (тензоры); методы load_\* по очереди заполняют эти данные из загрузчика.

\- **src/llama-vocab.cpp** — класс **llama_vocab**, реализация **llama_vocab::impl::load** (загрузка словаря из GGUF), токенизация и обратный перевод токенов в текст (**tokenize**, **token_to_piece**, **detokenize**).\
 **Для чего:**\
 \
словарь нужен, чтобы превращать текст в числа (токены) при вводе и числа обратно в текст при выводе; без него модель не сможет ни принять промпт, ни выдать читаемый ответ.

**Контекст и decode:**

\- **src/llama-context.cpp** — класс **llama_context**: создание контекста (память, планировщик, резерв графов), метод **decode**, **process_ubatch**, **llama_get_logits_ith**.\
 **Для чего:**\
 \
контекст — это «рабочая среда» одного сеанса генерации: в нём задаётся размер контекста, KV-cache, планировщик вычислений; метод decode прогоняет батч через модель и возвращает логиты.

\- **src/llama-batch.cpp** — структура батча, класс **llama_batch_allocr**, метод **init** (заполнение позиций и флагов логитов).\
 **Для чего:**\
 \
батч — это «пакет» токенов для одного вызова decode; аллокатор проверяет батч и при отсутствии полей заполняет их (позиции из памяти, логиты только для последнего токена), чтобы вызывающему коду не нужно было вручную всё выставлять.

\- **src/llama-memory.cpp** — выделение и обновление KV-cache, **init_batch** (разбиение на подбатчи).\
 **Для чего:**\
 \
KV-cache хранит уже посчитанные ключи и значения по всем предыдущим позициям, чтобы не пересчитывать их на каждом шаге; init_batch разбивает большой батч на подбатчи ограниченного размера, чтобы не переполнить память.

**Библиотеки и бэкенды:**

\- **ggml** (подмодуль или отдельная папка) — вычислительный граф (GGML), типы тензоров, планировщик (**ggml_backend_sched**), бэкенды CPU/GPU.\
 **Для чего:**\
 \
граф описывает, какие операции (умножения матриц, активации и т.д.) выполнить и в каком порядке; планировщик решает, на каком устройстве (CPU или GPU) считать каждый узел графа.

\- **gguf** — чтение и запись формата GGUF (заголовок, метаданные, тензоры).\
 **Для чего:**\
 \
формат GGUF задаёт, как в файле лежат заголовок, метаданные и данные тензоров; библиотека gguf предоставляет функции для их чтения без ручного разбора байтов.

\- **include/llama.h** — заголовок API для приложений: объявления **llama_model_load_from_file**, **llama_context**, **llama_decode**, **llama_tokenize**, **llama_get_logits_ith**, сэмплеры и т.д.\
 **Для чего:**\
 \
приложение подключает этот заголовок и вызывает объявленные функции, не заходя во внутренние файлы движка.

## Основные типы и структуры (справочно) {#osnovnye-tipy-i-struktury-spravochno}

Для ориентирования в коде полезно знать основные типы. Ниже — что хранит каждый тип и для чего он нужен.

**Основные структуры:**

\- **llama_model** — объект загруженной модели. В нём хранятся: гиперпараметры (**hparams**), словарь (**vocab**), тензоры весов, список устройств (**devices**), разбиение слоёв по CPU/GPU.\
 **Для чего:**\
 \
модель — это всё, что прочитано из файла и нужно для вычислений; один объект модели можно использовать для нескольких контекстов (несколько сеансов генерации).

\- **llama_context** — контекст инференса. В нём: ссылка на модель, параметры контекста (**cparams**: n_ctx, n_batch, n_ubatch, n_threads и т.д.), аллокатор батча (**balloc**), память KV-cache (**memory**), планировщик (**sched**), зарезервированные графы (**gf_res_prev** и т.п.), буфер логитов.\
 **Для чего:**\
 \
контекст — «рабочая среда» одного сеанса: размер контекста, кэш ключей и значений, планировщик и графы для decode; при каждом запросе вызывается decode именно для этого контекста.

\- **llama_batch** — массив токенов (или эмбеддингов), позиций, идентификаторов последовательностей и флагов логитов.\
 **Для чего:**\
 \
один вызов **llama_decode** принимает один батч; в нём передаётся, какие токены обработать, на каких позициях и для каких позиций вернуть логиты (обычно только для последней).

\- **llama_model_loader** — загрузчик GGUF. В нём: метаданные (**meta**), карта тензоров (**weights_map**), открытые файлы.\
 **Для чего:**\
 \
загрузчик живёт только во время загрузки модели; по нему по очереди читаются архитектура, гиперпараметры, словарь и тензоры; после загрузки он не нужен.

\- **ggml_context** — контекст графа GGML: в нём создаются узлы и тензоры графа.\
 **Для чего:**\
 \
граф описывает последовательность операций (умножения, активации и т.д.); все узлы графа создаются в одном таком контексте.

\- **ggml_backend_sched** — планировщик: список бэкендов и логика распределения узлов графа по устройствам.\
 **Для чего:**\
 \
планировщик решает, на каком устройстве (CPU или GPU) выполнять каждый узел графа, и выделяет под граф буферы на этих устройствах.

**Что возвращают основные функции:**\
 \- **llama_model_load_from_file** возвращает **llama_model\*** или **nullptr** при ошибке.\
 **Для чего:**\
 \
приложение проверяет указатель: если не nullptr, модель загружена и можно создавать контекст.\
 \- **llama_model_load** возвращает 0 при успехе, -1 при ошибке, -2 при отмене (например, по колбэку прогресса).\
 **Для чего:**\
 \
вызывающий код по возврату решает, удалять ли модель и выходить.\
 \- **llama_decode** возвращает 0 при успехе, 1 если нужно больше входных данных, отрицательное значение при ошибке.\
 **Для чего:**\
 \
приложение по возврату понимает, удалось ли выполнить decode и можно ли читать логиты.\
 \- **llama_tokenize** возвращает число записанных токенов.\
 **Для чего:**\
 \
чтобы знать, сколько элементов массива токенов заполнено.\
 \- **llama_get_logits_ith** возвращает указатель на массив float размером n_vocab.\
 **Для чего:**\
 \
по этому массиву сэмплер выбирает следующий токен (по одному числу на каждый токен словаря).

## Шаг 1: Инициализация бэкенда {#inicializaciya-bekenda}

**Шаг 1** в схеме процесса — инициализация бэкенда. Перед загрузкой модели и инференсом приложение один раз вызывает **llama_backend_init()**. Движок опирается на внутренние структуры: таймер для замеров скорости и таблицы для работы с числами в формате f16 (половинная точность). Они создаются при первом вызове; без инициализации загрузка модели или decode могут вести себя некорректно. Ниже — цитата целиком и по фрагментам (файл **src/llama.cpp**).

```cpp
// Шаг 1: единственная точка входа инициализации движка; вызывается один раз при старте приложения
void llama_backend_init(void) {
    // Включаем точный таймер — потом по нему замеряют время загрузки и decode
    ggml_time_init();
    // Один раз проинициализировать таблицы формата f16 (половинная точность):
    // веса и часть вычислений хранятся в f16 — экономия памяти и ускорение на GPU
    {
        struct ggml_init_params params = { 0, NULL, false };  // нулевой буфер — только таблицы
        struct ggml_context * ctx = ggml_init(params);
        ggml_free(ctx);  // контекст освобождаем, таблицы остаются инициализированными
    }
}
```

**Что в коде происходит по шагам:**

```cpp
// Фрагмент 1: таймер нужен, чтобы потом замерить время загрузки модели и время decode
ggml_time_init();

// Фрагмент 2: нулевой буфер — только инициализация таблиц f16, данные тензоров не выделяются
struct ggml_init_params params = { 0, NULL, false };
struct ggml_context * ctx = ggml_init(params);
ggml_free(ctx);  // таблицы f16 остаются инициализированными на всё время работы процесса
```

Сначала включается таймер для последующих замеров скорости. Затем создаётся временный контекст GGML с нулевым буфером — этого достаточно, чтобы один раз проинициализировать внутренние таблицы формата f16 (преобразование между float32 и float16). Контекст сразу освобождается, но таблицы остаются. После этого движок готов к загрузке модели и построению графа; формат f16 используется для весов и части вычислений — он экономит память и ускоряет работу на GPU.

## Шаг 2: Загрузка модели из файла (точка входа) {#tochka-vhoda-zagruzki-modeli}

**Шаг 2** — загрузка модели из файла. Она начинается с вызова **llama_model_load_from_file**: приложение передаёт путь к .gguf и параметры, движок возвращает готовый объект модели или nullptr при ошибке.\
 **Для чего эта функция:**\
 \
 \- она — единственная точка входа для загрузки модели из файла;\
 \- приложение передаёт путь к файлу и параметры, движок возвращает готовый объект модели или nullptr при ошибке.\
 Цитата из кода (файл **src/llama.cpp**):

```cpp
// Точка входа загрузки модели (шаг 2): путь к .gguf и параметры (mmap, n_gpu_layers и т.д.)
struct llama_model * llama_model_load_from_file(
        const char * path_model,
        struct llama_model_params params) {
    // splits — пути к частям модели, если она разбита на несколько файлов; для одного файла пусто
    std::vector<std::string> splits = {};
    return llama_model_load_from_file_impl(path_model, splits, params);
}
```

Функция принимает путь к файлу модели (обычно .gguf) и параметры загрузки — приложение указывает, откуда читать модель и как её загружать (mmap, число слоёв на GPU и т.д.). При одном файле **splits** пустой; при разбитой модели в **splits** передают пути к частям. Вся дальнейшая логика (проверка бэкенда, колбэк прогресса, создание модели, вызов **llama_model_load**) сосредоточена в **llama_model_load_from_file_impl**.

**Параметры загрузки (**llama_model_params**), важные для понимания:**\
 \
 \- **use_mmap** — использовать отображение файла в память вместо чтения в буфер.\
 **Для чего:**\
 \
экономит RAM и ускоряет старт загрузки; файл остаётся открытым.\
 \- **use_direct_io** — прямой ввод-вывод.\
 **Для чего:**\
 \
для некоторых дисков и ОС даёт более предсказуемую скорость чтения.\
 \- **n_gpu_layers** — сколько слоёв загружать на GPU (остальные на CPU).\
 **Для чего:**\
 \
чтобы часть вычислений шла на видеокарте, часть на процессоре.\
 \- **progress_callback** — колбэк прогресса загрузки.\
 **Для чего:**\
 \
приложение может показывать прогресс-бар или отменять загрузку (вернуть false).\
 \- **vocab_only** — загрузить только словарь (без весов).\
 **Для чего:**\
 \
когда нужен только токенайзер, без тяжёлых весов. Полный список параметров — в **include/llama.h** в структуре **llama_model_params**.

## Шаг 2: Подготовка к чтению файла {#realizaciya-zagruzki-iz-fajla}

**Шаг 2 (продолжение)** — внутри точки входа выполняется подготовка к чтению файла. В **llama_model_load_from_file_impl** выполняется основная подготовка перед чтением файла.\
 **Для чего так:**\
 \
 \- перед загрузкой нужно убедиться, что есть бэкенд для вычислений, настроить отображение прогресса и создать объект модели;\
 \- только после этого вызывается внутренняя функция **llama_model_load**, которая читает файл по шагам.\
 Цитата начала функции (файл **src/llama.cpp**):

```cpp
static struct llama_model * llama_model_load_from_file_impl(
        const std::string & path_model,
        std::vector<std::string> & splits,
        struct llama_model_params params) {
    ggml_time_init();  // таймер для замеров времени загрузки
    // Если загружаем не только словарь — проверяем, что есть бэкенд (CPU или GPU)
    if (!params.vocab_only && ggml_backend_reg_count() == 0) {
        LLAMA_LOG_ERROR("%s: no backends are loaded...\n", __func__);
        return nullptr;
    }
    unsigned cur_percentage = 0;
    // Если колбэк прогресса не передан — подставляем свой: выводим точки до 100%
    if (params.progress_callback == NULL) {
        params.progress_callback_user_data = &cur_percentage;
        params.progress_callback = [](float progress, void * ctx) {
            unsigned * cur_percentage_p = (unsigned *) ctx;
            unsigned percentage = (unsigned) (100 * progress);
            while (percentage > *cur_percentage_p) {
                *cur_percentage_p = percentage;
                LLAMA_LOG_CONT(".");
                if (percentage >= 100) { LLAMA_LOG_CONT("\n"); }
            }
            return true;  // не отменяем загрузку
        };
    }
    llama_model * model = new llama_model(params);  // создаём объект модели
```

**Что происходит в этом фрагменте и для чего:**\
 \
 \- **ggml_time_init()** — включается таймер.\
 **Для чего:**\
 \
чтобы потом замерить, сколько заняла загрузка.\
 \- Проверка **ggml_backend_reg_count() == 0** — есть ли хотя бы один бэкенд (CPU или GPU).\
 **Для чего:**\
 \
без бэкенда нельзя будет выполнять вычисления модели; при загрузке только словаря (vocab_only) бэкенд не обязателен.\
 \- Настройка колбэка прогресса — если приложение не передало свой, подставляется колбэк по умолчанию, который выводит точки.\
 **Для чего:**\
 \
пользователь видит, что загрузка идёт; при желании можно передать свой колбэк и показывать прогресс-бар или отменять загрузку (вернуть false).\
 \- **new llama_model(params)** — создаётся объект модели с переданными параметрами.\
 **Для чего:**\
 \
в этот объект потом будут записаны архитектура, гиперпараметры, словарь и тензоры из файла.

Далее в той же функции формируется список устройств и вызывается **llama_model_load**. Цитата:

```cpp
// Список устройств: если не задан в params — берём все доступные (CPU + GPU)
std::vector<ggml_backend_dev_t> devs = params.devices.empty() ? ggml_backend_dev_get_all() : params.devices;
model->devices = devs;
// Внутренняя загрузка: читает файл, заполняет модель (arch, hparams, vocab, tensors)
int ret = llama_model_load(path_model, splits, *model, params);
if (ret != 0) {
    delete model;
    return nullptr;
}
return model;
```

\- **devs** — список устройств (CPU и видеокарты).\
 **Для чего:**\
 \
от него зависит, на какие устройства будут загружаться слои модели (см. load_tensors).\
 \- **llama_model_load(path_model, splits, \*model, params)** — читает файл и по шагам заполняет модель (загрузчик, load_arch, load_hparams, load_vocab, load_tensors).\
 **Для чего:**\
 \
вся логика чтения GGUF и заполнения модели сосредоточена в одной функции.\
 \- При ненулевом возврате модель удаляется и возвращается **nullptr**.\
 **Для чего:**\
 \
приложение по nullptr понимает, что загрузка не удалась, и не использует неполную модель.

Колбэк прогресса вызывается внутри **load_tensors** при чтении каждого тензора: ему передаётся число от 0.0 до 1.0 (доля загруженных данных). Если колбэк возвращает **false**, загрузка прерывается и **llama_model_load** возвращает -2 (отмена).\
 **Для чего:**\
 \
 \- приложение может отменить долгую загрузку или показывать прогресс-бар.

## Шаги 2–6: Пошаговая загрузка модели из файла {#vnutrennyaya-funkciya-llama-model-load}

**Шаги 2–6** выполняются внутри одной функции **llama_model_load**: создаётся загрузчик (шаг 2), затем по очереди вызываются **load_arch** (шаг 3), **load_hparams** (шаг 4), **load_vocab** (шаг 5), **load_tensors** (шаг 6).\
 **Для чего нужна функция** llama_model_load**:** она выполняет пошаговую загрузку модели из файла: создаёт загрузчик GGUF (открывает файл и строит карту тензоров), затем по очереди загружает архитектуру, гиперпараметры, словарь и тензоры. Порядок важен: архитектура задаёт набор ключей в GGUF; гиперпараметры читаются по этим ключам; словарь загружается с учётом архитектуры; тензоры создаются по известным размерам и заполняются из файла.

Цитата функции целиком (файл **src/llama.cpp**):

```cpp
// Возвращает 0 при успехе, -1 при ошибке, -2 при отмене (колбэк прогресса вернул false)
static int llama_model_load(const std::string & fname, std::vector<std::string> & splits, llama_model & model, llama_model_params & params) {
    model.t_load_us = 0;
    time_meas tm(model.t_load_us);  // замер времени загрузки
    model.t_start_us = tm.t_start_us;
    try {
        // Шаг 2 (продолжение): загрузчик открывает GGUF, читает заголовок и метаданные, строит карту тензоров (weights_map)
        llama_model_loader ml(fname, splits, params.use_mmap, params.use_direct_io, params.check_tensors, params.no_alloc, params.kv_overrides, params.tensor_buft_overrides);
        ml.print_info();
        model.hparams.vocab_only = params.vocab_only;
        model.hparams.no_alloc   = params.no_alloc;
        // Шаг 3: тип модели (LLaMA, Gemma и т.д.) — от него зависят имена полей в GGUF
        try { model.load_arch(ml); } catch(const std::exception & e) {
            throw std::runtime_error("error loading model architecture: " + std::string(e.what()));
        }
        // Шаг 4: размеры модели, длина контекста, число слоёв — нужны для выделения памяти и построения графа
        try { model.load_hparams(ml); } catch(const std::exception & e) {
            throw std::runtime_error("error loading model hyperparameters: " + std::string(e.what()));
        }
        if (model.arch == LLM_ARCH_CLIP) {
            throw std::runtime_error("CLIP cannot be used as main model, use it with --mmproj instead");
        }
        // Шаг 5: словарь и токенайзер — текст ↔ токены при вводе и выводе
        try { model.load_vocab(ml); } catch(const std::exception & e) {
            throw std::runtime_error("error loading model vocabulary: " + std::string(e.what()));
        }
        model.load_stats(ml);
        model.print_info();
        if (params.vocab_only) {
            LLAMA_LOG_INFO("%s: vocab only - skipping tensors\n", __func__);
            return 0;
        }
        // Шаг 6: веса модели из файла в память (или mmap) на CPU/GPU
        if (!model.load_tensors(ml)) { return -2; }
    } catch (const std::exception & err) {
        LLAMA_LOG_ERROR("%s: error loading model: %s\n", __func__, err.what());
        return -1;
    }
    return 0;
}
```

**Ошибки и отмена загрузки:**\
 \
 \- при ошибке в любом из шагов (load_arch, load_hparams, load_vocab, load_tensors) выбрасывается исключение; в блоке catch логируется сообщение и возвращается -1;\
 \- при отмене по колбэку прогресса (колбэк возвращает false внутри load_tensors) **load_tensors** возвращает false, исключение не выбрасывается, но **llama_model_load** возвращает -2 (отмена);\
 \- вызывающий код (**llama_model_load_from_file_impl**) при ненулевом возврате удаляет модель и возвращает **nullptr**;\
 \- таким образом, приложение может отменить долгую загрузку через колбэк и корректно освободить ресурсы.

**По шагам (что происходит и для чего):**\
 \
 \- сбрасывается время загрузки и запускается таймер.\
 \
 **Для чего:**\
 \
 \- чтобы потом можно было замерить, сколько заняла загрузка.\
 \
 \- создаётся загрузчик **llama_model_loader ml(...)** — он открывает GGUF-файл и читает метаданные, строит **weights_map**.\
 \
 **Для чего:**\
 \
 \- без загрузчика нельзя прочитать архитектуру, гиперпараметры, словарь и тензоры из файла.\
 \
 \- вызывается **ml.print_info()** — выводит в лог архитектуру, число тензоров, размер файла.\
 \
 **Для чего:**\
 \
 \- чтобы пользователь видел прогресс и информацию о файле.\
 \
 \- вызывается **model.load_arch(ml)** — определяем тип модели (LLaMA, Gemma, Qwen и т.д.).\
 \
 **Для чего:**\
 \
 \- от архитектуры зависят имена полей в GGUF и то, какие тензоры создавать.\
 \
 \- вызывается **model.load_hparams(ml)** — читаем размерности и параметры (длина контекста, число слоёв и т.п.).\
 \
 **Для чего:**\
 \
 \- чтобы знать «форму» модели и выделить под неё память.\
 \
 \- для CLIP выбросится ошибка — его используют отдельно как проектор, не как основную модель.\
 \
 \- вызывается **model.load_vocab(ml)** — загружаем словарь токенов.\
 \
 **Для чего:**\
 \
 \- чтобы потом превращать текст в числа (токены) и обратно при генерации.\
 \
 \- вызываются **load_stats(ml)** и **print_info** — выводят статистику. Если загружаем только словарь (**vocab_only == true**), на этом выход. Иначе вызывается **model.load_tensors(ml)** — читаются и раскладываются по памяти веса модели. При успехе возвращается 0, при отмене (колбэк прогресса вернул false) — -2, при ошибке — -1.

**Порядок вызовов при загрузке модели (сводка):**\
 \
 \- **llama_model_load_from_file** → **llama_model_load_from_file_impl**;\
 \- в impl: проверка бэкенда, колбэк прогресса, **new llama_model(params)**, формирование списка устройств, **llama_model_load(path_model, splits, \*model, params)**;\
 \- внутри **llama_model_load**: создание **llama_model_loader**, **load_arch**, **load_hparams**, **load_vocab**, **load_stats**, **print_info**, при необходимости **load_tensors**. Все эти шаги выполняются последовательно; при ошибке в любом из них загрузка прерывается.

Порядок вызовов **load_arch** → **load_hparams** → **load_vocab** важен: архитектура задаёт набор ключей GGUF; гиперпараметры читаются по этим ключам; словарь загружается с учётом архитектуры (например, имена ключей для токенайзера). Поэтому **vocab.load(ml, kv)** вызывается именно после **load_hparams(ml)** — к этому моменту и архитектура, и гиперпараметры уже известны, и загрузчик может корректно прочитать тип токенайзера, списки токенов и слияния.

## Шаг 2: Загрузчик GGUF — открытие файла и карта тензоров {#zagruzchik-gguf-llama-model-loader}

**Шаг 2** (внутри **llama_model_load**) — создаётся загрузчик GGUF: открывается файл, читаются заголовок и метаданные, строится карта тензоров.\
 **Для чего нужен загрузчик GGUF:** чтобы открыть файл модели, прочитать заголовок и метаданные (без самих весов) и построить «карту» — по имени тензора знать, где в файле лежат его данные и какого он размера. Без этой карты нельзя потом загружать веса по одному тензору. **Тензоры** здесь — многомерные массивы чисел (матрицы и векторы), в которых хранятся веса нейросети; каждый слой модели — это несколько тензоров, и загрузчик должен знать для каждого имя, размер и смещение в файле.

Создание загрузчика — вызов конструктора **llama_model_loader**. В коде **llama_model_load** это выглядит так:

```cpp
// Конструктор загрузчика (шаг 2): открывает GGUF, читает заголовок и метаданные, строит карту тензоров
llama_model_loader::llama_model_loader(
        const std::string & fname,
        std::vector<std::string> & splits,
        bool use_mmap,
        bool use_direct_io,
        bool check_tensors,
        bool no_alloc,
        const llama_model_kv_override * param_overrides_p,
        const llama_model_tensor_buft_override * param_tensor_buft_overrides_p) {
    // no_alloc = true: читаем только заголовок и метаданные, данные тензоров пока не загружаем
    struct ggml_context * ctx = NULL;
    struct gguf_init_params params = {
        /*.no_alloc = */ true,
        /*.ctx      = */ &ctx,
    };
    // Заголовок + метаданные (ключ–значение) в meta; в ctx — список тензоров с именами, типами, смещениями в файле
    meta.reset(gguf_init_from_file(fname.c_str(), params));
    if (!meta) {
        throw std::runtime_error(format("%s: failed to load model from %s", __func__, fname.c_str()));
    }
    // Имя архитектуры (llama, qwen2 и т.д.) — от него зависят имена полей при load_hparams и load_vocab
    get_key(llm_kv(LLM_KV_GENERAL_ARCHITECTURE), arch_name, false);
    llm_kv = LLM_KV(llm_arch_from_string(arch_name));
    // Файл открыт для чтения — потом по смещениям из weights_map будем читать или маппить данные тензоров
    files.emplace_back(new llama_file(fname.c_str(), "rb", use_direct_io));
    contexts.emplace_back(ctx);
    // Карта: имя тензора → (файл, смещение, метаданные, тензор); при load_tensor_data по имени найдём, откуда читать
    for (ggml_tensor * cur = ggml_get_first_tensor(ctx); cur; cur = ggml_get_next_tensor(ctx, cur)) {
        std::string tensor_name = std::string(cur->name);
        if (weights_map.find(tensor_name) != weights_map.end()) {
            throw std::runtime_error(format("invalid model: tensor '%s' is duplicated", ggml_get_name(cur)));
        }
        n_elements += ggml_nelements(cur);
        n_bytes    += ggml_nbytes(cur);
        weights_map.emplace(tensor_name, llama_tensor_weight(files.back().get(), 0, meta.get(), cur));
    }
}
```

В конструкторе **gguf_init_from_file** читает заголовок GGUF и метаданные (без самих весов) — так получают список тензоров и пары ключ–значение (архитектура, гиперпараметры) без чтения тяжёлых данных в память. Из метаданных берётся имя архитектуры (**general.architecture**) — от него зависят имена остальных полей в GGUF (у разных моделей разные ключи). Файл открывается для чтения; потом по смещениям из **weights_map** будут читаться или маппиться данные тензоров. По списку тензоров из GGUF для каждого в **weights_map** записывается имя, размер и смещение в файле — при вызове **load_tensor_data** загрузчик по имени найдёт тензор в карте и прочитает данные по смещению.

Структура **llama_tensor_weight** (элемент **weights_map**) хранит ссылку на файл, смещение в файле, ссылку на метаданные GGUF и указатель на тензор в контексте GGUF — по ним загрузчик потом читает байты в буфер тензора. Метод **ml.get_key(...)** читает из метаданных GGUF значение по ключу (имя ключа зависит от архитектуры — его возвращает **kv(...)**). Так загрузчик получает, например, имя архитектуры, тип токенайзера, гиперпараметры.

После создания загрузчика в **llama_model_load** вызывается **ml.print_info()**. Цитата из кода:

```cpp
// В llama_model_load после создания llama_model_loader:
ml.print_info();  // выводит в лог архитектуру, число тензоров, размер файла — для отладки и информации пользователю
```

Функция **gguf_init_from_file** (библиотека GGUF) открывает файл и читает заголовок:\
 \
 \- версию формата;\
 \- число ключей метаданных;\
 \- число тензоров.\
 Метаданные читаются в контекст **gguf_context** (пары ключ–значение); сами данные тензоров на этом шаге не загружаются — только имена, типы и смещения в файле. Метод **print_info** (**src/llama-model-loader.cpp**) выводит в лог информацию о файле:\
 \
 \- архитектуру;\
 \- количество тензоров;\
 \- размер в байтах.

## Формат GGUF (справочно) {#format-gguf-kratko}

GGUF (GPT-Generated Unified Format) — бинарный формат для хранения моделей машинного обучения.\
 **Для чего он нужен:**\
 \
 \- чтобы в одном файле хранить и веса модели, и метаданные (размеры, тип архитектуры), и словарь;\
 \- загрузчик по заголовку и метаданным строит «карту» и потом по запросу читает нужные куски файла или маппит их в память (mmap).

**Структура заголовка GGUF:**\
 \
 \- магическое число (идентификация формата — по нему понимают, что это GGUF);\
 \- версия формата (для совместимости при изменениях формата);\
 \- число ключей метаданных (n_kv);\
 \- число тензоров (n_tensors).

**Метаданные** хранятся как массив пар «ключ — значение». Для каждой пары записаны:\
 \
 \- тип ключа (строка, число, массив и т.д.);\
 \- имя ключа;\
 \- значение (имя архитектуры, размерности, параметры RoPE, тип токенайзера, списки токенов и т.д.).

**Тензоры в файле** идут после метаданных. Для каждого тензора записаны:\
 \
 \- имя;\
 \- тип элемента (F32, F16, Q8_0, Q4_K_M и т.д. — от полной точности до квантизованных форматов);\
 \- размерности (например, \[n_layer, n_embd, n_embd\]);\
 \- смещение в файле, по которому начинаются данные.

**Зачем это загрузчику:** по этой информации он строит **weights_map** (карту «имя тензора → где в файле лежат данные») и при вызове **load_tensor_data** читает или маппит соответствующий участок файла.

**Версия формата и типы элементов:**\
 \
 \- версия формата задаётся в заголовке GGUF.\
 \
 **Для чего:**\
 \
 \- при изменениях формата версия позволяет загрузчику понять, с какой версией он работает; при несовместимости загрузчик может выдать ошибку или проигнорировать неизвестные поля.\
 \
 \- по типу элемента тензора (F32, F16, Q8_0, Q4_K_M и т.д.) загрузчик знает, сколько байт занимает один элемент и как интерпретировать данные при копировании в буфер или при mmap.\
 \
 **Для чего:**\
 \
 \- без этого нельзя корректно прочитать или отобразить в память данные тензора; разные типы имеют разный размер и разную интерпретацию байтов.

Типы данных тензоров в GGUF задают, как интерпретировать байты: F32, F16, Q8_0, Q4_K_M и т.д. — от полной точности до квантизованных форматов. Квантизация уменьшает размер модели и ускоряет вычисления за счёт приближённого представления весов. Движок при загрузке создаёт тензоры в нужном формате и копирует или маппит данные из файла в буферы на CPU или GPU.

Чтение метаданных GGUF выполняется через функции библиотеки gguf:\
 \
 \- **gguf_get_n_kv** — число ключей;\
 \- **gguf_get_key** — имя ключа по индексу;\
 \- **gguf_get_kv_type** — тип значения (строка, число, массив и т.д.);\
 \- **gguf_get_val_\*** — значение по индексу или по имени.\
 Загрузчик модели оборачивает это в метод **get_key(key, value)** с учётом архитектуры: ключ преобразуется в имя поля в GGUF (например, **llama.embedding_length** для LLaMA).

## Шаг 3: Определение типа модели (архитектура) {#zagruzka-arhitektury-load-arch}

**Шаг 3** — определение типа модели (LLaMA, Gemma, Qwen и т.д.) по имени архитектуры из GGUF. Метод **llama_model::load_arch** определяет тип модели по имени из GGUF. **Для чего это нужно:** от типа архитектуры (LLaMA, Gemma, Qwen и т.д.) зависят имена полей в метаданных GGUF и то, какие тензоры создавать при загрузке весов; без этого нельзя корректно прочитать гиперпараметры и словарь. Цитата (файл **src/llama-model.cpp**):

```cpp
// Шаг 3: по имени архитектуры из GGUF выбираем тип модели — от него зависят ключи при load_hparams и load_vocab
void llama_model::load_arch(llama_model_loader & ml) {
    arch = ml.get_arch();  // читает general.architecture (строка) и превращает в enum: LLM_ARCH_LLAMA, LLM_ARCH_GEMMA и т.д.
    if (arch == LLM_ARCH_UNKNOWN) {
        throw std::runtime_error("unknown model architecture: '" + ml.get_arch_name() + "'");
    }
}
```

**Что происходит и для чего:**\
 \
 \- **ml.get_arch()** читает из метаданных GGUF ключ **general.architecture** (строка) и преобразует его в enum **llm_arch**.\
 **Для чего:**\
 \
дальше по этому enum выбираются имена полей для гиперпараметров и словаря (у разных архитектур — разные ключи в GGUF).\
 \- Если тип неизвестен (**LLM_ARCH_UNKNOWN**), выбрасывается ошибка.\
 **Для чего:**\
 \
движок не умеет работать с неизвестной архитектурой; приложение получит сообщение об ошибке и сможет сообщить пользователю.

**Реализация** get_arch() **в загрузчике (**src/llama-model-loader.cpp**):**

```cpp
// Вспомогательный метод загрузчика: читает general.architecture из GGUF и превращает строку в enum
llm_arch llama_model_loader::get_arch() const {
    std::string arch_name;
    get_key(llm_kv(LLM_KV_GENERAL_ARCHITECTURE), arch_name, false);  // ключ из meta (метаданные GGUF)
    return llm_arch_from_string(arch_name);  // "llama" → LLM_ARCH_LLAMA, "qwen2" → LLM_ARCH_QWEN и т.д.
}
```

**Что делают эти вызовы:**\
 \
 \- **get_key(llm_kv(LLM_KV_GENERAL_ARCHITECTURE), arch_name, false)** — из метаданных GGUF читается значение по ключу «общая архитектура» (например, **llama**, **qwen2**) и записывается в **arch_name**.\
 **Для чего:**\
 \
без имени архитектуры нельзя выбрать набор ключей для гиперпараметров и словаря.\
 \- **llm_arch_from_string(arch_name)** — строка превращается в enum **llm_arch** (LLM_ARCH_LLAMA, LLM_ARCH_QWEN и т.д.).\
 **Для чего:**\
 \
по enum дальше выбираются имена полей в GGUF для **load_hparams** и **load_vocab**.

\- Примеры значений enum: **LLM_ARCH_LLAMA**, **LLM_ARCH_GEMMA**, **LLM_ARCH_QWEN**, **LLM_ARCH_PHI**, **LLM_ARCH_MISTRAL**, **LLM_ARCH_CLIP** и т.д. Для каждой архитектуры задан свой набор ключей GGUF (**LLM_KV**), по которым загрузчик читает имена полей (гиперпараметры, словарь, имена тензоров).\
 **Для чего:**\
 \
без правильного набора ключей нельзя прочитать гиперпараметры и словарь из GGUF.\
 \- Имена тензоров в GGUF зависят от архитектуры: для LLaMA — blk.N.attn_q.weight, blk.N.attn_k.weight и т.д.; для других моделей — свои префиксы и суффиксы. При добавлении поддержки новой архитектуры в код добавляется новый enum и набор ключей LLM_KV. Гиперпараметры используются при создании тензоров в load_tensors (размеры матриц, число слоёв) и при создании контекста (n_ctx, n_batch, параметры RoPE и т.д.).

## Шаг 4: Загрузка гиперпараметров — размеры и контекст {#zagruzka-giperparametrov-load-hparams}

**Шаг 4** — загрузка гиперпараметров (размеры модели, длина контекста, число слоёв и т.д.) из метаданных GGUF. Метод **llama_model::load_hparams** заполняет структуру **hparams** (гиперпараметры) из метаданных GGUF. **Для чего это нужно:** гиперпараметры задают «форму» модели — длину контекста, число слоёв, размер эмбеддинга, число голов внимания и т.п.; без них нельзя выделить память под тензоры и построить вычислительный граф. Цитата с основными ключами (файл **src/llama-model.cpp**):

```cpp
// Шаг 4: читаем гиперпараметры из GGUF — размеры модели, длина контекста, число слоёв; от них зависят выделение памяти и граф
void llama_model::load_hparams(llama_model_loader & ml) {
    const gguf_context * ctx = ml.meta.get();
    // Сохраняем все пары ключ–значение из GGUF (кроме массивов) для справки
    for (int i = 0; i < gguf_get_n_kv(ctx); i++) {
        gguf_type type = gguf_get_kv_type(ctx, i);
        if (type == GGUF_TYPE_ARRAY) {
            continue;
        }
        const char * name = gguf_get_key(ctx, i);
        const std::string value = gguf_kv_to_str(ctx, i);
        gguf_kv.emplace(name, value);
    }
    ml.get_key(LLM_KV_GENERAL_NAME, name, false);
    if (hparams.vocab_only || ml.get_arch() == LLM_ARCH_CLIP) {
        return;
    }
    // Основные размеры (имена ключей зависят от архитектуры — load_arch уже вызван)
    ml.get_key(LLM_KV_CONTEXT_LENGTH,          hparams.n_ctx_train);   // длина контекста при обучении
    ml.get_key(LLM_KV_EMBEDDING_LENGTH,        hparams.n_embd);       // размер эмбеддинга (скрытого слоя)
    ml.get_key(LLM_KV_EMBEDDING_LENGTH_OUT,    hparams.n_embd_out_impl, false);
    ml.get_key(LLM_KV_BLOCK_COUNT,             hparams.n_layer);      // число слоёв трансформера
    ml.get_key(LLM_KV_EXPERT_COUNT,            hparams.n_expert,        false);  // для MoE-моделей
    ml.get_key_or_arr(LLM_KV_FEED_FORWARD_LENGTH,  hparams.n_ff_arr,   hparams.n_layer, false);  // размер FF по слоям
    ml.get_key_or_arr(LLM_KV_ATTENTION_HEAD_COUNT, hparams.n_head_arr, hparams.n_layer, false);  // число голов внимания
    hparams.n_head_kv_arr = hparams.n_head_arr;
    ml.get_key_or_arr(LLM_KV_ATTENTION_HEAD_COUNT_KV, hparams.n_head_kv_arr, hparams.n_layer, false);  // число KV-голов (GQA)
    ml.get_key(LLM_KV_ROPE_FREQ_BASE, hparams.rope_freq_base_train, false);  // базовая частота RoPE
    // rope_scaling_type, rope_freq_scale и т.д.
}
```

**Что происходит и для чего:**\
 \
 \- В цикле читаются все пары ключ–значение из GGUF (кроме массивов) и сохраняются в **gguf_kv**.\
 \
 **Для чего:**\
 \
 \- чтобы потом при необходимости можно было обратиться к любому полю по имени.\
 \- Затем по ключам, зависящим от архитектуры, заполняются поля **hparams**:\
 \- **n_ctx_train** — длина контекста при обучении.\
 \
 **Для чего:**\
 \
 \- от неё зависит размер KV-cache и максимальная длина ввода.\
 \- **n_embd** — размер эмбеддинга (скрытого слоя).\
 \
 **Для чего:**\
 \
 \- от него зависят размеры матриц весов.\
 \- **n_layer** — число слоёв трансформера.\
 \
 **Для чего:**\
 \
 \- от него зависит, сколько тензоров создавать в load_tensors.\
 \- **n_ff_arr**, **n_head_arr**, **n_head_kv_arr** — размеры feed-forward и число голов внимания по слоям.\
 \
 **Для чего:**\
 \
 \- от них зависят размеры матриц Q, K, V и feed-forward.\
 \- **rope_freq_base_train** и др. — параметры RoPE (позиционного кодирования).\
 \
 **Для чего:**\
 \
 \- они задают, как в графе применяются позиционные кодировки. В итоге **hparams** полностью описывает размеры модели — этого достаточно, чтобы выделить память под тензоры и построить вычислительный граф.

Часть ключей GGUF для гиперпараметров (в зависимости от архитектуры):\
 \
 \- **llama.context_length** — длина контекста при обучении;\
 \- **llama.embedding_length** — размер скрытого слоя (эмбеддинга);\
 \- **llama.block_count** — число слоёв трансформера;\
 \- **llama.attention.head_count** — число голов внимания;\
 \- **llama.attention.head_count_kv** — число KV-голов (для GQA);\
 \- **llama.feed_forward_length** — размер промежуточного слоя feed-forward;\
 \- **llama.rope.freq_base** — базовая частота RoPE.\
 Для MoE-моделей добавляются ключи числа экспертов и т.д. Метод **get_key_or_arr** читает либо одно значение, либо массив по слоям (когда размеры различаются по слоям). После загрузки гиперпараметров модель знает «форму» всех тензоров — этого достаточно для выделения буферов и построения графа при load_tensors и при создании контекста.

## Шаг 5: Загрузка словаря и токенайзера {#zagruzka-slovarya-load-vocab}

**Шаг 5** — загрузка словаря и токенайзера: по словарю текст превращается в токены при вводе и обратно в текст при выводе. **Словарь токенов** — таблица соответствия между кусочками текста и целыми числами (токенами); по нему текст режется на токены при вводе и обратно собирается в текст при выводе. Загрузка словаря выполняется **после** загрузки архитектуры и гиперпараметров: в **llama_model_load** сначала вызываются **load_arch(ml)** и **load_hparams(ml)**, затем **load_vocab(ml)**. Внутри **load_vocab** вызывается **vocab.load(ml, kv)**. Он загружается в **llama_model::load_vocab** (**src/llama-model.cpp**):

```cpp
void llama_model::load_vocab(llama_model_loader & ml) {
    const auto kv = LLM_KV(arch);

    vocab.load(ml, kv);
}
```

Для текущей архитектуры берётся набор ключей словаря, после чего вызывается **vocab.load(ml, kv)**. Реализация — метод **llama_vocab::impl::load** в **src/llama-vocab.cpp**. Из GGUF читаются:\
 \
 \- тип токенайзера (SPM, BPE, WPM и т.д.);\
 \- списки токенов;\
 \- типы токенов;\
 \- слияния BPE (если есть);\
 \- специальные токены — всё, что нужно, чтобы превращать текст в последовательность чисел (токенов) и обратно.\
 После этого модель умеет токенизировать промпт и переводить сгенерированные токены обратно в текст.

Вызов **vocab.load(ml, kv)** выполняется внутри **llama_model::load_vocab** после того, как для текущей архитектуры получен набор ключей **kv** (через **LLM_KV(arch)**). Загрузчик **ml** уже открыл GGUF и прочитал метаданные; архитектура и гиперпараметры к этому моменту загружены в **load_arch** и **load_hparams**. Начало реализации **llama_vocab::impl::load** (файл **src/llama-vocab.cpp**):

```cpp
void llama_vocab::impl::load(llama_model_loader & ml, const LLM_KV & kv) {
    struct gguf_context * ctx = ml.meta.get();
    // Тип токенайзера: llama → SPM, gpt2 → BPE и т.д.
    ml.get_key(LLM_KV_TOKENIZER_MODEL, tokenizer_model);
    ml.get_key(LLM_KV_TOKENIZER_PRE, tokenizer_pre, false);
    ml.get_key(LLM_KV_TOKENIZER_TOKEN_TYPE_COUNT, n_token_types, false);
    if (tokenizer_model == "no_vocab" || tokenizer_model == "none") {
        type = LLAMA_VOCAB_TYPE_NONE;
        special_bos_id = LLAMA_TOKEN_NULL;
        special_eos_id = LLAMA_TOKEN_NULL;
        special_unk_id = LLAMA_TOKEN_NULL;
        return;
    }
    if (tokenizer_model == "llama") {
        type = LLAMA_VOCAB_TYPE_SPM;  // SentencePiece-подобный
        special_bos_id = 1;
        special_eos_id = 2;
        special_unk_id = 0;
    } else if (tokenizer_model == "gpt2") {
        type = LLAMA_VOCAB_TYPE_BPE;
        // Читаем слияния BPE (пары подстрок для жадного слияния)
        const int merges_keyidx = gguf_find_key(ctx, kv(LLM_KV_TOKENIZER_MERGES).c_str());
        const int n_merges = gguf_get_arr_n(ctx, merges_keyidx);
        for (int i = 0; i < n_merges; i++) {
            const std::string word = gguf_get_arr_str(ctx, merges_keyidx, i);
            std::string first, second;
            const size_t pos = word.find(' ', 1);
            if (pos != std::string::npos) {
                first = word.substr(0, pos);
                second = word.substr(pos + 1);
            }
            bpe_ranks.emplace(std::make_pair(first, second), i);
        }
    }
    // Массив токенов: для каждого ID — строка (кусочек текста)
    const int token_idx = gguf_find_key(ctx, kv(LLM_KV_TOKENIZER_LIST).c_str());
    if (token_idx == -1) {
        throw std::runtime_error("cannot find tokenizer vocab in model file\n");
    }
    uint32_t n_tokens = gguf_get_arr_n(ctx, token_idx);
    id_to_token.resize(n_tokens);
    for (uint32_t i = 0; i < n_tokens; i++) {
        std::string word = gguf_get_arr_str(ctx, token_idx, i);
        token_to_id[word] = i;   // текст → ID
        max_token_len = std::max(max_token_len, (int) word.size());
        auto & token_data = id_to_token[i];
        token_data.text = std::move(word);   // ID → текст
        token_data.score = scores ? scores[i] : 0.0f;
        token_data.attr = LLAMA_TOKEN_ATTR_NORMAL;
    }
    init_tokenizer(type);  // инициализация токенайзера по типу (SPM, BPE и т.д.)
}
```

В коде видно: определение типа словаря по **tokenizer_model** (llama → SPM, gpt2 → BPE с чтением слияний), поиск массива токенов в GGUF, цикл по всем токенам — заполнение **id_to_token** и **token_to_id**, вызов **init_tokenizer(type)** для инициализации токенайзера (SPM, BPE и т.д.). Далее в той же функции читаются специальные токены (BOS, EOS, UNK и т.п.) и настраиваются флаги **add_bos**, **add_eos**.

**Специальные токены:**\
 \
 \- BOS (begin of sequence) — токен начала последовательности, при необходимости добавляется перед промптом.\
 \- EOS (end of sequence) — токен конца вывода, по нему приложение прекращает генерацию.\
 \- UNK (unknown) — токен для неизвестных или не входящих в словарь символов.\
 \
 Их ID хранятся в **special_bos_id**, **special_eos_id**, **special_unk_id**. В GGUF для словаря могут быть ключи вроде **tokenizer.ggml.bos_token_id**, **tokenizer.ggml.eos_token_id** и т.д. — они читаются в конце **llama_vocab::impl::load** и записываются в соответствующие поля. Флаги **add_bos** и **add_eos** задают, добавлять ли эти токены автоматически при токенизации (зависит от модели и формата чата). После **init_tokenizer(type)** словарь готов к токенизации и обратному переводу токенов в текст; эти операции используются при каждом запросе пользователя.

## Шаг 6: Загрузка весов в память или на GPU {#zagruzka-tensorov-load-tensors}

**Шаг 6** — веса модели (тензоры) загружаются из файла в память или на GPU. Метод **llama_model::load_tensors** (**src/llama-model.cpp**) создаёт в памяти тензоры модели (те самые массивы весов), назначает им буферы — участки памяти на CPU или видеокарте — и заполняет их данными из GGUF-файла. Ниже — цитаты начала метода и ключевых фрагментов.

```cpp
bool llama_model::load_tensors(llama_model_loader & ml) {
    const auto & split_mode   = params.split_mode;
    const auto & use_mlock    = params.use_mlock;
    const auto & tensor_split = params.tensor_split;
    const int n_layer      = hparams.n_layer;
    const int n_gpu_layers = this->n_gpu_layers();  // сколько слоёв загружать на GPU
    const bool use_mmap_buffer = true;
    LLAMA_LOG_INFO("%s: loading model tensors, this can take a while... (mmap = %s, direct_io = %s)\n",
        __func__, ml.use_mmap ? "true" : "false", ml.use_direct_io ? "true" : "false");
    // Списки типов буферов для CPU и для каждой видеокарты — от них зависит, куда попадут тензоры
    pimpl->cpu_buft_list = make_cpu_buft_list(devices, params.use_extra_bufts, params.no_host);
    for (auto * dev : devices) {
        buft_list_t buft_list = make_gpu_buft_list(dev, split_mode, tensor_split);
        buft_list.insert(buft_list.end(), pimpl->cpu_buft_list.begin(), pimpl->cpu_buft_list.end());
        pimpl->gpu_buft_list.emplace(dev, std::move(buft_list));
    }
    ggml_backend_dev_t cpu_dev = ggml_backend_dev_by_type(GGML_BACKEND_DEVICE_TYPE_CPU);
    // Точки разбиения слоёв по устройствам (по свободной памяти или tensor_split)
    const int i_gpu_start = std::max(int(hparams.n_layer) + 1 - n_gpu_layers, 0);
    const int act_gpu_layers = devices.empty() ? 0 : std::min(n_gpu_layers, int(n_layer) + 1);
    // Для номера слоя il возвращаем устройство и список буферов: вход всегда CPU, слои — по разбиению
    auto get_layer_buft_list = [&](int il) -> llama_model::impl::layer_dev {
        const bool is_swa = il < int(hparams.n_layer) && hparams.is_swa(il);
        if (il < i_gpu_start || (il - i_gpu_start) >= act_gpu_layers) {
            return {cpu_dev, &pimpl->cpu_buft_list};
        }
        const int layer_gpu = std::upper_bound(splits.begin(), splits.begin() + n_devices(), float(il - i_gpu_start)/act_gpu_layers) - splits.begin();
        auto * dev = devices.at(layer_gpu);
        return {dev, &pimpl->gpu_buft_list.at(dev)};
    };
    pimpl->dev_input = { cpu_dev, &pimpl->cpu_buft_list };   // входные тензоры всегда на CPU
    pimpl->dev_layer.resize(n_layer);
    for (int il = 0; il < n_layer; ++il) {
        pimpl->dev_layer[il] = get_layer_buft_list(il);  // каждый слой — на CPU или на одной из GPU
    }
    pimpl->dev_output = get_layer_buft_list(n_layer);  // выход по тому же разбиению
```

**Что происходит в начале load_tensors и для чего:**\
 \
 \- формируются списки типов буферов для CPU и для каждой видеокарты \
 **Для чего:**\
 \
от них зависит, на какие устройства будут размещены тензоры модели — входные обычно на CPU, слои по разбиению на CPU/GPU;\
 \- по настройкам (**tensor_split**) или по свободной памяти решается, какие слои считать на CPU, какие на GPU \
 **Для чего:**\
 \
чтобы равномерно загрузить устройства и не переполнить память одной видеокарты.

Функция **get_layer_buft_list(il)** для номера слоя возвращает устройство и список буферов: вход всегда на CPU, повторяющиеся слои могут быть на CPU или GPU, выход — по разбиению. Далее в цикле создаются тензоры: эмбеддинги, выход, для каждого слоя — матрицы внимания (Q/K/V/O), нормализации, feed-forward. Каждый тензор создаётся в нужном буфере, данные читаются из GGUF (**ml.load_all_data(...)** или mmap). В итоге веса модели оказываются в памяти (и при необходимости на видеокарте), модель готова к генерации текста.

Реализация чтения весов из файла — вызовы **ml.get_tensor(name)** и **ml.load_tensor_data(tensor)** (или mmap) в **src/llama-model-loader.cpp**: по имени тензора из **weights_map** берётся смещение и размер, данные копируются в буфер тензора на CPU или GPU. Для больших моделей используется mmap, чтобы не дублировать данные в оперативной памяти.

**Цитата метода** load_tensor_data **(загрузчик,** src/llama-model-loader.cpp**):**

```cpp
// Шаг 6 (деталь): загружает данные одного тензора — по карте weights_map знаем смещение в файле
void llama_model_loader::load_tensor_data(ggml_tensor * tensor) {
    auto it = weights_map.find(ggml_get_name(tensor));
    if (it == weights_map.end()) return;
    llama_tensor_weight & w = it->second;
    // mmap: участок файла отображаем в память (данные не копируются — читаются по обращению). Иначе — читаем байты в буфер
    if (use_mmap) {
        ggml_backend_tensor_set_from_file(tensor, w.file, w.offset);
    } else {
        w.file->seek(w.offset, SEEK_SET);
        w.file->read_raw(tensor->data, ggml_nbytes(tensor));
    }
}
```

**Что происходит в** load_tensor_data **и для чего:**\
 \
 \- по имени тензора ищется запись в **weights_map** (карта строится в конструкторе загрузчика).\
 **Для чего:**\
 \
без карты нельзя узнать смещение и размер данных тензора в файле.\
 \- при **use_mmap == true** участок файла отображается в память через **ggml_backend_tensor_set_from_file** — данные не копируются, читаются по мере обращения.\
 **Для чего:**\
 \
экономия RAM и ускорение старта загрузки.\
 \- при **use_mmap == false** файл позиционируется на **w.offset** и байты читаются в **tensor->data**.\
 **Для чего:**\
 \
полная копия данных в буфер (нужно, если файл будет закрыт или данные будут меняться).

Типичный фрагмент загрузки тензоров в цикле по слоям (логика из **llama_model::load_tensors** в **src/llama-model.cpp**): для каждого тензора модели (эмбеддинги, веса слоёв внимания и feed-forward) вызывается **ml.get_tensor(name)** — загрузчик возвращает указатель на тензор GGML с привязанным буфером; затем **ml.load_tensor_data(tensor)** читает данные из файла в этот буфер (или настраивает mmap). Имена тензоров зависят от архитектуры (например, **blk.0.attn_q.weight**, **blk.0.attn_k.weight** и т.д.). После прохода по всем тензорам веса модели полностью находятся в памяти (и при необходимости на GPU).

```cpp
// Упрощённая схема загрузки тензоров в load_tensors (логика)
for (const auto & it : ml.weights_map) {
    const std::string & name = it.first;
    ggml_tensor * cur = ggml_get_tensor(ml.ctx_gguf, name.c_str());
    if (cur == nullptr) continue;
    ggml_tensor * tensor = ml.get_tensor(name);
    if (tensor == nullptr) continue;
    ml.load_tensor_data(tensor);
    // тензор уже привязан к буферу на CPU или GPU, данные прочитаны или замаплены
}
```

Функция **get_tensor** в загрузчике по имени находит запись в **weights_map**, создаёт тензор GGML в нужном бэкенде (CPU или GPU по разбиению слоёв) и возвращает указатель. **load_tensor_data** по смещению в файле читает байты в буфер тензора или настраивает отображение файла в память (mmap). Так загружаются все веса: эмбеддинги, матрицы Q/K/V/O, нормализации, feed-forward по каждому слою.

При использовании mmap данные тензоров не копируются в оперативную память — вместо этого отображается участок файла. Это уменьшает потребление RAM и ускоряет старт загрузки, но требует, чтобы файл оставался открытым на время работы с моделью. При **use_direct_io** чтение идёт с выравниванием и параметрами, подходящими для прямого доступа к диску. Разбиение по слоям (**tensor_split** или по свободной памяти GPU) задаёт, какие слои загружать на какое устройство — так можно распределить большую модель по нескольким видеокартам. После завершения load_tensors модель полностью готова к инференсу: все веса находятся в памяти (и при необходимости на GPU), и можно создавать контекст и вызывать llama_decode.

## Шаг 7: Создание контекста инференса {#sozdanie-konteksta-dlya-inferensa}

**Шаг 7** — создание контекста инференса (KV-cache, планировщик, зарезервированные графы). Загруженная модель хранит только веса (тензоры). Чтобы генерировать текст, нужен **контекст инференса** — объект **llama_context**.\
 **Для чего он нужен:**\
 \
 \- контекст — это «рабочая среда» одного сеанса генерации: в нём задаётся, сколько токенов модель может «помнить» (размер контекста), как большими порциями подавать данные (размер батча), параметры RoPE и тип внимания;\
 \- без контекста нельзя вызвать **llama_decode** — именно контекст хранит KV-cache, планировщик и зарезервированные графы.\
 Реализация — в файле **src/llama-context.cpp**.

**Что создаётся при создании контекста и для чего:**\
 \
 \- Планировщик (**sched**) — решает, на каком устройстве (CPU или видеокарта) выполнять каждый узел вычислительного графа.\
 **Для чего:**\
 \
чтобы распределить вычисления по CPU и GPU и выделить под граф буферы на нужных устройствах.\
 \- Память KV-cache (**memory**) — буферы под ключи и значения механизма внимания.\
 **Для чего:**\
 \
в них хранятся уже посчитанные ключи и значения по всем предыдущим позициям; без кэша при каждом новом токене пришлось бы заново считать K и V для всей истории, что очень медленно (подробнее в разделе «KV-cache»).\
 \- Аллокатор батча (**balloc**) — заполняет позиции и флаги логитов в батче при необходимости.\
 **Для чего:**\
 \
чтобы вызывающему коду не нужно было вручную выставлять позиции и решать, для каких позиций считать логиты.\
 \- Зарезервированные графы (Prefill и Decode) — временные графы и буферы под них.\
 **Для чего:**\
 \
при первом вызове decode граф не строится с нуля и не выделяется память «на лету» — это уменьшает задержку первого ответа.

Начало конструктора:

```cpp
llama_context::llama_context(
        const llama_model & model,
              llama_context_params params) :
    model(model),
    balloc(std::make_unique<llama_batch_allocr>(model.hparams.n_pos_per_embd())) {
    LLAMA_LOG_INFO("%s: constructing llama_context\n", __func__);
    t_start_us = model.t_start_us;
    t_load_us  = model.t_load_us;
    const auto & hparams = model.hparams;
    cparams.n_seq_max = std::max(1u, params.n_seq_max);
    if (cparams.n_seq_max > LLAMA_MAX_SEQ) {
        throw std::runtime_error("n_seq_max must be <= " + std::to_string(LLAMA_MAX_SEQ));
    }
    cparams.n_threads        = params.n_threads;
    cparams.n_threads_batch  = params.n_threads_batch;
    cparams.yarn_ext_factor  = params.yarn_ext_factor  >= 0.0f ? params.yarn_ext_factor  : hparams.yarn_ext_factor;
    cparams.yarn_attn_factor = params.yarn_attn_factor >= 0.0f ? params.yarn_attn_factor : hparams.yarn_attn_factor;
    // ...
    cparams.n_ctx            = params.n_ctx == 0 ? hparams.n_ctx_train : params.n_ctx;
    cparams.rope_freq_base   = params.rope_freq_base  == 0.0f ? hparams.rope_freq_base_train  : params.rope_freq_base;
    cparams.rope_freq_scale  = params.rope_freq_scale == 0.0f ? hparams.rope_freq_scale_train : params.rope_freq_scale;
    // ...
    cparams.causal_attn = ...;
    cparams.n_batch = cparams.causal_attn ? std::min(cparams.n_ctx, params.n_batch) : params.n_batch;
    cparams.n_ubatch = std::min(cparams.n_batch, params.n_ubatch == 0 ? params.n_batch : params.n_ubatch);
    // ...
    LLAMA_LOG_INFO("%s: n_ctx         = %u\n", __func__, cparams.n_ctx);
    LLAMA_LOG_INFO("%s: n_batch       = %u\n", __func__, cparams.n_batch);
    LLAMA_LOG_INFO("%s: n_ubatch      = %u\n", __func__, cparams.n_ubatch);
    // Инициализация бэкендов (GPU, CPU), memory (KV-cache), sched (планировщик)
}
```

**Что задаётся в конструкторе контекста:**\
 \
 \- сохраняются ссылка на модель и время загрузки;\
 \- задаются максимальное число последовательностей (**n_seq_max**), число потоков, параметры YaRN/RoPE, размер контекста (**n_ctx**), размер батча (**n_batch**, **n_ubatch**), тип внимания (causal).\
 **Для чего:**\
 \
 \- от этих параметров зависят размер KV-cache, максимальная длина промпта и то, как большими порциями обрабатывается батч.

Далее инициализируются бэкенды (CPU и видеокарты), создаётся объект памяти **memory** (KV-cache и служебные буферы), планировщик **sched** (распределяет узлы графа по устройствам), резервируются графы для Prefill и Decode — чтобы при первом decode не строить граф с нуля. После этого можно вызывать **llama_decode** с батчами токенов.

**Параметры** n_batch **и** n_ubatch**:**\
 \
 \- **n_batch** — максимальное число токенов в одном батче (при Prefill в батче может быть до n_batch токенов промпта);\
 \- **n_ubatch** — максимальный размер подбатча, который обрабатывается за один вызов **process_ubatch**;\
 \- если промпт длиннее n_ubatch, он разбивается на подбатчи по n_ubatch токенов, каждый прогоняется через модель по очереди;\
 \- логиты нужны только для последней позиции в батче, поэтому копируются только они;\
 \- уменьшение n_ubatch снижает пиковое потребление памяти за счёт большего числа проходов.\
 Контекст создаётся один раз на сеанс (или при смене параметров); один контекст может использоваться для множества запросов подряд без пересоздания.

Реализация инициализации памяти и планировщика — в конструкторе **llama_context** (файл **src/llama-context.cpp**): создаётся **ggml_backend_sched** с учётом списка бэкендов модели, затем вызывается **graph_reserve** — для Prefill и Decode строятся временные графы, планировщик выделяет под них буферы, так что при первом реальном decode граф только подставляется в уже зарезервированную память. Объект **memory** (реализация в **src/llama-memory.cpp**) выделяет буферы под KV-cache для каждого слоя и каждой позиции в контексте.

Функция **graph_reserve** (в **src/llama-context.cpp**) создаёт два зарезервированных графа: один для Prefill (много токенов за раз), другой для Decode (один токен). Для каждого вызывается **model.build_graph** с соответствующими параметрами (тип графа, размер батча), затем **ggml_backend_sched_alloc_graph** — планировщик разбивает граф по бэкендам и резервирует буферы. При первом вызове **process_ubatch** граф либо переиспользуется (если размеры совпадают), либо строится заново; в любом случае выделение под большие графы уже сделано при создании контекста, что уменьшает задержки при первом decode.

## Шаг 8: Приходит текст промпта от пользователя {#kogda-prihodit-soobshhenie-polzovatelya}

**Шаг 8** — к движку приходит текст промпта от пользователя. После создания контекста пользователь отправляет сообщение (промпт). Модель работает только с числами — токенами. Поэтому текст сначала превращают в токены (шаг 9), упаковывают в батч (шаг 10), прогоняют через модель вызовом **llama_decode** (шаги 11–14): при первом запросе в батче все токены промпта (Prefill заполняет KV-cache), при генерации — один новый токен (Decode). Из логитов сэмплер выбирает один следующий токен (шаг 15), он переводится в текст и выводится (шаг 16); токен снова добавляется в батч, и цикл повторяется до токена «конец вывода» (EOS) или лимита. Ниже каждый из этих шагов разобран по коду: что вызывается, что происходит и что может быть неочевидно.

**Prefill и Decode — в чём разница и для чего:**\
 \
 \- При первом вызове **llama_decode** с полным промптом (Prefill) модель обрабатывает все токены промпта за один или несколько подбатчей; для них считаются K и V и записываются в KV-cache; логиты запрашиваются только для последней позиции — по ним выбирается первый токен ответа.\
 **Для чего:**\
 \
один раз «прогнать» весь промпт и заполнить кэш ключей и значений, чтобы дальше генерировать по одному токену, не пересчитывая историю.\
 \- При следующих вызовах в батче один новый токен (Decode); каждый раз вычисляются только эмбеддинг этого токена, один слой внимания (Q, K, V для одной позиции, чтение K и V из кэша) и остальные слои; логиты снова только для последней позиции.\
 **Для чего:**\
 \
эффективная генерация по одному токену без пересчёта всей истории — старые K и V уже в кэше.

```cpp
// Упрощённый цикл генерации (шаги 8–16): как приложение вызывает API (псевдокод по логике examples/)
// Шаг 9: текст промпта → массив токенов
n_tokens = llama_tokenize(model, prompt, tokens, n_max, add_bos, true);
llama_batch_clear(batch);
for (int i = 0; i < n_tokens; i++) llama_batch_add(batch, tokens[i], i, {0}, false);
llama_batch_set_logits(batch, n_tokens - 1, true);  // логиты запрашиваем только для последней позиции (шаг 14)
while (true) {
    if (llama_decode(ctx, batch) != 0) break;  // шаги 11–14: encode, подбатчи, process_ubatch, логиты в буфере
    float * logits = llama_get_logits_ith(ctx, batch.n_tokens - 1);  // шаг 14: читаем логиты
    llama_token next = llama_sampler_sample(sampler, ctx, batch.n_tokens - 1);  // шаг 15: сэмплинг
    if (next == llama_token_eos(model)) break;  // EOS — конец вывода
    char buf[256];
    int n = llama_token_to_piece(model, next, buf, sizeof(buf));  // шаг 16: токен → текст
    printf("%.*s", n, buf);
    llama_batch_clear(batch);
    llama_batch_add(batch, next, n_cur, {0}, true);  // один новый токен — следующий цикл будет Decode
    n_cur++;
}
```

## Шаг 9: Токенизация — от текста к последовательности токенов {#tokenizaciya}

**Шаг 9** — текст промпта превращается в последовательность токенов (целых чисел). Текст нужно превратить в последовательность целых чисел — **токенов**.\
 **Для чего это нужно:**\
 \
 \- модель (слои трансформера) принимает на вход не строки, а векторы чисел фиксированной длины;\
 \- каждому токену соответствует одна строка в эмбеддинг-таблице модели.\
 Токенизация — это первый шаг:\
 \
 \- по словарю текст режется на кусочки (токены), каждому кусочку ставится в соответствие число (ID);\
 \- дальше по этим ID берутся эмбеддинги и подаются в модель.

**Что такое токен:**\
 \
 \- токен — это ID (целое число), который соответствует кусочку текста: целому слову, части слова или служебному символу;\
 \- словарь модели задаёт соответствие «текст ↔ токены»: по тексту можно получить массив токенов (токенизация), по токену — текст (token_to_piece).\
 В API (заголовок **include/llama.h**) объявлена функция **llama_tokenize**; реализация делегирует вызов словарю модели.

Реализация в **src/llama-vocab.cpp**:

```cpp
// Шаг 9: текст промпта превращается в массив токенов (ID); словарь модели задаёт соответствие «текст ↔ токены»
int32_t llama_tokenize(
    const struct llama_vocab * vocab,
                  const char * text,
                     int32_t   text_len,
                 llama_token * tokens,
                     int32_t   n_tokens_max,
                        bool   add_special,
                        bool   parse_special) {
    return vocab->tokenize(text, text_len, tokens, n_tokens_max, add_special, parse_special);
}
```

**Параметры:**\
 \
 \- **vocab** — словарь модели;\
 \- **text** и **text_len** — строка промпта;\
 \- **tokens** — массив для записи токенов;\
 \- **n_tokens_max** — его размер;\
 \- **add_special** — добавлять ли служебный токен начала (BOS);\
 \- **parse_special** — обрабатывать ли специальные теги.\
 Реальная логика (BPE, SentencePiece и т.д.) в методе **vocab->tokenize** в **src/llama-vocab.cpp**. Результат — число записанных токенов. Токенизация выполняется при каждом новом сообщении пользователя; словарь при этом не меняется и уже загружен при загрузке модели.

Реализация токенизации — метод **llama_vocab::impl::tokenize** в **src/llama-vocab.cpp**. Внутри по типу словаря (SPM, BPE, WPM и т.д.) создаётся сессия токенайзера и вызывается её **tokenize**; для BPE, например, используется **llm_tokenizer_bpe_session**, который разбивает текст по регулярным выражениям и собирает токены по слияниям (merges).

**Типы токенайзеров в словаре:**\
 \
 \- **LLAMA_VOCAB_TYPE_SPM** — SentencePiece-подобный (модели LLaMA и др.), разбиение по правилам и словарю SPM;\
 \- **LLAMA_VOCAB_TYPE_BPE** — Byte Pair Encoding (GPT-2 и др.), слияния пар байт/подстрок хранятся в GGUF, токенизация — жадное слияние;\
 \- **LLAMA_VOCAB_TYPE_WPM** — WordPiece и подобные;\
 \- **LLAMA_VOCAB_TYPE_NONE** — словарь не используется (например, для CLIP). Функция **tokenizer_st_partition** разбивает входной текст на фрагменты: обычный текст и специальные теги (если **parse_special** включён) — теги превращаются в один токен, остальное идёт в токенайзер по типу словаря. Фрагмент:

```cpp
std::vector<llama_token> llama_vocab::impl::tokenize(
    const std::string & raw_text,
    bool add_special,
    bool parse_special) const {
    GGML_ASSERT(tokenizer && "Tokenizer not initialized.");
    std::vector<llama_token> output;
    std::forward_list<fragment_buffer_variant> fragment_buffer;
    if (!raw_text.empty()) {
        fragment_buffer.emplace_front(raw_text, 0, raw_text.length());
        tokenizer_st_partition(fragment_buffer, parse_special);
    }
    switch (get_type()) {
    case LLAMA_VOCAB_TYPE_SPM: {
        llm_tokenizer_spm_session session(vocab);
        if (add_special && add_bos) output.push_back(special_bos_id);
        for (const auto & fragment : fragment_buffer) {
            if (fragment.type == FRAGMENT_BUFFER_VARIANT_TYPE_RAW_TEXT) {
                std::string text = fragment.raw_text.substr(fragment.offset, fragment.length);
                session.tokenize(text, output);
            } else {
                output.push_back(fragment.token);
            }
        }
        if (add_special && add_eos) output.push_back(special_eos_id);
    } break;
    case LLAMA_VOCAB_TYPE_BPE: {
        llm_tokenizer_bpe_session session(vocab, *static_cast<llm_tokenizer_bpe*>(tokenizer.get()));
        if (add_special) session.append_bos(output);
        for (const auto & fragment : fragment_buffer) {
            if (fragment.type == FRAGMENT_BUFFER_VARIANT_TYPE_RAW_TEXT)
                session.tokenize(fragment.raw_text.substr(fragment.offset, fragment.length), output);
            else
                session.append(fragment.token, output);
        }
        if (add_special) session.append_eos(output);
    } break;
    default: break;
    }
    return output;
}
```

## Шаг 10: Формирование батча для одного вызова {#batch-i-llama-batch-allocr}

**Шаг 10** — токены упаковываются в батч для одного вызова к модели. Один вызов к модели передаётся через структуру **llama_batch** (в **include/llama.h**).\
 **Для чего нужен батч:**\
 \
 \- функция **llama_decode** принимает ровно один батч — набор токенов (и служебных полей: позиции, идентификаторы последовательностей, флаги логитов);\
 \- так движок знает, какие токены обработать за один проход, на каких позициях они стоят и для каких позиций нужно вернуть логиты (обычно только для последней — чтобы выбрать следующий токен);\
 \- при первом запросе в батче обычно все токены промпта; при генерации по одному токену — один новый токен.

```cpp
// Шаг 10: батч — «пакет» токенов для одного вызова llama_decode; token[], pos[], seq_id[], logits[] заполняются приложением или balloc->init
typedef struct llama_batch {
    int32_t n_tokens;
    llama_token  *  token;
    float        *  embd;
    llama_pos    *  pos;
    int32_t      *  n_seq_id;
    llama_seq_id ** seq_id;
    int8_t       *  logits;
} llama_batch;
```

**Структура батча:**\
 \
 \- массивы имеют размер **n_tokens**;\
 \- либо передаются ID токенов (**token**), либо готовые **эмбеддинги** (**embd**) — векторы чисел, в которые уже превращены токены (обычно передают токены, а эмбеддинги считаются внутри);\
 \- **pos** — позиция каждого токена; **seq_id** и **n_seq_id** — к какой последовательности относится токен;\
 \- **logits\[i\] != 0** означает, что для позиции **i** нужно вернуть логиты (обычно только для последней — чтобы выбрать следующий токен). Перед **llama_decode** батч обрабатывается классом **llama_batch_allocr** (файл **src/llama-batch.cpp**): метод **init** проверяет батч и при отсутствии полей заполняет их автоматически (позиции из памяти, логиты только для последнего токена).

Поля **seq_id** и **n_seq_id** используются при батчинге нескольких последовательностей (например, несколько запросов в одном батче): каждый токен может относиться к одной или нескольким последовательностям; позиции (**pos**) считаются отдельно для каждой последовательности по **memory->seq_pos_max(seq_id)**. В типичном случае одна последовательность — все токены имеют один и тот же **seq_id** (например, 0), и позиции идут по порядку 0, 1, 2, ... . Батч очищается и заполняется заново перед каждым вызовом **llama_decode**; при генерации по одному токену в батче один токен с позицией n_cur.

Реализация метода **llama_batch_allocr::init** (файл **src/llama-batch.cpp**):

```cpp
// Шаг 10 (продолжение): balloc->init заполняет позиции и флаги логитов в батче (если приложение не заполнило); логиты обычно только для последней позиции
bool llama_batch_allocr::init(
    const llama_batch & batch_inp,
    const llama_vocab & vocab,
    const llama_memory_i * memory,
    uint32_t n_embd,
    uint32_t n_seq_max,
    bool output_all) {
    clear();
    batch = batch_inp;
    this->vocab = &vocab;
    GGML_ASSERT(batch.n_tokens > 0);
    if (batch.token) {
        for (int32_t i = 0; i < batch.n_tokens; ++i) {
            if (batch.token[i] < 0 || (uint32_t) batch.token[i] >= vocab.n_tokens()) {
                LLAMA_LOG_ERROR("%s: invalid token[%d] = %d\n", __func__, i, batch.token[i]);
                return false;
            }
        }
    }
    if (!batch.n_seq_id) {
        n_seq_id.resize(batch.n_tokens);
        for (int32_t i = 0; i < batch.n_tokens; i++) {
            n_seq_id[i] = seq_id_0.size();
        }
        batch.n_seq_id = n_seq_id.data();
    }
    if (!batch.seq_id) {
        seq_id.resize(batch.n_tokens + 1);
        seq_id[batch.n_tokens] = NULL;
        for (int32_t i = 0; i < batch.n_tokens; i++) {
            seq_id[i] = seq_id_0.data();
        }
        batch.seq_id = seq_id.data();
    }
    if (!batch.pos) {
        pos.resize(batch.n_tokens);
        llama_pos p0[LLAMA_MAX_SEQ];
        for (uint32_t s = 0; s < n_seq_max; ++s) {
            p0[s] = memory ? memory->seq_pos_max(s) + 1 : 0;
        }
        for (int32_t i = 0; i < batch.n_tokens; i++) {
            pos[i] = p0[batch.seq_id[i][0]];
            for (int32_t s = 0; s < batch.n_seq_id[i]; ++s) {
                p0[batch.seq_id[i][s]] = pos[i] + 1;
            }
        }
        batch.pos = pos.data();
    }
    if (!batch.logits) {
        if (output_all) {
            output.resize(batch.n_tokens, true);
        } else {
            output.resize(batch.n_tokens, false);
            output[output.size() - 1] = true;
        }
        batch.logits = output.data();
    }
    return true;
}
```

**В коде:**\
 \
 \- проверка токенов на допустимость;\
 \- при отсутствии **n_seq_id** и **seq_id** заполняются нулевыми последовательностями;\
 \- при отсутствии **pos** позиции берутся из памяти (**memory->seq_pos_max(s) + 1**) и проставляются по порядку;\
 \- при отсутствии **logits** логиты запрашиваются только для последнего токена (или для всех, если **output_all**).

## Шаг 11: Токены превращаются в эмбеддинги {#otkuda-berutsya-embeddings-encode}

**Шаг 11** — внутри decode токены (если в батче переданы именно они, а не готовые эмбеддинги) превращаются в эмбеддинги. В батче можно передать либо ID токенов (**token**), либо готовые эмбеддинги (**embd**). В типичном случае приложение передаёт токены — тогда внутри **llama_context::decode** перед обработкой батча вызывается **encode**.\
 **Для чего нужен encode:**\
 \
 \- модель (слои трансформера) работает не с номерами токенов, а с векторами чисел фиксированной длины — эмбеддингами;\
 \- encode превращает каждый токен в такой вектор, копируя соответствующую строку из эмбеддинг-таблицы модели во входной тензор графа;\
 \- без encode граф не получил бы корректный вход.

**Что такое эмбеддинг-таблица:**\
 \
 \- это матрица весов модели размером «размер словаря × размер эмбеддинга»; одна строка — вектор чисел для одного токена;\
 \- она загружается в **load_tensors** (тензор с именем вроде **tok_embeddings** или **embed_tokens**);\
 \- при encode для каждого токена из батча берётся строка с индексом **token\[i\]** и копируется во входной тензор графа на позицию **i**;\
 \- после этого граф считает уже по эмбеддингам (слои трансформера, внимание, feed-forward, логиты).

**Где это в коде:** в **llama_context::decode** (файл **src/llama-context.cpp**) проверяется, передан ли батч с токенами или с эмбеддингами. Если с токенами — вызывается внутренняя функция (или метод), которая для каждого элемента батча берёт **batch.token\[i\]**, находит в модели тензор эмбеддингов и копирует строку с индексом **token\[i\]** во входной тензор графа на позицию **i**. Один вызов encode заполняет входной слой графа эмбеддингами для всех токенов батча. Дальше **process_ubatch** строит граф и выполняет вычисления уже по этим эмбеддингам. При decode по одному токену копируется одна строка матрицы эмбеддингов во входной буфер графа.

**Цитата логики encode (получение эмбеддингов по ID токенов):**

```cpp
// Шаг 11 (encode): для каждого токена в батче берём строку эмбеддинг-таблицы модели (индекс = token[i]) и копируем во входной тензор графа — модель считает по векторам, а не по ID
// В decode перед process_ubatch (если в батче переданы токены, а не эмбеддинги):
for (int32_t i = 0; i < batch.n_tokens; i++) {
    llama_token tid = batch.token[i];
    // tok_embeddings — тензор модели размером [n_vocab, n_embd]; одна строка — вектор эмбеддинга для одного токена
    float * row = (float *) ((char *) model.tok_embeddings->data + tid * ggml_row_size(model.tok_embeddings->type, n_embd));
    memcpy(embd_buffer + i * n_embd, row, n_embd * sizeof(float));
}
// Для чего: модель (слои трансформера) принимает на вход векторы фиксированной длины, а не ID токенов
```

**Что происходит при encode и для чего:**\
 \
 \- для каждого индекса **i** в батче берётся ID токена **batch.token\[i\]**.\
 **Для чего:**\
 \
по ID нужно получить вектор эмбеддинга из таблицы весов модели.\
 \- из тензора **tok_embeddings** (или **embed_tokens**) читается строка с индексом **tid** — это вектор длины **n_embd**.\
 **Для чего:**\
 \
эта строка и есть эмбеддинг токена — вход для первого слоя трансформера.\
 \- строка копируется во входной буфер графа на позицию **i**.\
 **Для чего:**\
 \
граф при выполнении будет читать эмбеддинги из этого буфера; без encode буфер был бы пуст или содержал мусор.

## Шаги 11–12: Прогон батча через модель (вход в decode, подбатчи) {#llama-decode-i-process-ubatch}

**Шаги 11 и 12** — приложение вызывает **llama_decode**, передавая батч; внутри выполняются при необходимости превращение токенов в эмбеддинги (шаг 11), затем разбиение батча на подбатчи (шаг 12) и прогон каждого подбатча через модель (шаг 13). Ниже — цитаты точки входа, цикла по подбатчам и разбиения батча.\
 **Для чего нужна функция** llama_decode**:**\
 \
 \- она прогоняет один батч токенов через модель и заполняет внутренний буфер контекста логитами — «сырыми» оценками по каждому возможному следующему токену;\
 \- по этим логитам приложение (через сэмплер) выбирает один следующий токен и при необходимости снова вызывает decode с одним новым токеном в батче;\
 \- так повторяется до конца ответа (EOS) или лимита.\
 Публичный API — **llama_decode** (в **src/llama-context.cpp**):

```cpp
// Шаг 11: точка входа decode — приложение передаёт батч; внутри: encode (если нужен), подбатчи, process_ubatch, логиты в буфере
int32_t llama_decode(llama_context * ctx, llama_batch batch) {
    const int ret = ctx->decode(batch);  // 0 — успех, 1 — нужно больше данных, отрицательное — ошибка
    if (ret != 0 && ret != 1) {
        LLAMA_LOG_ERROR("%s: failed to decode, ret = %d\n", __func__, ret);
    }
    return ret;
}
```

В методе **llama_context::decode** происходит следующее. Реализация — в файле **src/llama-context.cpp**: метод **llama_context::decode(const llama_batch & batch)** проверяет батч, при необходимости вызывает **encode** (если переданы токены, а не эмбеддинги), затем вызывает **balloc->init(batch, ...)**, резервирует планировщик и обновляет память (KV-cache), после чего в цикле получает подбатчи через **memory->init_batch** и для каждого вызывает **process_ubatch**; логиты копируются во внутренний буфер контекста.

**Цитата цикла по подбатчам внутри** decode **(схема,** src/llama-context.cpp**):**

```cpp
// В decode после balloc->init и обновления памяти: цикл по подбатчам (шаг 12 → шаг 13)
while (memory->init_batch(batch, ubatch)) {
    if (ubatch.n_tokens == 0) break;
    auto * res = process_ubatch(ubatch, gtype, mctx, status);  // шаг 13: граф + выполнение
    if (!res) { ...; break; }
    // Копирование логитов для позиций с logits[i]==true в буфер контекста (шаг 14)
    copy_logits(res, ...);
}
// После цикла приложение читает логиты через llama_get_logits_ith
```

**Сначала проверяется батч:**\
 \
 \- переданы ли токены или готовые эмбеддинги;\
 \- при необходимости вызывается **encode**.\
 Затем батч инициализируется через **balloc->init** — это нужно, чтобы заполнить позиции токенов и решить, для каких позиций считать логиты (обычно только для последней). Резервируется планировщик и обновляется память (KV-cache).

Дальше в цикле **memory->init_batch** выдаёт подбатчи (**ubatch**) размером не больше **n_ubatch** — так большой батч разбивается на части, которые по очереди прогоняются через модель. Для каждого подбатча вызывается **process_ubatch(...)**: внутри строится вычислительный граф (эмбеддинги, слои трансформера, выход в логиты), выполняется вычисление на CPU/GPU, возвращаются тензоры с логитами — вероятностями по словарю.

**Цитата логики разбиения батча на подбатчи (**init_batch**,** src/llama-memory.cpp**):**

```cpp
// Шаг 12: возвращает очередной подбатч (ubatch) размером не больше n_ubatch; в decode вызывается в цикле
bool llama_memory::init_batch(const llama_batch & batch, llama_ubatch & ubatch) {
    if (batch.n_tokens == 0) return false;
    const uint32_t n_ubatch = cparams.n_ubatch;
    // Из полного батча берём очередные n_ubatch токенов (или остаток) — так длинный промпт не переполняет память
    ubatch.n_tokens = std::min((uint32_t) batch.n_tokens, n_ubatch);
    // Копируем в ubatch: token[], pos[], seq_id[], logits[] для этой порции
    // ... (реализация копирования и сдвига индексов для следующего вызова)
    return true;  // при следующем вызове вернётся следующий подбатч; когда токены кончились — false
}
```

**Что делает** init_batch **и для чего:**\
 \
 \- из полного батча берётся очередная порция токенов размером не больше **n_ubatch**.\
 **Для чего:**\
 \
один вызов **process_ubatch** обрабатывает ограниченное число токенов — так не переполняется память под промежуточные тензоры графа.\
 \- в **ubatch** копируются токены, позиции и флаги логитов для этой порции.\
 **Для чего:**\
 \
**process_ubatch** получает готовый подбатч и строит граф только для него.\
 \- при следующем вызове **init_batch** возвращается следующий подбатч, пока все токены батча не будут обработаны.\
 **Для чего:**\
 \
длинный промпт обрабатывается за несколько проходов через модель без единого огромного графа.

Реализация цикла по подбатчам в **llama_context::decode** (схема): пока **memory->init_batch(batch, ubatch)** возвращает подбатч с **ubatch.n_tokens > 0**, вызывается **process_ubatch(ubatch, gtype, mctx, status)**. Тип графа **gtype** — Prefill, если в подбатче больше одного токена (или первый проход по промпту), иначе Decode. После каждого **process_ubatch** логиты для позиций с **logits\[i\] == true** копируются во внутренний буфер контекста, откуда их читает **llama_get_logits_ith**. Цикл завершается, когда все токены батча обработаны. При первом decode с полным промптом тип графа — Prefill; при последующих decode с одним токеном — Decode; планировщик и граф переключаются между зарезервированными графами по типу.

**Порядок вызовов при одном decode (сводка):**\
 \
 \- **llama_decode(ctx, batch)** → **ctx->decode(batch)**;\
 \- в decode: проверка батча, при необходимости **encode**, **balloc->init(batch, ...)**, резервирование планировщика, обновление памяти (KV-cache);\
 \- цикл: **memory->init_batch** возвращает очередной **ubatch**, вызывается **process_ubatch(ubatch, ...)** — внутри **model.build_graph**, **res->set_inputs**, **graph_compute**; логиты копируются в буфер контекста;\
 \- после цикла приложение читает логиты через **llama_get_logits_ith**, сэмплер возвращает следующий токен.

Логиты копируются в выходной буфер контекста. Оттуда их читает **llama_get_logits_ith** — по этим числам сэмплер выбирает следующий токен (например, самый вероятный или по temperature/top_p).

Возвращаемое значение **llama_decode**: 0 — успех; 1 — нужно больше входных данных (в некоторых режимах батчинга); отрицательное значение — ошибка (например, GGML_STATUS_ALLOC_FAILED при нехватке памяти, GGML_STATUS_FAILED при ошибке вычислений). Приложение должно проверять возвращаемое значение и при ошибке прекращать генерацию или выводить сообщение об ошибке.

## Шаг 13: Подготовка к прогону подбатча (память, параметры графа) {#process-ubatch-tochka-vhoda-i-podgotovka}

**Шаг 13** разбит в статье на три части: подготовка к прогону подбатча, построение графа и аллокация буферов, запись входов и выполнение графа. Вся работа по одному подбатчу выполняется в методе **llama_context::process_ubatch** (файл **src/llama-context.cpp**). Его вызывают из **llama_context::decode** в цикле: для каждого подбатча, который возвращает **memory->init_batch**, вызывается один раз **process_ubatch**. Внутри по шагам происходит: подготовка памяти, решение — переиспользовать ли уже построенный граф или строить заново, при необходимости построение графа и аллокация буферов, запись входных данных в граф, выполнение графа на CPU/GPU. Ниже — первая часть шага 13: точка входа и подготовка.

**Для чего нужен вычислительный граф:**\
 \
 \- модель (трансформер) — это цепочка операций: эмбеддинги, слои внимания (Q, K, V, softmax, взвешенная сумма), нормализации, feed-forward и т.д.;\
 \- вместо того чтобы вызывать каждую операцию вручную, движок строит граф — список узлов (операций) и связей между ними;\
 \- планировщик потом обходит граф в топологическом порядке и выполняет операции на CPU или GPU;\
 \- так можно автоматически распределять узлы по устройствам и переиспользовать граф при одинаковых размерах батча.

**Сигнатура и входные данные** process_ubatch **(цитата из** src/llama-context.cpp**):**

```cpp
// Шаг 13: один подбатч прогоняется через модель; вызывается из decode в цикле по подбатчам
llm_graph_result * llama_context::process_ubatch(const llama_ubatch & ubatch, llm_graph_type gtype, llama_memory_context_i * mctx, ggml_status & ret) {
    // 1) Обновляем контекст памяти (KV-cache, служебные буферы) перед построением графа
    if (mctx && !mctx->apply()) {
        ret = GGML_STATUS_FAILED;
        return nullptr;
    }
    // 2) Берём зарезервированный результат графа (в нём хранятся граф и буферы)
    auto * res = gf_res_prev.get();
    auto * gf  = res->get_gf();
    // 3) Формируем параметры графа: размер батча, тип Prefill или Decode, контекст памяти
    const auto gparams = graph_params(res, ubatch, mctx, gtype);
    // ... далее решение о переиспользовании, при необходимости build_graph и set_inputs, graph_compute
}
```

**Что происходит в начале** process_ubatch **и для чего:**\
 \
 \- Вызывается **mctx->apply()**, если передан контекст памяти (**mctx**).\
 **Для чего:**\
 \
 \- контекст памяти обновляет состояние KV-cache и служебных буферов (например, сдвигает указатели на следующие свободные позиции); перед построением графа граф будет обращаться к этим буферам — они должны быть в актуальном состоянии.\
 \
 \- Берётся указатель на зарезервированный результат графа: **res = gf_res_prev.get()**, **gf = res->get_gf()**.\
 **Для чего:**\
 \
 \- при создании контекста для Prefill и Decode уже зарезервированы графы и буферы; **gf_res_prev** указывает на тот из них, который соответствует текущему типу вызова (Prefill или Decode).\
 \
 \- Формируются параметры графа: **gparams = graph_params(res, ubatch, mctx, gtype)**.\
 **Для чего:**\
 \
 \- в **gparams** попадают размер батча (**ubatch.n_tokens**), тип графа (Prefill — много токенов, или Decode — один токен), ссылки на контекст памяти; по ним дальше решается, переиспользовать ли граф, и при построении задаются размеры узлов и входов.

Если **mctx->apply()** возвращает **false**, функция сразу возвращает **nullptr** и в **ret** записывается **GGML_STATUS_FAILED** — вызывающий код (**decode**) обработает ошибку и прекратит цикл по подбатчам.

**Цитата по смыслу: что делает** mctx->apply() **и что входит в** gparams **(по коду** src/llama-context.cpp**,** src/llama-memory.cpp**):**

```cpp
// mctx->apply() — обновляет KV-cache и служебные буферы: сдвигает указатели на следующие свободные позиции,
// применяет отложенные обновления; возвращает true при успехе, false при ошибке
if (mctx && !mctx->apply()) { ret = GGML_STATUS_FAILED; return nullptr; }

// graph_params(res, ubatch, mctx, gtype) — собирает в структуру: n_tokens, тип графа (Prefill/Decode),
// ссылки на буферы KV-cache и входные данные; по gparams потом решают can_reuse и строят граф
const auto gparams = graph_params(res, ubatch, mctx, gtype);
```

## Шаг 13: Построение вычислительного графа и аллокация буферов {#postroenie-grafa-build-graph}

**Шаг 13 (продолжение)** — построение графа и выделение под него буферов. После подготовки памяти и формирования **gparams** движок решает: можно ли переиспользовать уже построенный граф (те же размеры батча и тип Prefill/Decode), или нужно строить граф заново и выделять под него буферы. Если граф переиспользуется — сразу переходят к записи входов и выполнению. Если нет — вызываются **res->reset()**, **ggml_backend_sched_reset(sched.get())**, **model.build_graph(gparams)** и **ggml_backend_sched_alloc_graph(sched.get(), gf)**. Ниже по шагам, что за чем происходит и для чего; несколько цитат из кода.

**Цитата: решение о переиспользовании и построение графа (фрагмент** process_ubatch**,** src/llama-context.cpp**):**

```cpp
    // Переиспользование: при том же размере и типе граф не перестраиваем — быстрее при повторных decode с одним токеном
    if (!graph_reuse_disable && res->can_reuse(gparams)) {
        n_reused++;  // счётчик переиспользований (для профилирования)
    } else {
        res->reset();   // сбрасываем результат графа (старые узлы и буферы)
        ggml_backend_sched_reset(sched.get());  // сбрасываем планировщик
        gf = model.build_graph(gparams);  // строим граф: эмбеддинги → слои трансформера → логиты
        if (!gf) { ret = GGML_STATUS_FAILED; return nullptr; }
        if (!ggml_backend_sched_alloc_graph(sched.get(), gf)) {  // планировщик выделяет буферы под все тензоры графа на CPU/GPU
            ret = GGML_STATUS_ALLOC_FAILED;
            return nullptr;
        }
    }

```

**По шагам (что происходит и для чего):**\
 \
 \- Проверка **res->can_reuse(gparams)**: совпадают ли размер батча и тип графа с теми, для которых граф был построен в прошлый раз.\
 **Для чего:**\
 \
 \- при генерации по одному токену (Decode) каждый следующий вызов **process_ubatch** получает подбатч из одного токена и тот же тип Decode — граф можно не перестраивать, только подставить новые входы и выполнить; это сильно ускоряет генерацию.\
 \
 \- Если переиспользование отключено или параметры изменились: **res->reset()** — очищаются старые узлы графа и привязанные буферы.\
 **Для чего:**\
 \
 \- перед построением нового графа старый нужно сбросить, иначе узлы и тензоры накопятся.\
 \
 \- **ggml_backend_sched_reset(sched.get())** — планировщик сбрасывает своё состояние (распределение узлов по бэкендам, зарезервированные буферы для этого графа).\
 **Для чего:**\
 \
 \- планировщик будет заново определять, на каком устройстве выполнять каждый узел нового графа и выделять под него память.\
 \
 \- **gf = model.build_graph(gparams)** — строится граф GGML: узлы для эмбеддингов и позиций, затем для каждого слоя трансформера — внимание (Q, K, V, softmax, взвешенная сумма), нормализация, feed-forward, снова нормализация; в конце — выходной слой в логиты (размер словаря).\
 **Для чего:**\
 \
 \- граф описывает, какие операции выполнить и в каком порядке; без него планировщику нечего выполнять. Реализация **build_graph** зависит от архитектуры (LLaMA, Gemma и т.д.) и находится в **src/llama-model.cpp** или в специализированных файлах.\
 \
 \- **ggml_backend_sched_alloc_graph(sched.get(), gf)** — планировщик обходит граф, для каждого тензора определяет бэкенд (CPU или GPU по разбиению слоёв), выделяет буфер на этом бэкенде (или переиспользует зарезервированный при создании контекста).\
 **Для чего:**\
 \
 \- без выделения буферов данные графа негде хранить; при первом decode буферы резервируются здесь (или при **graph_reserve** при создании контекста), при последующих переиспользованиях графа повторное выделение не нужно.

**Структура графа по слоям (схема):**\
 \
 \- вход: эмбеддинги токенов подбатча + позиционные кодировки (RoPE) по позициям из **ubatch**;\
 \- для каждого слоя трансформера: нормализация входа → блок внимания (Q = вход × W_q, K = вход × W_k, V = вход × W_v; применение RoPE к Q и K; attention scores = Q × K^T; маска (causal); softmax; взвешенная сумма scores × V; линейный слой O) → остаточное соединение → нормализация → feed-forward (две линейные слоя с активацией между ними) → остаточное соединение;\
 \- после всех слоёв: финальная нормализация → выходной слой (матрица «размер скрытого слоя × размер словаря») → логиты (тензор размера \[n_tokens, n_vocab\]).

**Цитата вызова** build_graph **и** ggml_backend_sched_alloc_graph **(по смыслу кода в** src/llama-context.cpp **и GGML):**

```cpp
// model.build_graph(gparams) — возвращает граф GGML (ggml_context с узлами); gparams задаёт n_tokens, тип Prefill/Decode, контекст памяти
ggml_graph * gf = model.build_graph(gparams);

// ggml_backend_sched_alloc_graph — планировщик обходит граф, для каждого тензора выбирает бэкенд (CPU/GPU) и выделяет буфер
bool ok = ggml_backend_sched_alloc_graph(sched.get(), gf);
if (!ok) { ret = GGML_STATUS_ALLOC_FAILED; return nullptr; }
```

Резервирование графа для Prefill и Decode делается в **graph_reserve** при создании контекста: создаётся временный ubatch, вызывается **model.build_graph**, планировщик разбивает граф по устройствам и резервирует буферы — чтобы при первом decode не тратить время на выделение памяти. Функция **ggml_backend_sched_alloc_graph** (библиотека GGML) при выделении определяет для каждого тензора графа бэкенд по разбиению слоёв модели и выделяет буфер на соответствующем устройстве.

## Шаг 13: Запись входов в граф и выполнение на CPU/GPU {#vypolnenie-grafa-graph-compute}

**Шаг 13 (завершение)** — запись входов в граф и выполнение. После того как граф построен (или переиспользован), нужно записать во входные тензоры графа данные текущего подбатча — токены или эмбеддинги и позиции — и запустить выполнение графа. Это делают вызовы **res->set_inputs(&ubatch)** и **graph_compute(res->get_gf(), ubatch.n_tokens > 1)**. Ниже — цитаты и пошаговое объяснение.

**Цитата: запись входов и выполнение (конец** process_ubatch**,** src/llama-context.cpp**):**

```cpp
    res->set_inputs(&ubatch);  // записываем в входные тензоры графа: эмбеддинги (или токены) и позиции для этого подбатча
    const auto status = graph_compute(res->get_gf(), ubatch.n_tokens > 1);  // обход узлов графа, выполнение на CPU/GPU
    if (status != GGML_STATUS_SUCCESS) {
        ret = status;
        return nullptr;
    }
    ret = GGML_STATUS_SUCCESS;
    return res;  // в res — тензоры с логитами; decode потом копирует их в буфер контекста для llama_get_logits_ith
}
```

**Что происходит при** set_inputs **и для чего:**\
 \
 \- **res->set_inputs(&ubatch)** копирует данные подбатча во входные тензоры графа: эмбеддинги токенов (или сами ID токенов, если граф принимает их и сам делает lookup по таблице эмбеддингов) и позиции для каждой позиции в подбатче; при необходимости подставляются указатели на буферы KV-cache (куда писать новые K и V и откуда читать уже сохранённые).\
 **Для чего:**\
 \
 \- граф вычисляет по конкретным данным; без записи входов он работал бы с пустыми или устаревшими тензорами; при Decode в кэш подставляются буферы, куда дописываются новые ключи и значения для текущей позиции.

**Цитата вызова** graph_compute **(выполнение графа на планировщике, обычно в том же файле или в** src/llama-context.cpp**):**

```cpp
// Планировщик обходит узлы графа в топологическом порядке и выполняет каждую операцию на назначенном бэкенде (CPU/GPU)
ggml_status graph_compute(llm_graph_result * res, bool capture) {
    ggml_backend_sched * sched = ctx->sched.get();
    ggml_backend_sched_begin(sched);   // подготовка планировщика: резервирование буферов, порядок узлов
    ggml_backend_sched_run(sched, res->get_gf());  // обход графа, выполнение каждого узла на своём бэкенде
    ggml_backend_sched_end(sched);     // завершение и синхронизация бэкендов
    return ggml_backend_sched_get_status(sched);
}
```

**Что делает** graph_compute **по шагам и для чего:**\
 \
 \- **ggml_backend_sched_begin(sched)** подготавливает планировщик к выполнению графа: фиксирует порядок обхода узлов (топологическая сортировка), при необходимости резервирует промежуточные буферы.\
 **Для чего:**\
 \
 \- узлы графа нужно выполнять в таком порядке, чтобы к моменту выполнения узла все его входы уже вычислены; планировщик этот порядок обеспечивает.\
 \
 \- **ggml_backend_sched_run(sched, res->get_gf())** обходит граф: для каждого узла определяет бэкенд (CPU или GPU по разбиению модели), при необходимости копирует входные данные на это устройство, запускает операцию (умножение матриц, softmax, сложение и т.д.).\
 **Для чего:**\
 \
 \- так выполняются все слои трансформера; в итоге выходные тензоры графа — логиты — заполняются числами (по одному на каждый токен словаря для каждой позиции в подбатче, для которой запрошены логиты).\
 \
 \- **ggml_backend_sched_end(sched)** завершает выполнение и синхронизирует бэкенды (например, ждёт завершения операций на GPU).\
 **Для чего:**\
 \
 \- после этого выходные тензоры графа (логиты) готовы к чтению; вызывающий код (**decode**) копирует их в буфер контекста, откуда приложение читает логиты через **llama_get_logits_ith**.

Параметр **capture** в **graph_compute(res->get_gf(), ubatch.n_tokens > 1)** в некоторых сборках используется для отладки или профилирования (например, захват графа для визуализации). Возвращаемое значение **graph_compute** — статус GGML (успех или код ошибки); при успехе **process_ubatch** возвращает **res**, и в **decode** логиты из **res** копируются во внутренний буфер контекста для позиций с **logits\[i\] == true**.

При переиспользовании графа (когда размер батча и тип графа совпадают с предыдущим вызовом) граф не перестраивается и буферы не перевыделяются — вызываются только **res->set_inputs(&ubatch)** и **graph_compute**. Это ускоряет повторные вызовы decode при генерации по одному токену: граф Decode строится один раз при первом decode с одним токеном и далее переиспользуется. Счётчик **n_reused** в контексте увеличивается при каждом переиспользовании графа — по нему можно оценить долю переиспользований при профилировании.

## Шаги 14–15: Логиты в буфере и выбор следующего токена (сэмплинг) {#logity-i-sempling}

**Шаги 14 и 15** — после прогона подбатча логиты для последней позиции оказываются в буфере контекста (шаг 14); по ним сэмплер выбирает один следующий токен (шаг 15). После **llama_decode** во внутреннем буфере контекста лежат **логиты** — по одному числу на каждый токен словаря.\
 **Для чего нужны логиты:**\
 \
 \- модель на выходе выдаёт не один токен, а «оценки» по всем возможным следующим токенам (по одному числу — логиту — на каждый токен словаря);\
 \- по этим числам сэмплер решает, какой один токен выбрать: например, с максимальным логитом (жадный выбор) или случайно с учётом temperature/top_p;\
 \- без логитов приложение не смогло бы выбрать следующий токен.\
 Логиты можно понимать как «сырые» оценки того, насколько подходит каждый следующий токен; из них сэмплер выбирает один токен. Доступ к логитам — через **llama_get_logits_ith** (файл **src/llama-context.cpp**):\
 \- возвращается указатель на массив из **n_vocab** float — по одному числу на каждый токен словаря.

Логиты запрашиваются только для тех позиций в батче, для которых в батче был установлен флаг **logits\[i\] == true** (обычно только для последней позиции). Контекст копирует логиты во внутренний буфер после каждого вызова **process_ubatch** для позиций с этим флагом; при нескольких подбатчах в одном decode в буфере остаются логиты для последней такой позиции (обычно последний токен в батче). Размер буфера логитов в контексте — n_vocab (по одному float на каждый токен словаря).

Реализация **llama_get_logits_ith** в **src/llama-context.cpp** возвращает указатель на логиты для заданной позиции в батче (обычно запрашивают логиты для последней позиции — по ним выбирается следующий токен):

```cpp
// Шаг 14: возвращает указатель на логиты для позиции i (обычно i = последняя позиция в батче)
// Массив из n_vocab float — по одному числу на каждый токен словаря; сэмплер по ним выбирает следующий токен
float * llama_get_logits_ith(struct llama_context * ctx, int32_t i) {
    return ctx->get_logits_ith(i);
}
```

По этим числам выбирается следующий токен с помощью **сэмплера** — компонента, который по логитам решает, какой токен выдать (например, самый вероятный или случайный с учётом temperature/top_p). В API (**include/llama.h**) функция **llama_sampler_sample(...)** берёт логиты, применяет к ним цепочку сэмплеров и возвращает один токен. Этот токен добавляется в историю и при следующем вызове **llama_decode** передаётся в батче как единственный новый токен; цикл повторяется до конца ответа (токен EOS — «конец вывода») или лимита. Перевести токен обратно в текст — через **llama_token_to_piece** / **llama_detokenize** (реализация в **src/llama-vocab.cpp**).

**Цепочка сэмплеров:**\
 \
 \- сэмплинг в llama.cpp устроен как последовательность шагов;\
 \- сначала к логитам может применяться сдвиг по повторениям (repeat penalty) — уменьшение вероятности уже появившихся токенов;\
 \- затем применяется temperature — деление логитов на число (temperature > 1 делает распределение мягче, \< 1 — острее);\
 \- дальше может идти top-p (nucleus): оставляются только токены с накопленной вероятностью до порога p;\
 \- после этого из оставшихся выбирается один токен — например, с максимальным логитом (жадный выбор) или случайно с вероятностями по softmax от логитов;\
 \- реализация цепочки — в коде сэмплера (например, **common/sampling.cpp** или аналог в репозитории): цикл по зарегистрированным сэмплерам, каждый модифицирует логиты или выбирает токен.

Реализация перевода токена в текст — методы **llama_vocab::impl::token_to_piece** и **llama_vocab::impl::detokenize** в **src/llama-vocab.cpp**. **token_to_piece** по ID токена возвращает строку (кусочек текста); для байтовых токенов выполняется преобразование в символ, для обычных — берётся текст из **id_to_token**. **detokenize** проходит по массиву токенов и склеивает результаты **token_to_piece** в одну строку. API **llama_token_to_piece** (в **include/llama.h**) принимает модель, токен, буфер и размер; внутри вызывается **vocab.token_to_piece**. Для вывода по одному токену обычно используют **llama_token_to_piece**; для сборки полной строки из массива токенов — **llama_detokenize**. Фрагмент:

```cpp
// Шаг 16: по ID токена возвращаем кусочек текста (для вывода пользователю); id_to_token загружен в load_vocab
int32_t llama_vocab::impl::token_to_piece(llama_token token, char * buf, int32_t length, int32_t lstrip, bool special) const {
    if (0 <= token && token < (int32_t) id_to_token.size()) {
        const std::string & token_text = id_to_token[token].text;
        switch (get_type()) {
        case LLAMA_VOCAB_TYPE_SPM:
        case LLAMA_VOCAB_TYPE_UGM:
            if (attr & LLAMA_TOKEN_ATTR_NORMAL) {
                std::string result = token_text;
                llama_unescape_whitespace(result);
                memcpy(buf, result.data(), result.size());
                return (int32_t) result.size();
            }
            if (attr & LLAMA_TOKEN_ATTR_BYTE) {
                char byte = (char) token_to_byte(token);
                buf[0] = byte;
                return 1;
            }
            break;
        case LLAMA_VOCAB_TYPE_BPE:
            if (attr & LLAMA_TOKEN_ATTR_NORMAL) {
                std::string result = llama_decode_text(token_text);
                memcpy(buf, result.data(), result.size());
                return (int32_t) result.size();
            }
            break;
        default: break;
        }
    }
    return 0;
}
```

В коде видно: для SPM и UGM обычные токены превращаются в текст через **llama_unescape_whitespace**, байтовые — через **token_to_byte**; для BPE используется **llama_decode_text** (декодирование escape-последовательностей). Функция **llama_detokenize** в API вызывает **vocab.detokenize** и собирает строку по массиву токенов.

Сэмплер в API (например, **llama_sampler_sample**) принимает контекст, позицию в батче и опционально цепочку параметров сэмплинга (temperature, top_p, repeat_penalty и т.д.). Внутри читаются логиты для этой позиции через **ctx->get_logits_ith(pos)**, к ним применяется цепочка сэмплеров (repeat penalty, temperature, top_p и т.д.), затем выбирается один токен (жадный или случайный по softmax). Возвращаемый токен добавляется в историю и при следующем вызове **llama_decode** передаётся в батче как единственный новый токен.

## KV-cache и этапы Prefill / Decode (справочно) {#kv-cache-i-etapy-prefill-decode}

В механизме внимания каждый токен получает вектор запроса (Q), ключа (K) и значения (V) — это числовые векторы, которые модель считает из своих весов.\
 **Для чего нужны Q, K, V:**\
 \
 \- по ним вычисляется «внимание» — насколько каждая предыдущая позиция важна для текущей;\
 \- результат — взвешенная сумма значений V с весами по Q и K.\
 Логиты для следующего токена зависят от Q текущей позиции и от K, V **всех предыдущих** позиций — поэтому при генерации по одному токену старые K и V не меняются, их достаточно посчитать один раз и сохранить. **KV-cache** — это буфер в памяти, в котором для каждого слоя и каждой позиции хранятся уже посчитанные ключи и значения; так не нужно пересчитывать их заново на каждом шаге генерации.

**Prefill:**\
 \
 \- в первом проходе обрабатываются все токены промпта, для них считаются K и V и записываются в кэш.

**Decode:**\
 \
 \- в последующих шагах обрабатывается только один новый токен: по нему считаются Q, K, V; K и V дописываются в кэш; Q умножается на все ключи из кэша (и на себя), после маски и softmax — на все значения из кэша. В итоге получаем один вектор контекста для этой позиции и дальше feed-forward и т.д. Так мы не пересчитываем внимание по всей истории на каждом шаге, а только по новому токену — это и даёт ускорение генерации. Разделение на Prefill (много токенов за раз, нагрузка на вычислители) и Decode (один токен, нагрузка на память) характерно для llama.cpp и других движков.

Реализация KV-cache — в **src/llama-memory.cpp**: для каждого слоя модели выделяются буферы под ключи и значения (размер зависит от числа голов внимания, размера головы и длины контекста). При Prefill в эти буферы записываются K и V для всех позиций промпта; при Decode для нового токена вычисляются только новые K и V и дописываются в конец. Планировщик и граф модели при выполнении обращаются к этим буферам через указатели, переданные в параметрах графа.

**Расположение буферов KV-cache:** для каждого слоя трансформера создаётся два тензора (или один объединённый):\
 \
 \- один для ключей;\
 \- один для значений.\
 Размерность: \[число голов KV, размер головы, длина контекста\] или \[длина контекста, число голов × размер головы\] в зависимости от реализации. При создании контекста вызывается выделение памяти под эти тензоры на CPU или GPU (по настройкам). При Prefill граф записывает K и V для позиций 0..n-1 (n — число токенов в батче). При Decode граф пишет K и V только для позиции n_cur (текущая позиция) в соответствующее место буфера; чтение K и V для всех позиций 0..n_cur идёт из того же буфера. Так не нужно пересчитывать ключи и значения для уже обработанных токенов.

Объём памяти KV-cache растёт линейно с длиной контекста и числом слоёв: для каждого слоя хранятся ключи и значения для всех позиций. При длине контекста n_ctx и числе слоёв n_layer объём пропорционален n_ctx × n_layer × (размер одной головы × число голов × 2) (×2 — ключи и значения). Поэтому при ограниченной памяти уменьшают n_ctx или используют более агрессивную квантизацию для KV-cache. В llama.cpp при создании контекста выделяются буферы под максимальную длину контекста (n_ctx); при генерации заполняются только позиции до текущей.

Функция **memory->seq_pos_max(s)** возвращает максимальную позицию, до которой заполнен KV-cache для последовательности **s**. При добавлении нового токена в батч позиция для него берётся как **seq_pos_max(s) + 1** — так батч всегда ссылается на следующую свободную позицию в кэше. После успешного **process_ubatch** память обновляется: записанные в кэш позиции считаются занятыми, и при следующем вызове **init_batch** новые токены получат следующие позиции.

## Резюме: от файла до первого токена {#rezume-ot-fajla-do-pervogo-tokena}

Краткая последовательность шагов от запуска приложения до появления первого токена ответа.\
 **Для чего это полезно:**\
 \
 \- при отладке или изучении кода можно сверяться с этим списком и проверять, на каком шаге вы находитесь;\
 \- так проще найти в исходниках место, соответствующее этапу загрузки или генерации.

**Подготовка (один раз при старте или смене модели):**

\- **llama_backend_init()** — инициализация таймера и таблиц f16.\
 **Для чего:**\
 \
один раз при старте приложения.\
 \- **llama_model_load_from_file(path, params)** → **llama_model_load_from_file_impl** → проверка бэкенда, колбэк прогресса, список устройств, **llama_model_load(...)**.\
 **Для чего:**\
 \
загрузить модель из файла и получить указатель на **llama_model**.\
 \- Внутри **llama_model_load**: **llama_model_loader** (открытие GGUF, индекс тензоров), **load_arch**, **load_hparams**, **load_vocab** (в т.ч. **vocab.load(ml, kv)**), **load_stats**, **load_tensors**.\
 **Для чего:**\
 \
по шагам заполнить модель архитектурой, гиперпараметрами, словарём и весами.\
 \- **llama_new_context(model, ctx_params)** — создание контекста: параметры n_ctx, n_batch, n_ubatch, инициализация памяти (KV-cache), планировщика, резерв графов Prefill/Decode.\
 **Для чего:**\
 \
контекст нужен для вызова **llama_decode**; без него нельзя генерировать текст.

**Генерация (на каждое сообщение пользователя и каждый новый токен в ответе):**

\- **llama_tokenize(model, prompt, ...)** — промпт в токены.\
 **Для чего:**\
 \
модель работает только с числами (токенами).\
 \- Формирование батча: **llama_batch_clear**, **llama_batch_add** для каждого токена промпта, **llama_batch_set_logits** для последней позиции.\
 **Для чего:**\
 \
один вызов decode принимает один батч; логиты нужны только для последней позиции, чтобы выбрать следующий токен.\
 \- **llama_decode(ctx, batch)** → **ctx->decode(batch)** → при необходимости encode (токены → эмбеддинги), **balloc->init**, обновление памяти, цикл: **memory->init_batch** → **process_ubatch** (build_graph, set_inputs, graph_compute), копирование логитов.\
 **Для чего:**\
 \
прогон батча через модель и получение логитов во внутреннем буфере контекста.\
 \- **llama_get_logits_ith(ctx, pos)** — указатель на логиты для последней позиции.\
 **Для чего:**\
 \
по ним сэмплер выбирает следующий токен.\
 \- Сэмплер выбирает следующий токен; **llama_token_to_piece** — токен в текст; вывод; добавление токена в батч; повтор decode до EOS или лимита.\
 **Для чего:**\
 \
цикл генерации по одному токену до конца ответа.

Таким образом, путь от файла модели до первого токена ответа проходит через загрузчик GGUF, загрузку архитектуры и гиперпараметров, словаря и тензоров, создание контекста с KV-cache и планировщиком, токенизацию промпта, батч, encode, построение графа и его выполнение, сэмплинг по логитам.

При первом decode с полным промптом (Prefill) время выполнения зависит от длины промпта и размера подбатча (n_ubatch): чем длиннее промпт, тем больше подбатчей и проходов через модель; логиты нужны только для последней позиции. При последующих decode (по одному токену) каждый вызов обрабатывает один токен; граф Decode переиспользуется, основное время уходит на вычисление одного слоя внимания (Q, K, V для одной позиции, чтение K и V из кэша, softmax, взвешенная сумма) и остальных слоёв. Оптимизация скорости генерации связана с оптимизацией этого пути: эффективное чтение KV-cache, переиспользование графа, распределение по GPU. При профилировании полезно смотреть время первого decode (Prefill) и время последующих decode (по одному токену): первый зависит от длины промпта и n_ubatch, последующие — от эффективности одного прохода через модель и копирования данных между бэкендами.

**Примечания по версиям и сборке:** структура кода и имена файлов в репозитории llama.cpp могут меняться от версии к версии; номера строк и фрагменты кода в статье соответствуют одной из последних версий и могут отличаться у вас. При сборке с GPU нужны соответствующие бэкенды (CUDA, Metal, Vulkan и т.д.) и переменные окружения; без них движок работает только на CPU. Примеры вызовов API и имена структур приведены по **include/llama.h**; при использовании C-обёрток или других языков (Python, Go и т.д.) сигнатуры могут отличаться, но общий сценарий загрузки и decode остаётся тем же.

**Рекомендации по изучению кода:** для понимания пути загрузки модели удобно начать с **llama_model_load_from_file** в **src/llama.cpp** и пройти по вызовам до **llama_model_load**, затем по load_arch, load_hparams, load_vocab, load_tensors. Для пути decode — с **llama_decode** в **src/llama-context.cpp**, затем **ctx->decode**, balloc->init, цикл init_batch и process_ubatch. В process_ubatch смотреть build_graph и graph_compute. Словарь и токенизация — **src/llama-vocab.cpp** (load, tokenize, token_to_piece). Загрузчик GGUF — **src/llama-model-loader.cpp** (конструктор, get_tensor, load_tensor_data). Память и KV-cache — **src/llama-memory.cpp** (seq_pos_max, init_batch). При отладке полезно ставить точки останова на входах в load_arch, load_hparams, load_vocab, load_tensors и на входах в decode, process_ubatch, build_graph.

## Детализация реализации по файлам {#detalizaciya-po-fajlam}

Ниже — краткая привязка описанных в статье шагов к конкретным файлам и функциям репозитория llama.cpp.\
 **Для чего это нужно:**\
 \
 \- при изучении или отладке можно быстро найти нужный код по имени файла и функции;\
 \- каждый пункт указывает, что искать в файле и зачем это нужно.

**src/llama.cpp:**

\- **llama_backend_init** — инициализация таймера и таблиц f16.\
 **Для чего:**\
 \
один раз при старте приложения.\
 \- **llama_model_load_from_file**, **llama_model_load_from_file_impl** — точка входа загрузки модели.\
 **Для чего:**\
 \
приложение вызывает их, чтобы загрузить модель из файла.\
 \- **llama_model_load** (статическая) — последовательный вызов load_arch, load_hparams, load_vocab, load_stats, load_tensors.\
 **Для чего:**\
 \
здесь создаётся загрузчик и по шагам заполняется модель. В том же файле — проверка бэкенда, колбэк прогресса, создание объекта модели и список устройств.

**src/llama-model-loader.cpp:**

\- Класс **llama_model_loader**: конструктор открывает GGUF через **gguf_init_from_file**, строит **weights_map** по списку тензоров из контекста GGUF.\
 **Для чего:**\
 \
по имени тензора потом можно прочитать данные из файла.\
 \- Методы **get_key**, **get_arch**, **get_tensor**, **load_tensor_data** — чтение метаданных и данных тензоров.\
 **Для чего:**\
 \
load_arch, load_hparams, load_vocab и load_tensors вызывают их.\
 \- **print_info** — вывод информации о файле. Поддержка нескольких файлов (splits) и mmap/direct_io.

**src/llama-model.cpp:**

\- Класс **llama_model**: **load_arch** (вызов **ml.get_arch()**), **load_hparams** (чтение ключей GGUF в hparams), **load_vocab** (вызов **vocab.load(ml, kv)**), **load_tensors** (формирование списков буферов CPU/GPU, разбиение слоёв, создание тензоров и вызов **ml.get_tensor**/**ml.load_tensor_data**).\
 **Для чего:**\
 \
здесь создаются тензоры модели по архитектуре и назначаются буферы на CPU/GPU.

**src/llama-vocab.cpp:**

\- Класс **llama_vocab**, **llama_vocab::impl::load** — чтение типа токенайзера, списков токенов, слияний BPE, специальных токенов из GGUF.\
 **Для чего:**\
 \
словарь нужен для токенизации и перевода токенов в текст.\
 \- **init_tokenizer** — инициализация токенайзера по типу (SPM, BPE и т.д.). **tokenize** — разбиение текста на токены; **token_to_piece**, **detokenize** — перевод токенов в текст. Функции **llama_tokenize**, **llama_token_to_piece**, **llama_detokenize** делегируют вызовы словарю модели.

**src/llama-context.cpp:**

\- Класс **llama_context**: конструктор создаёт balloc, задаёт cparams (n_ctx, n_batch, n_ubatch и т.д.), инициализирует память (KV-cache) и планировщик (**ggml_backend_sched**), вызывает **graph_reserve** для Prefill и Decode.\
 **Для чего:**\
 \
контекст — «рабочая среда» одного сеанса генерации.\
 \- Метод **decode** — проверка батча, encode при необходимости, **balloc->init**, обновление памяти, цикл по подбатчам (**memory->init_batch**, **process_ubatch**), копирование логитов.\
 **Для чего:**\
 \
один вызов decode прогоняет батч через модель и заполняет буфер логитов.\
 \- **process_ubatch** — применение mctx, build_graph или переиспользование графа, set_inputs, graph_compute. **get_logits_ith** — возврат указателя на логиты для позиции. Публичные функции **llama_decode**, **llama_get_logits_ith** вызывают методы контекста.

**src/llama-batch.cpp:**

\- Структура **llama_batch** (объявлена в include/llama.h), класс **llama_batch_allocr**: метод **init** проверяет токены, заполняет n_seq_id, seq_id, pos, logits при их отсутствии; позиции берутся из **memory->seq_pos_max**.\
 **Для чего:**\
 \
чтобы вызывающему коду не нужно было вручную выставлять позиции и флаги логитов. Используется внутри **llama_context::decode** перед циклом по подбатчам.

**src/llama-memory.cpp:**

\- Объект памяти (реализация интерфейса **llama_memory_i**) — выделение буферов KV-cache для каждого слоя и каждой позиции контекста.\
 **Для чего:**\
 \
в них хранятся ключи и значения механизма внимания.\
 \- **seq_pos_max(s)** — максимальная позиция для последовательности s.\
 **Для чего:**\
 \
при формировании батча позиции новых токенов берутся как seq_pos_max(s) + 1.\
 \- **init_batch** — разбиение батча на подбатчи (ubatch) размером не больше n_ubatch; обновление занятых позиций после process_ubatch. Планировщик и граф обращаются к буферам KV-cache через параметры графа.

**GGML/GGUF (внешние библиотеки или подмодули):**

\- **ggml_init**, **ggml_free** — контекст графа. **gguf_init_from_file**, **gguf_get_\*** — чтение GGUF.\
 **Для чего:**\
 \
загрузчик и модель используют их для чтения файла и построения графа.\
 \- **ggml_backend_sched**, **ggml_backend_sched_alloc_graph**, **ggml_backend_sched_reset** — планировщик и выделение буферов под граф.\
 **Для чего:**\
 \
планировщик распределяет узлы графа по CPU/GPU и выделяет под них память.\
 \- Выполнение графа (graph_compute) — обход узлов в топологическом порядке, копирование между бэкендами при необходимости, запуск операций на CPU/GPU. Модель строит граф через **model.build_graph** (реализация по архитектуре в src/llama-model.cpp или в специализированных файлах). При добавлении поддержки новой архитектуры в build_graph добавляются узлы по специфике модели; общая схема «эмбеддинги → слои → логиты» сохраняется.

При изучении кода удобно искать по имени функции или метода: **llama_backend_init**, **llama_model_load_from_file**, **llama_model_load**, **load_arch**, **load_hparams**, **load_vocab**, **vocab.load**, **load_tensors**, **llama_new_context**, **llama_decode**, **decode**, **process_ubatch**, **build_graph**, **graph_compute**, **llama_get_logits_ith**, **llama_tokenize**, **llama_token_to_piece**. По ним можно проследить весь путь от загрузки модели до вывода токена. В репозитории полезно смотреть примеры в **examples/** — там показан типичный цикл: загрузка модели, создание контекста, токенизация, батч, decode, сэмплинг, вывод.

## Связь компонентов и потоки данных {#svyaz-komponentov-i-potoki-dannyh}

Кратко — как данные проходят между компонентами от файла модели до вывода токена. **Для чего это полезно:** при отладке или изучении кода можно проследить, откуда берётся каждое значение и куда оно передаётся.

**Загрузка (один раз при старте или смене модели):**

\- Файл GGUF → **llama_model_loader** (метаданные и индекс тензоров **weights_map**).\
 **Для чего:**\
 \
загрузчик даёт доступ к полям GGUF и к данным тензоров по имени.\
 \- **load_arch** (тип модели: LLaMA, Gemma и т.д.).\
 **Для чего:**\
 \
от типа зависят имена ключей в GGUF.\
 \- **load_hparams** (размерности и параметры: n_ctx, n_layer, n_embd и т.д.).\
 **Для чего:**\
 \
от них зависит «форма» модели и размеры тензоров.\
 \- **load_vocab** → **vocab.load** (словарь токенов и токенайзер).\
 **Для чего:**\
 \
словарь нужен для токенизации и перевода токенов в текст.\
 \- **load_tensors** (веса из файла в буферы CPU/GPU).\
 **Для чего:**\
 \
модель готова к вычислениям. Модель (**llama_model**) хранит hparams, vocab и тензоры; загрузчик после загрузки больше не нужен.

**Создание контекста (один раз на сеанс):**

\- Модель + параметры контекста → **llama_context**: balloc (аллокатор батча), memory (KV-cache и init_batch), sched (планировщик), зарезервированные графы Prefill/Decode.\
 **Для чего:**\
 \
контекст — «рабочая среда» одного сеанса генерации; в нём хранятся KV-cache, планировщик и графы для decode. Контекст ссылается на модель; модель не хранит ссылку на контекст.

**Генерация (на каждое сообщение и каждый новый токен):**

\- Текст промпта → **llama_tokenize** (модель.vocab) → массив токенов → батч (token, pos, seq_id, logits).\
 **Для чего:**\
 \
один вызов decode принимает один батч.\
 \- Батч → **llama_decode** → при необходимости encode (токены → эмбеддинги из модели) → balloc->init (позиции из memory) → цикл: memory->init_batch → ubatch → process_ubatch (build_graph по модели, set_inputs, graph_compute на sched) → логиты копируются в буфер контекста.\
 **Для чего:**\
 \
прогон батча через модель и получение логитов.\
 \- Логиты → **llama_get_logits_ith** → сэмплер → следующий токен → **llama_token_to_piece** (модель.vocab) → текст.\
 **Для чего:**\
 \
по логитам выбирается один токен и переводится в текст для вывода.\
 \- Токен добавляется в батч; при следующем decode в батче один новый токен; memory обновляет занятые позиции KV-cache; цикл повторяется до EOS или лимита.

**Сводка потоков данных:**

\- Файл GGUF → загрузчик → модель (hparams, vocab, тензоры). Модель + параметры → контекст (memory, sched, графы).\
 \- Промпт → vocab (токенизация) → батч. Батч → контекст.decode → encode (модель: эмбеддинги) → balloc, memory → process_ubatch (модель: build_graph, веса; memory: KV-cache; sched: выполнение графа) → логиты в контексте.\
 \- Логиты → сэмплер → токен → vocab (token_to_piece) → текст. Модель и контекст — центральные объекты; загрузчик, balloc, memory, sched — вспомогательные, привязаны к контексту или модели.

[Copyright: Roman Kryvolapov](https://t.me/RomanKryvolapov)
