---
title: "LLama.cpp AI LLM Engine — Як це працює"
url: "https://romankryvolapov.com/uk/llama-cpp/"
description: "Як влаштований інференс у llama.cpp: завантаження GGUF, токенізація, побудова обчислювального графа, KV-cache та семплінг токенів."
language: uk
updated: 2026-02-04
---
## Передмова

**Llama.cpp** — це програма (рушій) для запуску великих мовних моделей (LLM) на комп'ютері або на мобільних пристроях — на процесорі (CPU) чи відеокарті (GPU); я написав застосунок [Offline AI Launcher](/uk/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](/uk/offline-ai-launcher/) (запуск моделей на смартфоні);\
 \- KoboldCpp;\
 \- Text Generation WebUI.\
 Репозиторій: [github.com/ggml-org/llama.cpp](https://github.com/ggml-org/llama.cpp). Приклади коду в статті можуть відрізнятися від вашої версії.

**Як читати статтю:**\
 \
 \- розділи йдуть у порядку виконання сценарію;\
 \- у кожному розділі для дії наводиться цитата з коду та пояснення, що відбувається і для чого;\
 \- файли вихідників указані в тексті;\
 \- стаття розрахована на непідготовленого читача: усі терміни пояснюються в глосарії на початку статті, усі кроки супроводжуються цитатами коду та поясненнями.

## Глосарій термінів

**Батч (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) і виділяє під граф буфери на цих пристроях. При виконанні графа планувальник обходить вузли в топологічному порядку і запускає операції на обраних пристроях.

## Схема процесу: усі кроки по порядку

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

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

крок 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

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

## Структура репозиторію та основні файли

У репозиторії 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**, семплери тощо.\
 **Для чого:**\
 \
застосунок підключає цей заголовок і викликає оголошені функції, не заходячи у внутрішні файли рушія.

## Основні типи та структури (довідково)

Для орієнтування в коді корисно знати основні типи. Нижче — що зберігає кожен тип і для чого він потрібен.

**Основні структури:**

\- **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: Ініціалізація бекенда

**Крок 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: Завантаження моделі з файлу (точка входу)

**Крок 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: Підготовка до читання файлу

**Крок 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: Покрокове завантаження моделі з файлу

**Кроки 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 — відкриття файлу та карта тензорів

**Крок 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 (довідково)

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: Визначення типу моделі (архітектура)

**Крок 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: Завантаження гіперпараметрів — розміри й контекст

**Крок 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: Завантаження словника й токенайзера

**Крок 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

**Крок 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: Створення контексту інференсу

**Крок 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: Надходить текст промпту від користувача

**Крок 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: Токенізація — від тексту до послідовності токенів

**Крок 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: Формування батча для одного виклику

**Крок 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: Токени перетворюються на ембединги

**Крок 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, підбатчі)

**Кроки 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: Підготовка до проганяння підбатча (пам'ять, параметри графа)

**Крок 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: Побудова обчислювального графа та алокація буферів

**Крок 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

**Крок 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: Логіти в буфері та вибір наступного токена (семплінг)

**Кроки 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 (довідково)

У механізмі уваги кожен токен отримує вектор запиту (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** нові токени отримають наступні позиції.

## Резюме: від файлу до першого токена

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

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

\- **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.

## Деталізація реалізації за файлами

Нижче — коротка прив'язка описаних у статті кроків до конкретних файлів і функцій репозиторію 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, семплінг, виведення.

## Зв'язок компонентів і потоки даних

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

**Завантаження (один раз при старті або зміні моделі):**

\- Файл 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)
