---
title: "llama.cpp AI LLM Engine – Как работи"
url: "https://romankryvolapov.com/bg/llama-cpp/"
description: "Как е устроен инференсът в llama.cpp: зареждане на GGUF, токенизация, изграждане на изчислителния граф, KV-cache и семплиране на следващия токен."
language: bg
published: 2025-06-10
updated: 2026-11-13
---
## Предговор

**llama.cpp** е програма (двигател) за стартиране на големи езикови модели (LLM) на компютър или на мобилни устройства – върху процесора (CPU) или видеокартата (GPU); написах приложението [Offline AI Launcher](/bg/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](/bg/offline-ai-launcher/) (стартиране на модели на смартфон);
- KoboldCpp;
- Text Generation WebUI.

Хранилище: [github.com/ggml-org/llama.cpp](https://github.com/ggml-org/llama.cpp). Кодът в статията е взет от клона master при комит 8887a48f, веднага след релийза b10736; в друга версия детайлите може да се различават.

**Как да четете статията:**

- разделите следват реда на изпълнение на сценария;
- във всеки раздел за съответното действие са дадени цитат от кода и обяснение какво се случва и за какво е нужно;
- файловете с изходен код са посочени в текста;
- статията е предназначена за неподготвен читател: всички термини са обяснени в речника в началото на статията, а всички стъпки са придружени с цитати от кода и пояснения.

## Речник на термините

**Батч (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: определяне на типа на модела – архитектурата, вече прочетена от зареждача от ключа general.architecture в GGUF, определя кой клас модел се създава (`llama_model_create`)
- стъпка 4: зареждане на хиперпараметрите – размери, контекст (`load_hparams`)
- стъпка 5: зареждане на речника и токенайзера (`load_vocab`)
- стъпка 6: зареждане на теглата в паметта или на GPU (`load_tensors`)
- стъпка 7: създаване на контекст за инференс – KV-cache, планировчик (`llama_init_from_model`).

<div style="overflow-x:auto;margin:1.5rem 0" tabindex="0" role="region">
<svg viewBox="0 0 780 470" width="100%" style="min-width:620px;max-width:780px;height:auto;display:block;margin:0 auto" role="img" aria-label="Подготовка: от файла GGUF до готов контекст, стъпки 1–7" xmlns="http://www.w3.org/2000/svg">
  <defs>
    <marker id="lc-prepare-head" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
      <path d="M0 0 L10 5 L0 10 z" fill="var(--color-fg-dim)"/>
    </marker>
  </defs>
  <rect x="100" y="20" width="420" height="76" rx="8" fill="var(--color-bg-elevated)" stroke="var(--color-border)" stroke-width="1.5"/>
  <text x="310" y="44" text-anchor="middle" fill="var(--color-fg)" font-family="var(--font-mono)" font-size="14" font-weight="500">llama_backend_init()</text>
  <text x="310" y="64" text-anchor="middle" fill="var(--color-fg-dim)" font-family="var(--font-sans)" font-size="12.5">регистър на бекенди: CPU, CUDA, Metal, Vulkan</text>
  <text x="90" y="62" text-anchor="end" fill="var(--color-fg-dim)" font-family="var(--font-sans)" font-size="12.5" font-weight="400">стъпка 1</text>
  <rect x="100" y="132" width="420" height="76" rx="8" fill="var(--color-bg-elevated)" stroke="var(--color-border)" stroke-width="1.5"/>
  <text x="310" y="156" text-anchor="middle" fill="var(--color-fg)" font-family="var(--font-mono)" font-size="14" font-weight="500">llama_model_load_from_file()</text>
  <text x="310" y="176" text-anchor="middle" fill="var(--color-fg-dim)" font-family="var(--font-sans)" font-size="12.5">отваря GGUF, строи карта на тензорите</text>
  <text x="90" y="174" text-anchor="end" fill="var(--color-fg-dim)" font-family="var(--font-sans)" font-size="12.5" font-weight="400">стъпка 2</text>
  <rect x="100" y="244" width="420" height="96" rx="8" fill="var(--color-bg-elevated)" stroke="var(--color-border)" stroke-width="1.5"/>
  <text x="310" y="268" text-anchor="middle" fill="var(--color-fg)" font-family="var(--font-mono)" font-size="14" font-weight="500">llama_model_load()</text>
  <text x="310" y="288" text-anchor="middle" fill="var(--color-fg-dim)" font-family="var(--font-sans)" font-size="12.5">llama_model_create · load_hparams · load_vocab · load_tensors</text>
  <text x="310" y="305" text-anchor="middle" fill="var(--color-fg-dim)" font-family="var(--font-sans)" font-size="12.5">архитектура, размери, речник, тегла</text>
  <text x="90" y="296" text-anchor="end" fill="var(--color-fg-dim)" font-family="var(--font-sans)" font-size="12.5" font-weight="400">стъпки 3–6</text>
  <rect x="100" y="376" width="420" height="76" rx="8" fill="var(--color-bg-elevated)" stroke="var(--color-border)" stroke-width="1.5"/>
  <text x="310" y="400" text-anchor="middle" fill="var(--color-fg)" font-family="var(--font-mono)" font-size="14" font-weight="500">llama_init_from_model()</text>
  <text x="310" y="420" text-anchor="middle" fill="var(--color-fg-dim)" font-family="var(--font-sans)" font-size="12.5">контекст: KV-cache, планировчик, графи</text>
  <text x="90" y="418" text-anchor="end" fill="var(--color-fg-dim)" font-family="var(--font-sans)" font-size="12.5" font-weight="400">стъпка 7</text>
  <path d="M310 96 L310 132" fill="none" stroke="var(--color-fg-dim)" stroke-width="1.5" marker-end="url(#lc-prepare-head)"/>
  <path d="M310 208 L310 244" fill="none" stroke="var(--color-fg-dim)" stroke-width="1.5" marker-end="url(#lc-prepare-head)"/>
  <path d="M310 340 L310 376" fill="none" stroke="var(--color-fg-dim)" stroke-width="1.5" marker-end="url(#lc-prepare-head)"/>
  <rect x="570" y="176" width="190" height="100" rx="8" fill="var(--color-bg-elevated)" stroke="var(--color-link)" stroke-width="1.5"/>
  <text x="665" y="200" text-anchor="middle" fill="var(--color-link)" font-family="var(--font-mono)" font-size="14" font-weight="500">model.gguf</text>
  <text x="665" y="220" text-anchor="middle" fill="var(--color-fg-dim)" font-family="var(--font-sans)" font-size="12.5">хедър, метаданни</text>
  <text x="665" y="237" text-anchor="middle" fill="var(--color-fg-dim)" font-family="var(--font-sans)" font-size="12.5">тензорна карта и тегла</text>
  <path d="M570 200 L545 200 L545 170 L520 170" fill="none" stroke="var(--color-fg-dim)" stroke-width="1.5" marker-end="url(#lc-prepare-head)"/>
  <path d="M570 252 L545 252 L545 292 L520 292" fill="none" stroke="var(--color-fg-dim)" stroke-width="1.5" stroke-dasharray="5 4" marker-end="url(#lc-prepare-head)"/>
  <text x="556" y="320" text-anchor="middle" fill="var(--color-fg-dim)" font-family="var(--font-mono)" font-size="12" font-weight="400">mmap</text>
</svg>
</div>

**Генериране (за всяко съобщение и всеки нов токен в отговора):**

- стъпка 8: идва текстът на промпта от потребителя
- стъпка 9: токенизация – текстът се превръща в последователност от токени (`llama_tokenize`)
- стъпка 10: формиране на батч – токените се пакетират за едно извикване (`llama_batch_get_one` или `common_batch_add`), а вътре в извикването батчът се проверява и разпределя (`balloc->init`)
- стъпка 11: `llama_decode` – входната точка на стъпката на генериране: батчът се проверява и се предава във вътрешния път на декодиране (`llama_context::decode`)
- стъпка 12: при дълъг промпт батчът се разбива на подбатчове (`memory->init_batch`)
- стъпка 13: всеки подбатч се прекарва през модела: изграждане на графа → изпълнение на CPU/GPU (`process_ubatch` → `build_graph` → `graph_compute`)
- стъпка 14: логитите се копират от изхода на графа в буфера на контекста – по един ред за всяка позиция, маркирана в батча за изход; в цикъла на генериране това е само последната позиция
- стъпка 15: семплиране – по логитите се избира един следващ токен
- стъпка 16: ако токенът завършва генерирането (`llama_vocab_is_eog` – тук влизат EOS, EOT и други токени за край на генерирането), цикълът приключва; в противен случай токенът се превръща в текст и се извежда (`llama_token_to_piece`), от този един токен се съставя нов батч и управлението се връща към стъпка 11.

<div style="overflow-x:auto;margin:1.5rem 0" tabindex="0" role="region">
<svg viewBox="0 0 830 470" width="100%" style="min-width:620px;max-width:830px;height:auto;display:block;margin:0 auto" role="img" aria-label="Цикъл на генериране: от промпта до отговора, стъпки 8–16" xmlns="http://www.w3.org/2000/svg">
  <defs>
    <marker id="lc-generate-head" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
      <path d="M0 0 L10 5 L0 10 z" fill="var(--color-fg-dim)"/>
    </marker>
  </defs>
  <rect x="50" y="30" width="220" height="86" rx="8" fill="var(--color-bg-elevated)" stroke="var(--color-link)" stroke-width="1.5"/>
  <text x="160" y="54" text-anchor="middle" fill="var(--color-link)" font-family="var(--font-sans)" font-size="14" font-weight="500">промпт</text>
  <text x="160" y="74" text-anchor="middle" fill="var(--color-fg-dim)" font-family="var(--font-sans)" font-size="12.5">стъпка 8</text>
  <text x="160" y="91" text-anchor="middle" fill="var(--color-fg-dim)" font-family="var(--font-sans)" font-size="12.5">текст от потребителя</text>
  <rect x="310" y="30" width="220" height="86" rx="8" fill="var(--color-bg-elevated)" stroke="var(--color-border)" stroke-width="1.5"/>
  <text x="420" y="54" text-anchor="middle" fill="var(--color-fg)" font-family="var(--font-mono)" font-size="14" font-weight="500">llama_tokenize</text>
  <text x="420" y="74" text-anchor="middle" fill="var(--color-fg-dim)" font-family="var(--font-sans)" font-size="12.5">стъпка 9</text>
  <text x="420" y="91" text-anchor="middle" fill="var(--color-fg-dim)" font-family="var(--font-sans)" font-size="12.5">текст → токени</text>
  <rect x="570" y="30" width="220" height="86" rx="8" fill="var(--color-bg-elevated)" stroke="var(--color-border)" stroke-width="1.5"/>
  <text x="680" y="54" text-anchor="middle" fill="var(--color-fg)" font-family="var(--font-mono)" font-size="14" font-weight="500">llama_batch_get_one</text>
  <text x="680" y="74" text-anchor="middle" fill="var(--color-fg-dim)" font-family="var(--font-sans)" font-size="12.5">стъпка 10</text>
  <text x="680" y="91" text-anchor="middle" fill="var(--color-fg-dim)" font-family="var(--font-sans)" font-size="12.5">токени → батч</text>
  <rect x="50" y="175" width="220" height="86" rx="8" fill="var(--color-bg-elevated)" stroke="var(--color-accent)" stroke-width="1.5"/>
  <text x="160" y="199" text-anchor="middle" fill="var(--color-accent)" font-family="var(--font-mono)" font-size="14" font-weight="500">llama_decode</text>
  <text x="160" y="219" text-anchor="middle" fill="var(--color-fg-dim)" font-family="var(--font-sans)" font-size="12.5">стъпка 11</text>
  <text x="160" y="236" text-anchor="middle" fill="var(--color-fg-dim)" font-family="var(--font-sans)" font-size="12.5">проверка на батч, позиции</text>
  <rect x="310" y="175" width="220" height="86" rx="8" fill="var(--color-bg-elevated)" stroke="var(--color-border)" stroke-width="1.5"/>
  <text x="420" y="199" text-anchor="middle" fill="var(--color-fg)" font-family="var(--font-mono)" font-size="14" font-weight="500">memory-&gt;init_batch</text>
  <text x="420" y="219" text-anchor="middle" fill="var(--color-fg-dim)" font-family="var(--font-sans)" font-size="12.5">стъпка 12</text>
  <text x="420" y="236" text-anchor="middle" fill="var(--color-fg-dim)" font-family="var(--font-sans)" font-size="12.5">батч → подбатчове</text>
  <rect x="570" y="175" width="220" height="86" rx="8" fill="var(--color-bg-elevated)" stroke="var(--color-border)" stroke-width="1.5"/>
  <text x="680" y="199" text-anchor="middle" fill="var(--color-fg)" font-family="var(--font-mono)" font-size="14" font-weight="500">process_ubatch</text>
  <text x="680" y="219" text-anchor="middle" fill="var(--color-fg-dim)" font-family="var(--font-sans)" font-size="12.5">стъпка 13</text>
  <text x="680" y="236" text-anchor="middle" fill="var(--color-fg-dim)" font-family="var(--font-sans)" font-size="12.5">графът се строи и смята</text>
  <rect x="50" y="320" width="220" height="86" rx="8" fill="var(--color-bg-elevated)" stroke="var(--color-border)" stroke-width="1.5"/>
  <text x="160" y="344" text-anchor="middle" fill="var(--color-fg)" font-family="var(--font-mono)" font-size="14" font-weight="500">logits</text>
  <text x="160" y="364" text-anchor="middle" fill="var(--color-fg-dim)" font-family="var(--font-sans)" font-size="12.5">стъпка 14</text>
  <text x="160" y="381" text-anchor="middle" fill="var(--color-fg-dim)" font-family="var(--font-sans)" font-size="12.5">ред за всеки изход</text>
  <rect x="310" y="320" width="220" height="86" rx="8" fill="var(--color-bg-elevated)" stroke="var(--color-border)" stroke-width="1.5"/>
  <text x="420" y="344" text-anchor="middle" fill="var(--color-fg)" font-family="var(--font-mono)" font-size="14" font-weight="500">llama_sampler_sample</text>
  <text x="420" y="364" text-anchor="middle" fill="var(--color-fg-dim)" font-family="var(--font-sans)" font-size="12.5">стъпка 15</text>
  <text x="420" y="381" text-anchor="middle" fill="var(--color-fg-dim)" font-family="var(--font-sans)" font-size="12.5">един следващ токен</text>
  <rect x="570" y="320" width="220" height="86" rx="8" fill="var(--color-bg-elevated)" stroke="var(--color-link)" stroke-width="1.5"/>
  <text x="680" y="344" text-anchor="middle" fill="var(--color-link)" font-family="var(--font-mono)" font-size="14" font-weight="500">llama_token_to_piece</text>
  <text x="680" y="364" text-anchor="middle" fill="var(--color-fg-dim)" font-family="var(--font-sans)" font-size="12.5">стъпка 16</text>
  <text x="680" y="381" text-anchor="middle" fill="var(--color-fg-dim)" font-family="var(--font-sans)" font-size="12.5">токен → текст</text>
  <path d="M270 73 L310 73" fill="none" stroke="var(--color-fg-dim)" stroke-width="1.5" marker-end="url(#lc-generate-head)"/>
  <path d="M530 73 L570 73" fill="none" stroke="var(--color-fg-dim)" stroke-width="1.5" marker-end="url(#lc-generate-head)"/>
  <path d="M270 218 L310 218" fill="none" stroke="var(--color-fg-dim)" stroke-width="1.5" marker-end="url(#lc-generate-head)"/>
  <path d="M530 218 L570 218" fill="none" stroke="var(--color-fg-dim)" stroke-width="1.5" marker-end="url(#lc-generate-head)"/>
  <path d="M270 363 L310 363" fill="none" stroke="var(--color-fg-dim)" stroke-width="1.5" marker-end="url(#lc-generate-head)"/>
  <path d="M530 363 L570 363" fill="none" stroke="var(--color-fg-dim)" stroke-width="1.5" marker-end="url(#lc-generate-head)"/>
  <path d="M790 73 L810 73 L810 145 L160 145 L160 175" fill="none" stroke="var(--color-fg-dim)" stroke-width="1.5" marker-end="url(#lc-generate-head)"/>
  <path d="M790 218 L810 218 L810 290 L160 290 L160 320" fill="none" stroke="var(--color-fg-dim)" stroke-width="1.5" marker-end="url(#lc-generate-head)"/>
  <path d="M680 406 L680 442 L22 442 L22 218 L50 218" fill="none" stroke="var(--color-fg-dim)" stroke-width="1.5" marker-end="url(#lc-generate-head)"/>
  <text x="415" y="436" text-anchor="middle" fill="var(--color-fg-dim)" font-family="var(--font-sans)" font-size="12" font-weight="400">новият токен става батч от един токен – до токена за край на отговора</text>
</svg>
</div>

## Общият процес на инференс на LLM

По-горе е дадена схема: какво след какво се случва. По смисъл процесът се дели на два етапа. Първият е подготовка: инициализация на бекенда, зареждане на модела от GGUF (архитектура, хиперпараметри, речник, тегла) и създаване на контекст. Изпълнява се веднъж при старт или при смяна на модела. Вторият етап е генерирането: при всяко съобщение на потребителя текстът се превръща в токени, пакетира се в батч, прекарва се през модела (`llama_decode`), от логитите се избира следващият токен, той се превръща в текст и се извежда; цикълът се повтаря до токена „край на изхода“ (EOS) или до достигане на лимита. Този цикъл се повтаря за всяко ново съобщение и за всеки нов токен в отговора. По-долу всяка стъпка от схемата се разглежда поотделно: какво точно се извиква в кода, какво се случва и какво може да е неочевидно за неподготвения читател.

## Структура на хранилището и основни файлове

В хранилището на `llama.cpp` основните части на двигателя са разпределени по папки и файлове.

**Защо е направено така:**

- за да се разделят отговорностите: зареждането на файла, моделът, речникът, контекстът и батчът са в различни файлове;
- така кодът се търси и дебъгва по-лесно.

По-долу са изброени файловете, които пряко се отнасят до зареждането на модела и до инференса; за всеки е посочено какво се намира в него и за какво е нужно.

**Входна точка и зареждане на модела:**

- `src/llama.cpp` – тук живеят функциите `llama_backend_init`, `llama_model_load_from_file`, `llama_model_load` (статична).

  **За какво е нужно:** това е „входната врата“ към двигателя: приложението извиква тези функции, за да инициализира библиотеката и да зареди модела; тук се създава и зареждачът, и се изисква обект на модела за архитектурата, прочетена от файла. Самата фабрика `llama_model_create` (тя пита зареждача за архитектурата чрез `get_arch`) този файл само я извиква – тя се намира в `src/llama-model.cpp`. След това последователно се извикват `load_hparams`, `load_vocab` и `load_tensors`.

- `src/llama-model-loader.cpp` – класът `llama_model_loader`: отваряне на GGUF файла, построяване на индекс на тензорите (`weights_map`), методите `get_weight` и `get_tensor_meta` за търсене на тензор по име и `load_all_data` за четене на самите данни на тензорите.

  **За какво е нужно:** зареждачът е нужен, за да се знае по името на тензора къде във файла са неговите данни и как да се прочетат или да се проектират в паметта (mmap); без него не може стъпка по стъпка да се заредят архитектурата, хиперпараметрите, речникът и теглата.

**Модел и речник:**

- `src/llama-model.cpp` – класът `llama_model` и неговата базова реализация `llama_model_base`: методите `load_hparams`, `load_vocab`, `load_tensors`, създаване на тензори и назначаване на буфери. Това, което се различава от архитектура на архитектура, е изнесено във `load_arch_hparams` и `load_arch_tensors`, които всяко семейство модели реализира в свой файл в `src/models/`.

  **За какво е нужно:** обектът на модела съхранява всичко, прочетено от файла: типа архитектура, хиперпараметрите, речника и самите тегла (тензорите); методите load_\* един след друг попълват тези данни от зареждача. Самата архитектура се определя по-рано, при създаването на обекта на модела: `llama_model_create` я пита зареждача (`get_arch`) и построява клас на подходящото семейство.

- `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-graph.cpp` – построяване на изчислителния граф: класът `llm_graph_context` с `build_inp_embd` (търсене на ембединги по таблица) и резултатът от графа `llm_graph_result` с методите `set_inputs` и `can_reuse`.

  **За какво е нужно:** графът е това, което реално се изчислява при decode: тук се събират входните му възли, преди старта се попълват с данните на подбатча и се решава дали може да се преизползва предишният граф, вместо да се строи наново.

- `src/llama-batch.cpp` – класът `llama_batch_allocr` и неговият метод `init` (попълване на позиции и флагове за логити); самата структура `llama_batch` е обявена в `include/llama.h`.

  **За какво е нужно:** батчът е „пакетът“ от токени за едно извикване на decode; алокаторът проверява батча и при липса на полета ги попълва (позициите – от паметта, логитите – само за последния токен), за да не се налага на извикващия код да задава всичко ръчно.

- `src/llama-kv-cache.cpp` – заделяне и обновяване на KV-cache и неговият `init_batch` (разбиване на подбатчове и резервиране за тях на места в кеша). Самият интерфейс е обявен отделно, в `src/llama-memory.h`, защото моделът може да използва памет от друг вид – рекурентно състояние или хибрид – и всеки вариант реализира същия `init_batch` в свой файл.

  **За какво е нужно:** KV-cache съхранява вече изчислените ключове и стойности за всички предишни позиции, за да не се изчисляват отново на всяка стъпка; `init_batch` разбива големия батч на подбатчове с ограничен размер, за да не се препълни паметта.

- `src/llama-sampler.cpp` – семплери и верига от тях: `llama_sampler_chain_init`, `llama_sampler_chain_add`, `llama_sampler_sample`.

  **За какво е нужно:** след decode логитите все още трябва да се превърнат в един токен: веригата прекарва последователно семплерите и връща избрания токен.

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

- `ggml/` (директория вътре в хранилището – библиотеката се намира направо тук, а не е свързана като подмодул) – изчислителен граф (GGML), типове тензори, планировчик (`ggml_backend_sched`) и бекенди за CPU/GPU; публичните хедъри са в `ggml/include/`, бекендите – в поддиректориите на `ggml/src/`.

  **За какво е нужно:** графът описва кои операции (умножения на матрици, активации и т.н.) да се изпълнят и в какъв ред; планировчикът решава на кое устройство (CPU или GPU) да се изчислява всеки възел на графа.

- **gguf** (`ggml/include/gguf.h` и `ggml/src/gguf.cpp` – част от ggml, а не отделна библиотека) – четене и запис на формата 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. В него има: метаданните, прочетени от файла (`metadata`), картата на тензорите (`weights_map`), отворените файлове (`files`) и техните проекции в паметта (`mappings`).

  **За какво е нужно:** зареждачът живее само докато се зарежда моделът; чрез него последователно се четат архитектурата, хиперпараметрите, речникът и тензорите; след зареждането не е нужен.

- `ggml_context` – контекстът на графа GGML: в него се създават възлите и тензорите на графа.

  **За какво е нужно:** графът описва последователност от операции (умножения, активации и т.н.); всички възли на графа се създават в един такъв контекст.

- `ggml_backend_sched` – планировчикът: списък с бекенди и логика за разпределяне на възлите на графа по устройствата.

  **За какво е нужно:** планировчикът решава на кое устройство (CPU или GPU) да се изпълнява всеки възел на графа и заделя за графа буфери на тези устройства.

**Какво връщат основните функции:**

- `llama_model_load_from_file` връща `llama_model*` или `nullptr` при грешка.

  **За какво е нужно:** приложението проверява указателя: ако не е nullptr, моделът е зареден и може да се създава контекст.
- `llama_model_load` – помощна функция вътре в `src/llama.cpp`, в публичния хедър я няма – връща двойка от статус и модел: 0 при успех, -1 при грешка, -2 при отказ чрез callback за прогреса.

  **За какво е нужно:** при отрицателен статус указателят към модела вече е нулев, така че извикващият код само записва в лога причината и връща на приложението nullptr.
- `llama_decode` връща 0 при успех. Положителна стойност е предупреждение, а не фатална грешка: 1 означава, че за този батч не е намерено свободно място в KV-cache (трябва да се намали батчът или да се увеличи контекстът), 2 – че извикването е било прекъснато. -1 означава, че самият батч е некоректен, а всичко под -1 е фатална грешка.

  **За какво е нужно:** по върнатата стойност приложението разбира дали decode е успял и дали могат да се четат логитите; при 1 може да се опита отново с по-малък батч, а след 2 или фатална грешка част от батча вече е попаднала в паметта на контекста, така че докъде е стигнал се налага да се пита контекстът отделно.
- `llama_tokenize` връща броя записани токени, но не повече от размера на подадения буфер. Ако буферът е малък, връща се отрицателно число, по модул равно на това колко токена биха се получили – затова обикновено я извикват първо с празен буфер, заделят толкова елемента и я извикват още веднъж.

  **За какво е нужно:** за да се знае колко елемента от масива с токени са попълнени и какъв размер трябва да има масивът.
- `llama_get_logits_ith` връща указател към масив от float с размер `n_vocab` – логитите на i-тия токен от последния decode; при неотрицателно i аргументът е индексът на токена вътре в батча, който контекстът превежда в ред от изходите чрез `output_ids`, така че за токен, чиито логити не са били поискани, ще се върне NULL; отрицателният индекс се отчита по редовете на изходите, тоест -1 е последният. При некоректен индекс се връща NULL.

  **За какво е нужно:** по този масив семплерът избира следващия токен (по едно число за всеки токен от речника); в цикъла това почти винаги е индекс -1, току-що изчислената позиция.

## Стъпка 1: Инициализация на бекенда

**Стъпка 1** в схемата на процеса е инициализацията на бекенда. Преди зареждането на модела и инференса приложението веднъж извиква `llama_backend_init()`. Извикването върши три неща: пуска прецизен таймер, по който по-късно се измерва времето за зареждане и за decode, създава и веднага освобождава празен контекст на GGML и – това е най-важното – попълва регистъра на бекендите, ако той още е празен, за да има двигателят върху какво да изчислява: процесорът, а ако за видеокартата има бекенд, то и тя. Без регистриран бекенд зареждането не се влошава тихо, а се проваля: двигателят връща nullptr и пише, че бекендите не са заредени. По-долу е цитатът изцяло и на фрагменти (файл `src/llama.cpp`).

```cpp
// Стъпка 1: единствената входна точка за инициализация на двигателя; извиква се веднъж при старт на приложението
void llama_backend_init(void) {
    ggml_time_init();  // прецизен таймер; на Windows трябва да се инициализира изрично

    // нужно за инициализация на таблиците f16
    {
        struct ggml_init_params params = { 0, NULL, false };
        struct ggml_context * ctx = ggml_init(params);
        ggml_free(ctx);
    }

    // ако още нищо не е регистрирано – зареждаме бекендите (CPU, CUDA, Metal, Vulkan ...);
    // именно това дава на двигателя устройствата, на които ще изчислява
    if (!ggml_backend_reg_count()) {
        ggml_backend_load_all();
    }
}
```

**Какво се случва в кода стъпка по стъпка:**

```cpp
// Фрагмент 1: таймерът е нужен, за да се измери по-късно времето за зареждане на модела и времето за decode
ggml_time_init();

// Фрагмент 2: временен контекст с нулев буфер – създава се и веднага се освобождава.
// Коментарът в изходния код го нарича „инициализация на таблиците f16“, но в сегашния ggml първото
// извикване на ggml_init() само пуска таймера – блокът е останал като рудимент и нищо не струва
struct ggml_init_params params = { 0, NULL, false };
struct ggml_context * ctx = ggml_init(params);
ggml_free(ctx);

// Фрагмент 3: същинската част – ако регистърът е празен, зареждаме библиотеките на бекендите
// (CPU, CUDA, Metal, Vulkan и така нататък); без тях няма върху какво да се изчислява
if (!ggml_backend_reg_count()) {
    ggml_backend_load_all();
}
```

Първо се включва таймерът – по него по-късно се измерват времето за зареждане и времето за decode. След това се създава и веднага се освобождава временен контекст на GGML с нулев буфер; коментарът до него в изходния код още твърди, че така се инициализират таблиците f16, но в сегашния ggml първото извикване на `ggml_init` само пуска таймера, а таблиците за половинна точност се строят по-късно, когато се регистрира бекендът за CPU. Истинската работа върши последният блок: ако още никой бекенд не е регистриран, `ggml_backend_load_all` търси библиотеките на бекендите и регистрира намерените. След това двигателят знае на кои устройства може да изчислява и е готов за зареждане на модела и построяване на графа.

## Стъпка 2а: Зареждане на модела от файл (входна точка)

**Стъпка 2** е зареждането на модела от файл. Започва с извикването на `llama_model_load_from_file`: приложението подава пътя до .gguf и параметрите, а двигателят връща готов обект на модела или nullptr при грешка.

**За какво е нужна тази функция:**

- тя е единствената входна точка за зареждане на модел от файл;
- приложението подава пътя до файла и параметрите, а двигателят връща готов обект на модела или nullptr при грешка.

Цитат от кода (файл `src/llama.cpp`):

```cpp
// Входна точка за зареждане на модел (стъпка 2): път до .gguf и параметри на зареждането
struct llama_model * llama_model_load_from_file(
        const char * path_model,
        struct llama_model_params params) {
    // splits – пътища до частите на разбит на файлове модел; тук е празно, зареждачът ще ги изведе сам
    std::vector<std::string> splits = {};
    // трите nullptr и FILE * са останалите възможни източници на модела
    // (подготвени в паметта метаданни на GGUF и вече отворен файл); може да е зададен само един
    return llama_model_load_from_file_impl(nullptr, nullptr, nullptr, path_model, splits, /*file*/ nullptr, params);
}
```

Функцията приема пътя до файла на модела (обикновено .gguf) и параметрите на зареждането – приложението указва откъде да се чете моделът, как да се чете и колко слоя да се поставят на видеокартата. `splits` тук винаги е празен: ако в метаданните е казано, че моделът е разбит на няколко файла, пътищата до частите зареждачът извежда сам от името на файла – затова това име трябва да следва шаблона `<name>-00001-of-00003.gguf`. Изричен списък с части се подава през отделна входна точка – `llama_model_load_from_splits`. Всичко останало – проверката, че бекендът е регистриран, callback-ът за прогреса по подразбиране, извикването на `llama_model_load` и обработката на резултата от него – става в `llama_model_load_from_file_impl`.

**Параметри на зареждането (`llama_model_params`), важни за разбирането:**

- `load_mode` – как да се чете файлът: mmap, mlock, директен вход-изход или обикновено четене в буфер.

  **За какво е нужно:** mmap спестява RAM и ускорява старта на зареждането, защото файлът се проектира, а не се копира; директният вход-изход на някои дискове и системи дава по-предвидима скорост на четене. По подразбиране е auto: двигателят избира mmap и преминава към обикновено четене, ако някое от устройствата не умее да работи с проектирана памет. Вътре в зареждача тази единствена стойност отново се превръща в два флага – `use_mmap` и `use_direct_io`; именно в този вид се срещат по-нататък.
- `n_gpu_layers` – колко слоя да се заредят на GPU (останалите – на CPU).

  **За какво е нужно:** за да върви част от изчисленията на видеокартата, а част на процесора.
- `progress_callback` – 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(
        struct gguf_context * metadata,
        llama_model_set_tensor_data_t set_tensor_data,
        void * set_tensor_data_ud,
        const std::string & path_model,
        std::vector<std::string> & splits,
        FILE * file,
        struct llama_model_params params) {
    // ... пропуснато: проверка, че е зададен точно един източник – metadata, path_model или file
    ggml_time_init();  // таймер за измерване на времето за зареждане

    // Ако зареждаме не само речника – проверяваме, че е регистриран поне един бекенд
    if (!params.vocab_only && ggml_backend_reg_count() == 0) {
        LLAMA_LOG_ERROR("%s: no backends are loaded. hint: use ggml_backend_load() or ggml_backend_load_all() to load a backend before calling this function\n", __func__);
        return nullptr;
    }

    unsigned cur_percentage = 0;
    // Ако не е подаден callback за прогреса – слагаме свой: извежда точки до 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;  // не отменяме зареждането
        };
    }
```

Тази реализация е обща за всички публични входни точки, затова приема едновременно всички възможни източници на модела – готови метаданни на GGUF, път (частите на разбития на файлове модел се подават в `splits`) или вече отворен файл – и започва с проверка, че е подаден точно един от тях.

**Какво се случва в този фрагмент и за какво е нужно:**

- `ggml_time_init()` – включва се таймерът.

  **За какво е нужно:** за да се измери по-късно колко е отнело зареждането.
- Проверката `ggml_backend_reg_count() == 0` – има ли поне един бекенд (CPU или GPU).

  **За какво е нужно:** без бекенд няма да може да се изпълняват изчисленията на модела; при зареждане само на речника (`vocab_only`) бекендът не е задължителен.
- Настройка на callback-а за прогреса – ако приложението не е подало свой, слага се callback по подразбиране, който извежда точки.

  **За какво е нужно:** потребителят вижда, че зареждането върви; при желание може да се подаде свой callback и да се показва лента за напредъка или да се отмени зареждането (като върне false).
- Самият обект на модела тук не се създава. Той се появява едно ниво по-надолу, вътре в `llama_model_load`, при извикването на `llama_model_create` – и то едва след като е отворен зареждачът на GGUF.

  **За какво е нужно:** класът на обекта зависи от архитектурата, записана във файла (LLaMA, Gemma, Qwen и така нататък), затова първо трябва да се прочете архитектурата от GGUF; хиперпараметрите, речникът и тензорите се записват вече в този обект.

Останалата част от функцията е само извикване на вътрешния зареждач и обработка на резултата от него (същият файл, краят на `llama_model_load_from_file_impl`):

```cpp
    // Вътрешно зареждане: чете файла и сглобява модела (arch, hparams, vocab, tensors)
    const auto [status, model] = llama_model_load(metadata, set_tensor_data, set_tensor_data_ud, path_model, splits, file, params);
    GGML_ASSERT(status <= 0);
    if (status < 0) {
        if (status == -1) {
            LLAMA_LOG_ERROR("%s: failed to load model\n", __func__);
        } else if (status == -2) {
            LLAMA_LOG_INFO("%s: cancelled model load\n", __func__);
        }

        if (model) {
            llama_model_free(model);
        }
        return nullptr;
    }

    return model;
}
```

- Списъкът с устройства също се строи едно ниво по-надолу, в `llama_prepare_model_devices`, която `llama_model_load` извиква веднага след създаването на обекта на модела; резултатът отива в `model->devices`.

  **За какво е нужно:** от него зависи по кои ускорители ще се разпределят слоевете на модела (вж. `load_tensors`). В този списък попадат само графичните устройства – първо отдалечените RPC сървъри, после дискретните видеокарти, а вградените – само ако не е намерена дискретна; CPU се обработва отделно и не влиза в него.
- `llama_model_load` – чете файла и стъпка по стъпка попълва модела: създава зареждач на GGUF, определя архитектурата и създава за нея обект на модела, избира устройствата, а след това последователно извиква `load_hparams`, `load_vocab` и `load_tensors`.

  **За какво е нужно:** цялата логика за четене на GGUF и попълване на модела е съсредоточена в една функция.
- При отрицателен статус в лога се записва причината (-1 – грешка при зареждане, -2 – отмяна чрез callback-а за прогреса) и се връща `nullptr`. Извикването на `llama_model_free` в този клон е само застраховка: `llama_model_load` сама унищожава недоизградения модел и при неуспех никога не връща указателя навън.

  **За какво е нужно:** по nullptr приложението разбира, че зареждането не е успяло, и не използва непълния модел.

Callback-ът за прогреса се извиква вътре в `load_tensors` при четенето на всеки тензор: на него се подава число от 0,0 до 1,0 (делът на заредените данни). Ако callback-ът върне `false`, зареждането се прекъсва и `llama_model_load` връща -2 (отмяна).

**За какво е нужно:**

- приложението може да отмени дългото зареждане или да показва лента за напредъка.

## Стъпки от 2 до 6 заедно: Поетапно зареждане на модела от файл

**Стъпки 2–6** се изпълняват вътре в една функция, `llama_model_load`: създава се зареждачът (стъпка 2), после се създава обектът на модела за архитектурата, която е прочел зареждачът (`llama_model_create`, стъпка 3), а след него последователно се извикват `load_hparams` (стъпка 4), `load_vocab` (стъпка 5) и `load_tensors` (стъпка 6).

**За какво е нужна функцията** `llama_model_load`: тя извършва поетапното зареждане на модела от файл: създава зареждач на GGUF (отваря файла и строи картата на тензорите), създава обект на модела с нужната архитектура, а след това последователно зарежда хиперпараметрите, речника и тензорите. Редът е важен: архитектурата задава набора от ключове в GGUF; хиперпараметрите се четат по тези ключове; речникът се зарежда с оглед на архитектурата; тензорите се създават по известните размери и се попълват от файла.

Съкратен цитат на функцията (файл `src/llama.cpp`, функция `llama_model_load`):

```cpp
// src/llama.cpp – llama_model_load(); празните редове и двете обвивки try/catch са махнати за краткост
// Връща 0 при успех, -1 при грешка и -2 при отмяна чрез llama_progress_callback
static std::pair<int, llama_model *> llama_model_load(struct gguf_context * metadata, llama_model_set_tensor_data_t set_tensor_data, void * set_tensor_data_ud,
        const std::string & fname, std::vector<std::string> & splits, FILE * file, llama_model_params & params) {
    try {
        // Стъпка 2: зареждачът отваря GGUF, чете заглавната част и метаданните, строи картата на тензорите (weights_map)
        llama_model_loader ml(metadata, set_tensor_data, set_tensor_data_ud, fname, splits, file, params.load_mode,
            params.check_tensors, params.no_alloc, params.load_mtp, params.kv_overrides, params.tensor_buft_overrides);
        ml.lazy.mode = params.lazy_mode;  // мързеливо четене на тензорите: част от теглата остава на диска и се чете при поискване
        ml.print_info();
        // Стъпка 3: архитектурата, прочетена от зареждача, избира C++ класа на модела
        std::unique_ptr<llama_model> model_ptr(llama_model_create(ml, params));
        // списък с устройствата (CPU, GPU), по които ще бъде разположен именно този модел
        bool ok = llama_prepare_model_devices(params, model_ptr.get());
        if (!ok) {
            return {-1, nullptr};
        }
        auto * model = dynamic_cast<llama_model_base *>(model_ptr.get());
        // ... пропуснато: проверка за null, която прекъсва работата, ако моделът не реализира llama_model_base
        model->t_load_us = 0;
        time_meas tm(model->t_load_us);  // времето за зареждане ще бъде преизчислено след първия eval, за да се отчетат пропуските на страници, отложени от mmap
        model->t_start_us = tm.t_start_us;
        model->hparams.vocab_only = params.vocab_only;
        model->hparams.no_alloc   = params.no_alloc;
        // ... пропуснато: всяко от двете извиквания по-долу е обвито в try/catch, който прехвърля грешката напред с префикс
        model->load_hparams(ml);  // Стъпка 4: размери на модела, дължина на контекста, брой слоеве
        if (model->arch == LLM_ARCH_CLIP) {
            throw std::runtime_error("CLIP cannot be used as main model, use it with --mmproj instead");
        }
        model->load_vocab(ml);    // Стъпка 5: речник и токенайзер – текст <-> токени
        model->load_stats(ml);
        model->print_info();
        if (params.vocab_only) {
            LLAMA_LOG_INFO("%s: vocab only - skipping tensors\n", __func__);
            return {0, model_ptr.release()};
        }
        // Стъпка 6: теглата на модела от файла в паметта (или mmap) на CPU/GPU
        if (!model->load_tensors(ml)) {
            return {-2, nullptr};
        }
        return {0, model_ptr.release()};
    } catch (const std::exception & err) {
        LLAMA_LOG_ERROR("%s: error loading model: %s\n", __func__, err.what());
        return {-1, nullptr};
    }
}
```

**Грешки и отмяна на зареждането:**

- при грешка на някоя от стъпките (създаване на зареждача, `llama_model_create`, `load_hparams`, `load_vocab`, `load_tensors`) се хвърля изключение; в блока catch се записва съобщение в лога и се връща -1;
- при отмяна чрез callback-а за прогреса (callback-ът връща false вътре в `load_tensors`) `load_tensors` връща false, изключение не се хвърля, но `llama_model_load` връща -2 (отмяна);
- при всеки неуспех недоизграденият модел се унищожава веднага: той живее в `unique_ptr`, който се предава на извикващия код само при успех, затова `llama_model_load_from_file_impl` получава нулев указател, записва причината в лога и връща `nullptr`;
- по този начин приложението може да отмени дългото зареждане чрез callback и коректно да освободи ресурсите.

**Стъпка по стъпка (какво се случва и за какво е нужно):**

- създава се зареждачът `llama_model_loader ml(...)` – той отваря GGUF файла, чете метаданните и строи `weights_map`.

  **За какво е нужно:**
- без зареждач не може да се прочетат архитектурата, хиперпараметрите, речникът и тензорите от файла.
- извиква се `ml.print_info()` – извежда в лога формата на файла, типа на файла (квантуването) и размера на файла заедно с броя битове на тегло.

  **За какво е нужно:**
- за да вижда потребителят кой точно файл се отваря.
- извиква се `llama_model_create(ml, params)` – архитектурата, прочетена от зареждача (LLaMA, Gemma, Qwen и т.н.), избира C++ класа на модела и обектът се създава.

  **За какво е нужно:**
- от архитектурата зависят имената на полетата в GGUF и кои тензори да се създават; неизвестна архитектура е грешка още тук.
- извиква се `llama_prepare_model_devices` – строи се и се извежда в лога списъкът с устройства (CPU и видеокарти) за този модел.

  **За какво е нужно:**
- той определя по кои устройства по-късно ще бъдат разположени слоевете в `load_tensors`.
- едва сега се нулира времето за зареждане и се пуска таймерът.

  **За какво е нужно:**
- измерената тук стойност не е окончателна: при първия eval `llama_context` преизчислява времето за зареждане от запазеното t_start_us, затова в отчета попадат и пропуските на страници, отложени от mmap – те се случват само когато до теглата реално се стигне.
- извиква се `model->load_hparams(ml)` – четем размерностите и параметрите (дължина на контекста, брой слоеве и т.н.).

  **За какво е нужно:**
- за да знаем „формата“ на модела и да заделим памет за него.
- за CLIP ще се хвърли грешка – той се използва отделно като проектор, а не като основен модел.
- извиква се `model->load_vocab(ml)` – зареждаме речника на токените.

  **За какво е нужно:**
- за да превръщаме по-късно текста в числа (токени) и обратно при генерирането.
- `load_stats(ml)` копира от зареждача в модела броя на елементите и размера в байтове, а `print_info()` извежда в лога архитектурата и всички хиперпараметри. Ако зареждаме само речника (`vocab_only == true`), това е краят. Иначе се извиква `model->load_tensors(ml)` – теглата на модела се четат и се разполагат в паметта. При успех се връща 0, при отмяна (callback-ът за прогреса върна false) – -2, при грешка – -1.

**Редът на извикванията при зареждане на модела (обобщение):**

- `llama_model_load_from_file` → `llama_model_load_from_file_impl`;
- в impl: проверка, че източникът на модела е зададен точно веднъж, проверка на бекенда, callback за прогреса по подразбиране и извикване на `llama_model_load(metadata, set_tensor_data, set_tensor_data_ud, path_model, splits, file, params)`;
- вътре в `llama_model_load`: създаване на `llama_model_loader`, `ml.print_info`, `llama_model_create`, `llama_prepare_model_devices`, `load_hparams`, `load_vocab`, `load_stats`, `print_info`, при нужда `load_tensors`. Всички тези стъпки се изпълняват последователно; при грешка в някоя от тях зареждането се прекъсва.

Редът на извикванията `llama_model_create` → `load_hparams` → `load_vocab` е важен: архитектурата задава набора от ключове в GGUF; хиперпараметрите се четат по тези ключове; речникът се зарежда с оглед на архитектурата (например имената на ключовете за токенайзера). Затова `vocab.load(ml, kv)` се извиква именно след `load_hparams(ml)` – към този момент и архитектурата, и хиперпараметрите вече са известни и зареждачът може коректно да прочете типа на токенайзера, списъците с токени и сливанията.

## Стъпка 2в: Зареждач на GGUF – отваряне на файла и карта на тензорите

**Стъпка 2 (завършване)** (вътре в `llama_model_load`) – създава се зареждачът на GGUF: отваря се файлът, четат се заглавната част и метаданните, строи се картата на тензорите.

**За какво е нужен зареждачът на GGUF:** за да отвори файла на модела, да прочете заглавната част и метаданните (без самите тегла) и да построи „карта“ – по името на тензора да се знае къде във файла са неговите данни и какъв е размерът му. Без тази карта по-късно не може да се зареждат теглата тензор по тензор. **Тензорите** тук са многомерни масиви от числа (матрици и вектори), в които се съхраняват теглата на невронната мрежа; всеки слой на модела е няколко тензора, а зареждачът трябва да знае за всеки името, размера и отместването във файла.

Създаването на зареждача е извикване на конструктора на `llama_model_loader`. Самият конструктор се намира в `src/llama-model-loader.cpp`; `llama_model_load` само го извиква. В съкратен вид изглежда така:

```cpp
// src/llama-model-loader.cpp - конструкторът (стъпка 2): отваря GGUF, чете заглавната част и метаданните, строи картата на тензорите
llama_model_loader::llama_model_loader(
        struct gguf_context * meta,
        llama_model_set_tensor_data_t set_tensor_data,
        void * set_tensor_data_ud,
        const std::string & fname,
        std::vector<std::string> & splits,
        FILE * file,
        llama_load_mode load_mode,
        bool check_tensors,
        bool no_alloc,
        bool load_mtp,
        const llama_model_kv_override * param_overrides_p,
        const llama_model_tensor_buft_override * param_tensor_buft_overrides_p)
        : metadata(meta), set_tensor_data(set_tensor_data), set_tensor_data_ud(set_tensor_data_ud) {
    // ... презаписване на KV и превръщане на load_mode в use_mmap / use_direct_io ...
    if (!fname.empty()) {
        struct ggml_context * ctx = NULL;
        struct gguf_init_params params = {
            /*.no_alloc = */ true,  // да се създадат само празни тензори, без да се чете блокът с данни
            /*.ctx      = */ &ctx,
        };
        metadata_ptr.reset(gguf_init_from_file(fname.c_str(), params));
        metadata = metadata_ptr.get();
        if (metadata == nullptr) {
            throw std::runtime_error(format("%s: failed to load model from %s", __func__, fname.c_str()));
        }
        // Името на архитектурата (llama, qwen2 ...) задава имената на всички останали ключове на GGUF
        get_key(llm_kv(LLM_KV_GENERAL_ARCHITECTURE), arch_name, false);
        llm_kv = LLM_KV(llm_arch_from_string(arch_name));
        files.emplace_back(new llama_file(fname.c_str(), "rb", use_direct_io));
        contexts.emplace_back(ctx);
        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, metadata, cur));
        }
        // ... същият цикъл по файловете-части, когато моделът е разбит на няколко ...
    }
    // ... клонове за вече отворен FILE * и за метаданни, подадени отвън ...
    n_kv      = gguf_get_n_kv(metadata);
    n_tensors = weights_map.size();
    fver = (enum llama_fver) gguf_get_version(metadata);
    // ... налучкване на типа на файла и извеждане на KV двойките в лога ...
}
```

В конструктора `gguf_init_from_file` чете заглавната част на GGUF и метаданните (без самите тегла) – така се получава списъкът с тензори и двойките ключ–стойност (архитектура, хиперпараметри), без да се четат тежките данни в паметта. От метаданните се взема името на архитектурата (`general.architecture`) – от него зависят имената на останалите полета в GGUF (при различните модели ключовете са различни). Файлът се отваря за четене; после по отместванията от `weights_map` ще се четат или проектират данните на тензорите. По списъка с тензори от GGUF за всеки в `weights_map` се записват името, размерът и отместването във файла – после `get_weight(name)` намира записа в картата, а `load_all_data` чете байтовете по това отместване (или, при mmap, веднага сочи към тях).

Структурата `llama_tensor_weight` (елемент на `weights_map`) съхранява три неща: индекса на изходния файл (моделът може да е разбит на няколко части GGUF), абсолютното отместване на данните на тензора в този файл и указател към тензора в контекста на GGUF. Отместването се изчислява веднъж, при създаването на записа, като началото на секцията с данни плюс отместването на тензора в нея – и същият конструктор проверява, че тензорът се побира изцяло във файла, така че отрязан модел се отсява тук, а не по време на генерирането. Методът `ml.get_key(...)` чете от метаданните на GGUF стойност по ключ (името на ключа зависи от архитектурата – връща го `kv(...)`). Така зареждачът получава например името на архитектурата, типа на токенайзера, хиперпараметрите.

След създаването на зареждача в `llama_model_load` се извиква `ml.print_info()`. Цитат от кода:

```cpp
// src/llama.cpp, вътре в llama_model_load, веднага след създаването на зареждача:
ml.print_info();  // пише в лога версията на GGUF, налучкания тип на файла и общия размер на теглата
```

Функцията `gguf_init_from_file` (библиотеката GGUF) отваря файла и чете заглавната част:

- версията на формата;
- броя на ключовете с метаданни;
- броя на тензорите.

Метаданните се четат в контекста `gguf_context` (двойки ключ–стойност); самите данни на тензорите на тази стъпка не се зареждат – само имената, типовете и отместванията във файла. Самият конструктор вече записва в лога броя на двойките ключ–стойност, броя на тензорите, името на файла и версията на GGUF. Методът `print_info` (`src/llama-model-loader.cpp`) добавя още три реда:

- версията на формата GGUF;
- типа на файла – Q4_K - Medium и подобни, налучкан по най-често срещания тип тензори, ако метаданните не го казват направо;
- общия размер на теглата в MiB или GiB заедно със средния брой битове на тегло.

## Формат GGUF (справочно)

GGUF (GPT-Generated Unified Format) е двоичен формат за съхранение на модели за машинно обучение.

**За какво е нужен:**

- за да се съхраняват в един файл и теглата на модела, и метаданните (размери, тип архитектура), и речникът;
- зареждачът по заглавната част и метаданните строи „карта“ и после при поискване чете нужните части на файла или ги проектира в паметта (mmap).

<div style="overflow-x:auto;margin:1.5rem 0" tabindex="0" role="region">
<svg viewBox="0 0 880 470" width="100%" style="min-width:620px;max-width:880px;height:auto;display:block;margin:0 auto" role="img" aria-label="Устройство на файла GGUF: хедър, метаданни, тензорни описания, данни" xmlns="http://www.w3.org/2000/svg">
  <defs>
    <marker id="lc-gguf-head" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
      <path d="M0 0 L10 5 L0 10 z" fill="var(--color-fg-dim)"/>
    </marker>
  </defs>
  <rect x="130" y="20" width="260" height="68" rx="8" fill="var(--color-bg-elevated)" stroke="var(--color-border)" stroke-width="1.5"/>
  <text x="260" y="59" text-anchor="middle" fill="var(--color-fg)" font-family="var(--font-sans)" font-size="14" font-weight="500">хедър</text>
  <text x="420" y="46" text-anchor="start" fill="var(--color-fg-dim)" font-family="var(--font-mono)" font-size="12.5" font-weight="400">magic · version · tensor_count · metadata_kv_count</text>
  <text x="420" y="66" text-anchor="start" fill="var(--color-fg-dim)" font-family="var(--font-sans)" font-size="12.5" font-weight="400">два брояча са 64-битови</text>
  <rect x="130" y="104" width="260" height="84" rx="8" fill="var(--color-bg-elevated)" stroke="var(--color-border)" stroke-width="1.5"/>
  <text x="260" y="151" text-anchor="middle" fill="var(--color-fg)" font-family="var(--font-sans)" font-size="14" font-weight="500">метаданни</text>
  <text x="420" y="130" text-anchor="start" fill="var(--color-fg-dim)" font-family="var(--font-sans)" font-size="12.5" font-weight="400">двойки „ключ – стойност“ по брояча от хедъра</text>
  <text x="420" y="150" text-anchor="start" fill="var(--color-fg-dim)" font-family="var(--font-sans)" font-size="12.5" font-weight="400">първо ключ-низ, после тип на стойността, после стойността</text>
  <rect x="130" y="204" width="260" height="84" rx="8" fill="var(--color-bg-elevated)" stroke="var(--color-border)" stroke-width="1.5"/>
  <text x="260" y="251" text-anchor="middle" fill="var(--color-fg)" font-family="var(--font-sans)" font-size="14" font-weight="500">тензорни описания</text>
  <text x="420" y="230" text-anchor="start" fill="var(--color-fg-dim)" font-family="var(--font-sans)" font-size="12.5" font-weight="400">име, размерности, тип елемент, отместване</text>
  <text x="420" y="250" text-anchor="start" fill="var(--color-fg-dim)" font-family="var(--font-sans)" font-size="12.5" font-weight="400">отместването е от началото на данните, не на файла</text>
  <rect x="130" y="304" width="260" height="40" rx="8" fill="var(--color-bg-elevated)" stroke="var(--color-border)" stroke-width="1.5" stroke-dasharray="4 4"/>
  <text x="260" y="329" text-anchor="middle" fill="var(--color-fg)" font-family="var(--font-sans)" font-size="14" font-weight="500">подравняване</text>
  <text x="420" y="330" text-anchor="start" fill="var(--color-fg-dim)" font-family="var(--font-sans)" font-size="12.5" font-weight="400">запълване до general.alignment</text>
  <rect x="130" y="360" width="260" height="84" rx="8" fill="var(--color-bg-elevated)" stroke="var(--color-accent)" stroke-width="1.5"/>
  <text x="260" y="407" text-anchor="middle" fill="var(--color-accent)" font-family="var(--font-sans)" font-size="14" font-weight="500">тензорни данни</text>
  <text x="420" y="386" text-anchor="start" fill="var(--color-fg-dim)" font-family="var(--font-sans)" font-size="12.5" font-weight="400">самите тегла; четат се или се отразяват в паметта</text>
  <text x="420" y="406" text-anchor="start" fill="var(--color-fg-dim)" font-family="var(--font-sans)" font-size="12.5" font-weight="400">квантуваните типове пазят елементи на блокове</text>
  <path d="M116 20 L110 20 L110 444 L116 444" fill="none" stroke="var(--color-border)" stroke-width="1.5"/>
  <text x="20" y="236" text-anchor="start" fill="var(--color-fg-dim)" font-family="var(--font-sans)" font-size="12" font-weight="400">един файл</text>
</svg>
</div>

**Структура на заглавната част на GGUF:**

- магическо число, четири байта **GGUF** (идентификация на формата – по него се разбира, че това е GGUF);
- версия на формата (за съвместимост при промени във формата);
- брой на тензорите (n_tensors);
- брой на ключовете с метаданни (n_kv).

**Метаданните** се съхраняват като масив от двойки „ключ – стойност“. За всяка двойка са записани:

- името на ключа (низ с дължина, записана пред него);
- типът на стойността (низ, число, булева, масив и т.н.);
- самата стойност (име на архитектурата, размерности, параметри на RoPE, тип на токенайзера, списъци с токени и т.н.); за масив първо идват типът на елементите и техният брой, после самите елементи.

**Тензорите във файла** следват след метаданните. За всеки тензор са записани:

- име;
- брой на размерностите и размер по всяка от тях (не повече от четири; например \[n_embd, n_ff\] за теглата на напълно свързания блок на един слой – всеки слой е отделен набор от тензори, затова номерът на слоя влиза в името, `blk.0.ffn_up.weight`, и никога не е размерност);
- тип на елемента (F32, F16, Q8_0, Q4_K и т.н. – от пълна точност до квантувани формати);
- отместване, от което започват данните – от началото на блока с данни на тензорите, който следва след метаданните.

Имена като Q4_K_M описват файла като цяло, а не отделен тензор: това е рецепта, която разпределя различните тензори в различни типове.

**Защо е нужно това на зареждача:** по тази информация той строи `weights_map` (картата „име на тензор → къде във файла са данните“), и когато теглата наистина са нужни, `load_all_data` чете или проектира съответния участък от файла.

**Версия на формата и типове на елементите:**

- версията на формата се задава в заглавната част на GGUF.

  **За какво е нужно:**
- при промени във формата версията казва на зареждача с какво има работа. Текущата версия е 3; файл с по-нова или вече неподдържана версия се отхвърля веднага и моделът не се зарежда. Неизвестните ключове с метаданни, напротив, не струват нищо – зареждачът пита само за ключовете, които познава, и на останалите не се препъва.
- по типа на елемента на тензора (F32, F16, Q8_0, Q4_K и т.н.) зареждачът знае колко байта заема блокът от елементи – квантуваните типове съхраняват елементите на блокове с фиксиран размер с общ мащаб, а не по един – и как да интерпретира данните при копиране в буфер или при mmap.

  **За какво е нужно:**
- без това не може коректно да се прочетат или проектират в паметта данните на тензора; различните типове имат различен размер и различна интерпретация на байтовете.

Типовете данни на тензорите в GGUF задават как да се интерпретират байтовете: F32, F16, Q8_0, Q4_K и т.н. – от пълна точност до квантувани формати. Квантуването намалява размера на модела и ускорява изчисленията за сметка на приблизително представяне на теглата. При зареждане двигателят създава тензорите в нужния формат и копира или проектира данните от файла в буфери на CPU или GPU.

Четенето на метаданните на GGUF се извършва чрез функциите на библиотеката gguf:

- `gguf_get_n_kv` – броят на ключовете;
- `gguf_get_key` – името на ключ по индекс;
- `gguf_get_kv_type` – типът на стойността (низ, число, масив и т.н.);
- `gguf_find_key` – индексът на ключ по неговото име или -1, ако такъв няма;
- `gguf_get_val_*` – стойността по този индекс, по една функция за всеки тип.

Зареждачът на модела обвива това в метода `get_key(key, value)` с отчитане на архитектурата: ключът се преобразува в име на поле в GGUF (например `llama.embedding_length` за LLaMA).

## Стъпка 3: Определяне на типа на модела (архитектура)

**Стъпка 3** – определяне на типа на модела (LLaMA, Gemma, Qwen и т.н.) по името на архитектурата от GGUF. Работата върши фабричната функция `llama_model_create` в `src/llama-model.cpp`. **За какво е нужно това:** от архитектурата зависят имената на ключовете, които се четат от метаданните на GGUF, наборът от тензори, създавани при зареждането на теглата, и построяването на изчислителния граф; без това не може коректно да се прочетат хиперпараметрите и речникът. В `llama.cpp` това не е просто флаг: всяка архитектура е отделен C++ клас, а фабриката избира кой от тях да създаде. Цитат (файл `src/llama-model.cpp`, функцията `llama_model_mapping` и двете претоварвания на `llama_model_create`):

```cpp
// src/llama-model.cpp — архитектурата от GGUF решава кой C++ клас представя този модел
static llama_model * llama_model_mapping(llm_arch arch, const llama_model_params & params) {
    switch (arch) {
        case LLM_ARCH_LLAMA:
            return new llama_model_llama(params);
        // ... по един case за всяка поддържана архитектура ...
        default:
            throw std::runtime_error(std::string("unsupported model architecture: '") + llm_arch_name(arch) + "'");
    }
}

llama_model * llama_model_create(llm_arch arch, const llama_model_params & params) {
    llama_model * model = llama_model_mapping(arch, params);

    if (model != nullptr) {
        model->arch = arch;
        // ... проверка, че поисканият режим на разбиване на тензорите е реализиран за тази архитектура ...
    }

    return model;
}

llama_model * llama_model_create(llama_model_loader & ml, const llama_model_params & params) {
    llm_arch arch = ml.get_arch();
    if (arch == LLM_ARCH_UNKNOWN) {
        throw std::runtime_error("unknown model architecture: '" + ml.get_arch_name() + "'");
    }

    return llama_model_create(arch, params);
}
```

**Какво се случва и за какво е нужно:**

- `ml.get_arch()` връща enum `llm_arch`, който зареждачът вече е определил по ключа `general.architecture` от метаданните на GGUF.

  **За какво е нужно:** по-нататък по този enum се избират имената на полетата за хиперпараметрите и речника (при различните архитектури ключовете в GGUF са различни).
- Ако типът е неизвестен (`LLM_ARCH_UNKNOWN`), хвърля се грешка.

  **За какво е нужно:** двигателят не може да работи с неизвестна архитектура; приложението ще получи съобщение за грешка и ще може да уведоми потребителя.

**Къде архитектурата се чете всъщност** (файл `src/llama-model-loader.cpp`): конструкторът на зареждача го прави веднъж, веднага след отварянето на GGUF и преди индексирането на тензорите, за да използва всичко останало в зареждача вече готовата стойност. `get_arch()` и `get_arch_name()` след това са прости гетъри:

```cpp
// src/llama-model-loader.cpp — конструкторът определя архитектурата веднъж, веднага след отварянето на GGUF
llama_model_loader::llama_model_loader(/* ... */) : metadata(meta), /* ... */ {
    // ... файлът GGUF току-що е отворен в `metadata` ...

    get_key(llm_kv(LLM_KV_GENERAL_ARCHITECTURE), arch_name, false);  // низът "general.architecture"
    llm_kv = LLM_KV(llm_arch_from_string(arch_name));                // "llama" -> LLM_ARCH_LLAMA, "qwen2" -> LLM_ARCH_QWEN2

    // ... по-нататък се строи индексът на тензорите (weights_map) ...
}

// към момента на заявката enum вече е определен – и двата гетъра са тривиални
std::string llama_model_loader::get_arch_name() const {
    return arch_name;
}

enum llm_arch llama_model_loader::get_arch() const {
    return llm_kv.arch;
}
```

**Какво правят тези извиквания:**

- `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` (`llama` → `LLM_ARCH_LLAMA`, `qwen2` → `LLM_ARCH_QWEN2`).

  **За какво е нужно:** по enum-а по-нататък се избират имената на полетата в GGUF за `load_hparams` и `load_vocab`, а също и класът на модела в `llama_model_create`. Четенето е незадължително (`false`), затова файл без ключ за архитектура дава `LLM_ARCH_UNKNOWN`, а грешката възниква там, където се създава моделът, а не тук.

- Примери за стойности на enum: `LLM_ARCH_LLAMA`, `LLM_ARCH_GEMMA`, `LLM_ARCH_QWEN`, `LLM_ARCH_PHI3`, `LLM_ARCH_MISTRAL3`, `LLM_ARCH_CLIP` и т.н. – в момента в таблицата има около 150 именувани архитектури. Самите идентификатори на ключовете на GGUF (`LLM_KV`) са един общ набор: тези, които се отнасят до хиперпараметрите, са шаблони от вида `%s.context_length`, където вместо **%s** се подставя името на архитектурата, затова един и същ ключ се чете като `llama.context_length` при един модел и като `qwen3.context_length` при друг; общите ключове, сред които `general.architecture` и `general.name`, са обикновени низове, в които няма какво да се подставя.

  **За какво е нужно:** без правилна архитектура имената на ключовете излизат грешни и хиперпараметрите с речника от GGUF не могат да се прочетат. Наистина свои за архитектурата остават само няколко допълнителни хиперпараметъра – чете ги нейният собствен `load_arch_hparams` в `src/models/`.
- Имената на тензорите в GGUF зависят от архитектурата: за LLaMA – blk.N.attn_q.weight, blk.N.attn_k.weight и т.н.; за други модели – свои префикси и суфикси. Да се добави поддръжка на нова архитектура означава да се добави стойност в enum-а и името ѝ в таблицата с имена, да се напише нов клас в `src/models/`, който казва какви хиперпараметри да се четат, какви тензори да се създават и как да се строи графът, и да се регистрира във фабриката; нови ключове на GGUF се добавят само когато на архитектурата е нужен параметър, който го няма в нито един от съществуващите ключове. Хиперпараметрите се използват при създаването на тензорите в `load_tensors` (размери на матриците, брой слоеве) и при създаването на контекста (`n_ctx`, `n_batch`, параметри на RoPE и т.н.).

## Стъпка 4: Зареждане на хиперпараметрите – размери и контекст

**Стъпка 4** – зареждане на хиперпараметрите (размери на модела, дължина на контекста, брой слоеве и т.н.) от метаданните на GGUF. Методът `llama_model_base::load_hparams` попълва структурата `hparams` (хиперпараметри) от метаданните на GGUF. **За какво е нужно това:** хиперпараметрите задават „формата“ на модела – дължина на контекста, брой слоеве, размер на ембединга, брой глави на вниманието и т.н.; без тях не може да се задели памет за тензорите и да се построи изчислителният граф. Тук се четат ключовете, общи за всички архитектури; след това методът извиква `load_arch_hparams` – хук, който всяка архитектура реализира в свой файл в `src/models/` и който чете параметрите, характерни само за това семейство. След връщането от хука се задават още няколко полета, сред които типът на RoPE – той може да се изчисли едва когато архитектурата е определила своите параметри. Цитат с основните ключове (файл `src/llama-model.cpp`, методът `llama_model_base::load_hparams`):

```cpp
// src/llama-model.cpp — llama_model_base::load_hparams(): „формата“ на модела, прочетена от метаданните на GGUF
void llama_model_base::load_hparams(llama_model_loader & ml) {
    const gguf_context * ctx = ml.metadata;

    // запазваме всички двойки ключ-стойност (освен масивите) като низове, за справка
    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;
    }

    // архитектурата е префикс на всеки ключ по-долу: llama.context_length, qwen3.context_length, ...
    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_BLOCK_COUNT,      hparams.n_layer_all);   // брой слоеве на трансформъра
    GGML_ASSERT(hparams.n_layer_all > 0 && hparams.n_layer_all <= LLAMA_MAX_LAYERS);
    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);  // брой глави на вниманието по слоеве

    // n_head_kv е незадължителен, по подразбиране е равен на n_head
    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)

    // rope_freq_base (незадължителен)
    hparams.rope_freq_base_train = 10000.0f;
    ml.get_key(LLM_KV_ROPE_FREQ_BASE, hparams.rope_freq_base_train, false);

    // ... мащабиране на RoPE, размери на главите, варианти с плъзгащ се прозорец ...

    load_arch_hparams(ml);  // всичко, което е нужно само на тази архитектура — src/models/<arch>.cpp

    // ... след хука се задават още няколко полета, сред които hparams.rope_type ...
}
```

**Какво се случва и за какво е нужно:**

- В цикъл се четат всички двойки ключ–стойност от GGUF (освен масивите) и се запазват в `gguf_kv`.

  **За какво е нужно:**
- за да може по-късно при нужда да се стигне до всяко поле по име.
- След това по ключовете, зависещи от архитектурата, се попълват полетата на `hparams`:
- `n_ctx_train` – дължината на контекста при обучението.

  **За какво е нужно:**
- от нея зависят размерът на KV-cache и максималната дължина на входа.
- `n_embd` – размерът на ембединга (скрития слой).

  **За какво е нужно:**
- от него зависят размерите на матриците с тегла.
- `n_layer_all` – броят на блоковете на трансформъра, записан във файла.

  **За какво е нужно:**
- от него зависи колко тензора да се създадат в `load_tensors`; реално се строи `hparams.n_layer()` – то изважда допълнителните блокове за multi-token prediction, които имат някои модели.
- `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` архитектурата се определя при създаването на обекта на модела (`llama_model_create` я иска от зареждача през `ml.get_arch()`), след това се извиква `load_hparams(ml)` и едва тогава `load_vocab(ml)`. Вътре в `load_vocab` се извиква `vocab.load(ml, kv)`. Той се зарежда в `llama_model_base::load_vocab` (`src/llama-model.cpp`):

```cpp
void llama_model_base::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 и т.н.);
- списъците с токени;
- типовете на токените;
- сливанията (merges) на BPE (ако има такива);
- специалните токени – всичко, което е нужно, за да се превръща текстът в последователност от числа (токени) и обратно.

След това моделът може да токенизира промпта и да превежда генерираните токени обратно в текст.

Извикването `vocab.load(ml, kv)` се изпълнява вътре в `llama_model_base::load_vocab`, след като за текущата архитектура е получен наборът от ключове `kv` (чрез `LLM_KV(arch)`). Зареждачът `ml` вече е отворил GGUF и е прочел метаданните; архитектурата е известна от зареждача (`ml.get_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.metadata;

    // Определяме типа на речника
    {
        ml.get_key(LLM_KV_TOKENIZER_MODEL, tokenizer_model);
        ml.get_key(LLM_KV_TOKENIZER_PRE,   tokenizer_pre, false);
        // ... "no_vocab" / "none" -> LLAMA_VOCAB_TYPE_NONE и ранен изход ...
        if (tokenizer_model == "llama") {
            type = LLAMA_VOCAB_TYPE_SPM;
            // Специални токени по подразбиране
            special_bos_id  = 1;
            special_eos_id  = 2;
            special_unk_id  = 0;
        } else if (tokenizer_model == "gpt2" || tokenizer_model == "hybriddna" || tokenizer_model == "whitespace") {
            type = LLAMA_VOCAB_TYPE_BPE;
            // Четем сливанията на BPE и попълваме ранговете на BPE
            const int merges_keyidx = gguf_find_key(ctx, kv(LLM_KV_TOKENIZER_MERGES).c_str());
            // ... масив със сливания от грешен тип е грешка; липсващ също е грешка, освен
            //     когато tokenizer_pre == "kimi-k2"; елемент "first second" -> bpe_ranks[{first, second}] = i ...
        }
        // ... "bert" -> WPM, "t5" -> UGM, "rwkv", "plamo2", "gemma4"; всичко останало е грешка ...
    }
    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");
    }
    // ... тук са масивите с оценки и типове на токените и се проверява, че стигат за всички токени ...
    const 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);
        // ... празен низ се заменя със заместител "[EMPTY_<i>]" ...
        token_to_id[word] = i;
        max_token_len = std::max(max_token_len, (int) word.size());
        auto & token_data = id_to_token[i];
        token_data.text = std::move(word);
        token_data.attr = LLAMA_TOKEN_ATTR_NORMAL;
        // ... score и attr после се вземат от масивите на GGUF, ако ги има във файла ...
    }
    init_tokenizer(type);
    // ... след това следват токенът за нов ред, ID на специалните токени и флаговете add_bos / add_eos ...
}
```

В кода се вижда: определяне на типа на речника по `tokenizer_model` (llama → SPM, gpt2 → BPE с четене на сливанията), търсене на масива с токени в GGUF, цикъл по всички токени – попълване на `id_to_token` и `token_to_id`, извикване на `init_tokenizer(type)` за инициализиране на токенизатора (SPM, BPE и т.н.). Всичко, което е скрито зад редовете `// ...`, са проверки: масивът със сливания и масивът с токени трябва да присъстват и да са с правилния тип на GGUF, а масивите с оценки и типове на токените трябва да покриват всички токени – иначе зареждането се прекъсва с изключение. Единственото облекчение е за пре-токенизатора `kimi-k2`: той токенизира без обичайните сливания на 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_base::load_tensors` (`src/llama-model.cpp`) създава тензорите на модела, определя им буфери – участъци памет на CPU или на видеокартата – и ги попълва с данни от GGUF файла. По-долу е началото на метода, съкратено.

```cpp
// src/llama-model.cpp — llama_model_base::load_tensors()
bool llama_model_base::load_tensors(llama_model_loader & ml) {
    const auto & split_mode   = params.split_mode;
    const bool use_mlock      = params.load_mode == LLAMA_LOAD_MODE_MLOCK || params.load_mode == LLAMA_LOAD_MODE_MMAP_MLOCK;
    const auto & tensor_split = params.tensor_split;
    const int n_layer_all  = hparams.n_layer_all;
    const int n_gpu_layers = this->n_gpu_layers();   // отрицателна стойност в параметрите означава „всички слоеве“
    // ... при LLAMA_LOAD_MODE_AUTO ml.use_mmap се изключва, ако някое устройство не поддържа mmap
    // строим списъци с типове буфери за CPU и за устройствата GPU
    pimpl->cpu_buft_list = make_cpu_buft_list(devices, params.use_extra_bufts, params.no_host);
    for (const auto & dev : devices) {
        buft_list_t buft_list = make_gpu_buft_list(dev.dev, split_mode, tensor_split);
        // добавяме типовете буфери на CPU като резервен вариант
        buft_list.insert(buft_list.end(), pimpl->cpu_buft_list.begin(), pimpl->cpu_buft_list.end());
        pimpl->gpu_buft_list.emplace(dev.dev, std::move(buft_list));
    }

    ggml_backend_dev_t cpu_dev = ggml_backend_dev_by_type(GGML_BACKEND_DEVICE_TYPE_CPU);
    // ... splits[] се попълва от tensor_split или по свободната памет на всяко устройство и се нормира

    const int i_gpu_start = std::max(n_layer_all + 1 - n_gpu_layers, 0);
    const int act_gpu_layers = devices.empty() ? 0 : std::min(n_gpu_layers, n_layer_all + 1);
    auto get_layer_buft_list = [&](int il) -> llama_model::impl::layer_dev {
        // ... редът LLAMA_LOG_DEBUG за всеки слой е пропуснат
        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).dev;
        return {dev, &pimpl->gpu_buft_list.at(dev)};
    };

    // от прехвърлянето на входния слой към GPU почти няма полза, затова той винаги остава на CPU
    pimpl->dev_input = { cpu_dev, &pimpl->cpu_buft_list };

    // разпределяме повтарящите се слоеве по устройствата според splits
    pimpl->dev_layer.resize(n_layer_all);
    for (int il = 0; il < n_layer_all; ++il) {
        pimpl->dev_layer[il] = get_layer_buft_list(il);
    }
    // определяме изходния слой
    pimpl->dev_output = get_layer_buft_list(n_layer_all);
```

**Какво се случва в началото на `load_tensors` и за какво е нужно:**

- формират се списъци с типове буфери за CPU и за всяка видеокарта

  **За какво е нужно:** от тях зависи на кои устройства ще бъдат разположени тензорите на модела – входните обикновено на CPU, слоевете според разпределението между CPU/GPU;
- по настройките (`tensor_split`) или по свободната памет се решава кои слоеве да се изчисляват на CPU и кои на GPU

  **За какво е нужно:** за да се натоварят равномерно устройствата и да не се препълни паметта на една видеокарта.

Обърнете внимание: mmap и mlock вече не са отделни булеви параметри – в `llama_model_params` има едно поле `load_mode` (`auto`, `none`, `mmap`, `mlock`, `mmap+mlock`, `dio`), а от командния ред то се задава с ключа `-lm / --load-mode`; предишните ключове `--mmap`, `--no-mmap` и `--mlock` се свеждат до него.

Функцията `get_layer_buft_list(il)` за номера на слоя връща устройство и списък с буфери: входът винаги е на CPU, повтарящите се слоеве може да са на CPU или на GPU, изходът – според разпределението. След това в цикъл се създават тензорите: ембедингите, изходът, за всеки слой – матриците на вниманието (Q/K/V/O), нормализациите, feed-forward. Създаването на тензор на този етап само фиксира формата му и избира буфера, в който ще живее – байтовете още не се местят. Данните идват при втория проход, когато всички тензори вече са обявени, а буферите на бекендите са заделени: `load_tensors` извиква `ml.load_all_data(...)` по веднъж за всеки буферен контекст и вече вътре в това извикване се избира mmap или обикновено четене. В резултат теглата на модела се оказват в паметта (и при нужда на видеокартата), а моделът е готов за генериране на текст.

Самият зареждач (`src/llama-model-loader.cpp`) разделя това на две половини. `create_tensor` обявява един тензор: сверява формата с метаданните на GGUF, избира типа на буфера според разпределението на устройствата, изчислено по-горе, и създава тензора в съответния ggml-контекст. След това `load_all_data` обхожда всички тензори на контекста, по име намира записа им в `weights_map` – оттам се вземат индексът на файла и отместването в байтове – и или насочва тензора към проектирането на файла в паметта, или чете байтовете в неговия буфер. За големите модели се използва mmap, за да не се дублират данните в оперативната памет.

**Проходът на обявяване на тензорите, за семейството llama – методът `load_arch_tensors` във файла `src/models/llama.cpp`:**

```cpp
// src/models/llama.cpp — llama_model_llama::load_arch_tensors(), извиква се от load_tensors()
void llama_model_llama::load_arch_tensors(llama_model_loader &) {
    LLAMA_LOAD_LOCALS;   // въвежда в областта на видимост n_embd, n_layer, n_ff, n_vocab и останалите

    tok_embd = create_tensor(tn(LLM_TENSOR_TOKEN_EMBD, "weight"), {n_embd, n_vocab}, 0);

    // изход
    output_norm = create_tensor(tn(LLM_TENSOR_OUTPUT_NORM, "weight"), {n_embd}, 0);
    output      = create_tensor(tn(LLM_TENSOR_OUTPUT,      "weight"), {n_embd, n_vocab}, TENSOR_NOT_REQUIRED);

    // ако output е празен, инициализираме го от входните ембединги на токените
    if (output == NULL) {
        output = create_tensor(tn(LLM_TENSOR_TOKEN_EMBD, "weight"), {n_embd, n_vocab}, TENSOR_DUPLICATED);
    }

    for (int i = 0; i < n_layer; ++i) {
        auto & layer = layers[i];

        layer.attn_norm = create_tensor(tn(LLM_TENSOR_ATTN_NORM, "weight", i), {n_embd}, 0);

        create_tensor_qkv(layer, i, n_embd, n_embd_head_k * n_head, n_embd_k_gqa, n_embd_v_gqa, 0);
        layer.wo = create_tensor(tn(LLM_TENSOR_ATTN_OUT, "weight", i), {n_embd_head_k * n_head, n_embd}, 0);

        // ... незадължителните тензори на отместванията и тензорите на коефициентите на RoPE са пропуснати

        layer.ffn_norm = create_tensor(tn(LLM_TENSOR_FFN_NORM, "weight", i), {n_embd}, 0);

        if (n_expert == 0) {
            layer.ffn_gate = create_tensor(tn(LLM_TENSOR_FFN_GATE, "weight", i), {n_embd,   n_ff}, 0);
            layer.ffn_down = create_tensor(tn(LLM_TENSOR_FFN_DOWN, "weight", i), {  n_ff, n_embd}, 0);
            layer.ffn_up   = create_tensor(tn(LLM_TENSOR_FFN_UP,   "weight", i), {n_embd,   n_ff}, 0);
            // ... незадължителните тензори на отместванията на MLP са пропуснати
        }
        // ... клонът mixture-of-experts е пропуснат
    }
}
```

**Какво се случва тук и за какво е нужно:**

- всяко тегло на модела се именува чрез `tn(...)`, който строи името на тензора в GGUF – `token_embd.weight`, `blk.0.attn_norm.weight`, `blk.0.ffn_down.weight` и така нататък.

  **За какво е нужно:** името е ключът, по който зареждачът после ще намери отместването на този тензор във файла.
- `create_tensor` сверява обявената форма с метаданните, избира типа на буфера според изчисленото по-рано разпределение на слоевете и създава тензора в ggml-контекста на този тип буфер.

  **За какво е нужно:** именно тук се решава дали слоят живее на CPU или на конкретна видеокарта – още преди да са докоснати данните.
- тензорите с флаг `TENSOR_NOT_REQUIRED` могат напълно законно да липсват, а `TENSOR_DUPLICATED` преизползва вече създаден тензор – така моделът без отделна изходна матрица свързва изхода си с ембедингите на токените.

  **За какво е нужно:** една процедура на зареждане трябва да покрива много варианти на една и съща архитектура.

Самият проход с данните е цикъл по тензорите на един буферен контекст вътре в `ml.load_all_data` (`src/llama-model-loader.cpp`). За всеки тензор записът му се търси в `weights_map` по името в GGUF – `blk.0.attn_q.weight`, `blk.0.attn_k.weight` и така нататък – и оттам се вземат индексът на файла и отместването в байтове. След това са възможни два варианта. При проектиране на файла в паметта тензорът се насочва направо към проектираните страници, ако над тях може да се построи буфер – това е случаят без копиране и точно така става за паметта на хоста; иначе байтовете се копират от проекцията в буфера на тензора. Без проектиране байтовете се четат от файла в този буфер, а ако буферът е на видеокарта, байтовете се изпращат към устройството. След този проход теглата на модела са изцяло в паметта (и при нужда на GPU).

```cpp
// src/llama-model-loader.cpp — цикъл по тензорите в llama_model_loader::load_all_data()
for (struct ggml_tensor * cur = ggml_get_first_tensor(ctx); cur != NULL; cur = ggml_get_next_tensor(ctx, cur)) {
    const auto * weight = get_weight(ggml_get_name(cur));
    if (weight == nullptr) {
        // такова нещо се случва при модели с разделени експерти
        continue;
    }
    // ... тук се извиква progress_callback; връщане на false прекъсва зареждането
    size_t n_size = ggml_nbytes(cur);
    const bool from_mapping = use_mmap || lazy.has(cur);

    if (from_mapping) {
        const auto & mapping = mappings.at(weight->idx);
        ggml_backend_buffer_t buf_mmap = nullptr;
        if (bufs.count(weight->idx)) {
            buf_mmap = bufs.at(weight->idx);
        }
        uint8_t * data = (uint8_t *) mapping->addr() + weight->offs;
        // ... незадължителната проверка check_tensors е пропусната
        GGML_ASSERT(buf_mmap || cur->data); // или има буфер за тензора, или той вече е разположен
        if (buf_mmap && cur->data == nullptr) {
            // изобщо няма копиране: тензорът се насочва направо към проектираните страници
            ggml_backend_tensor_alloc(buf_mmap, cur, data);
            // ... разширяването на mlock и отчитането на използвания диапазон са пропуснати
        } else {
            ggml_backend_tensor_set(cur, data, 0, n_size);
        }
    } else {
        const auto & file = files.at(weight->idx);
        if (ggml_backend_buffer_is_host(cur->buffer)) {
            file->seek(weight->offs, SEEK_SET);
            file->read_raw(cur->data, n_size);
        } else {
            // ... в буфер на GPU: асинхронно изпращане на парчета през закрепена памет на хоста,
            // ... ако устройството го може, иначе четене в read_buf + ggml_backend_tensor_set()
        }
    }
    size_done += n_size;
}
```

Така двата прохода заедно покриват всичко: `create_tensor` решава къде ще живее всяко тегло – ембединги, матрици Q/K/V/O, нормализации и feed-forward на всеки слой, – а `load_all_data` слага там байтовете или подменя проектирането на файла.

При използване на mmap данните на тензорите не се копират в оперативната памет – проектира се участък от файла и тензорът сочи направо към него, – но само там, където устройството може да построи буфер върху паметта на хоста. Процесорът винаги може; при единна памет, като Apple Silicon, може и графичното устройство. Дискретната карта не може: нейните байтове се копират от проекцията във видеопаметта и там mmap спестява RAM само за слоевете, останали на процесора. Това намалява потреблението на RAM и ускорява началото на зареждането, но файлът трябва да остане на диска на същото място и непроменен през цялото време, докато моделът е зареден: проекцията живее по-дълго от зареждача и се освобождава едва при унищожаването на модела. Режимът на директен вход-изход (**--load-mode dio**) отваря файла без буфериране и закръглява всяко четене до размера на блока, съобщен от файловата система; той е реализиран само на Linux, а ако файлът не може да се отвори без буфериране – става връщане към обикновеното буферирано четене. Разпределението по слоеве (`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 паметта за графа не се заделя „в движение“ – буферите вече са резервирани за най-лошия случай и това намалява забавянето на първия отговор (самият граф при първия decode въпреки това се строи отново).

Началото на конструктора `llama_context::llama_context` в `src/llama-context.cpp` (съкратено – пропуските са отбелязани):

```cpp
// src/llama-context.cpp — llama_context::llama_context (съкратено)
llama_context::llama_context(
        const llama_model & model,
              llama_context_params params) :
    model(model),
    cvec(std::make_unique<llama_adapter_cvec>()),
    loras(std::make_unique<llama_adapter_loras>()),
    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));
    }

    // ... n_rs_seq и нулирането му за архитектури без връщане назад на recurrent-състоянието
    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;
    // ... останалите параметри на YaRN, на пулинга и на Flash Attention
    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;
    // ...
    if (params.attention_type == LLAMA_ATTENTION_TYPE_UNSPECIFIED) {
        cparams.causal_attn = hparams.causal_attn;
    } else {
        cparams.causal_attn = params.attention_type == LLAMA_ATTENTION_TYPE_CAUSAL;
    }

    // при causal attention размерът на батча е ограничен от размера на контекста
    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);
    // ... n_ctx се допълва до кратно на 256 и, ако KV-cache не е общ за всички
    //     последователности, се дели между тях по лимита n_ctx_seq
    LLAMA_LOG_INFO("%s: n_ctx                 = %u\n",   __func__, cparams.n_ctx);
    LLAMA_LOG_INFO("%s: n_ctx_seq             = %u\n",   __func__, cparams.n_ctx_seq);
    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) и буферът за изходите, после модулът за памет
    //     (KV-cache), а накрая sched_reserve() строи планировчика и графовете
}
```

**Какво се задава в конструктора на контекста:**

- запазват се референцията към модела и времето на зареждане;
- задават се максималният брой последователности (`n_seq_max`), броят нишки, параметрите на YaRN/RoPE, размерът на контекста (`n_ctx`), размерът на батча (`n_batch`, `n_ubatch`), типът на вниманието (causal).

  **За какво е нужно:**
- от тези параметри зависят размерът на KV-cache, максималната дължина на промпта и това как батчът се обработва на големи порции.

Заявеният `n_ctx` се закръгля нагоре до кратно на 256 и, ако KV-cache не е общ за всички последователности, се дели между тях: реалното ограничение за един диалог се оказва лимитът за една последователност `n_ctx_seq` – именно него логът отпечатва до `n_ctx`.

След това се инициализират бекендите (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`), но не направо в него. Първо конструкторът събира собствен списък с бекенди – по един за всяко устройство на модела, плюс бекендите на ускорителите и CPU, – след това създава обекта за памет през `model.create_memory` и накрая извиква помощната функция `sched_reserve`. Именно в нея `ggml_backend_sched_new` строи планировчика по този списък с бекенди и се резервират графовете, така че при първия реален decode графът се строи наново, но се разполага във вече резервирана памет. Самият KV-cache не живее в `src/llama-memory.cpp` – в този файл има само две малки помощни функции за статуса на паметта. Буферите се заделят в конструктора на `llama_kv_cache` във файла `src/llama-kv-cache.cpp`: за всеки слой той създава тензор за ключовете и тензор за стойностите с размер за целия контекст, а след това иска от бекенда по един буфер за всяко устройство.

Функцията `graph_reserve` (в `src/llama-context.cpp`) резервира по един граф на извикване: тя прави фиктивен батч с нужната форма, извиква за него `model.build_graph` и предава резултата на `ggml_backend_sched_reserve` – планировчикът разбива графа по бекендите и резервира буферите. `sched_reserve` я извиква три пъти: първо за Prefill (цял подбатч токени наведнъж), след това за Decode (по един токен за последователност), после отново за Prefill – за да останат буферите с размер за най-лошия случай и да не се заделят наново по време на инференса. По-късно, при истинското извикване на `process_ubatch`, предишният граф се преизползва, ако параметрите му позволяват (`can_reuse`), иначе се строи наново и се разполага чрез `ggml_backend_sched_alloc_graph`; във всеки случай заделянето за големите графове вече е направено при създаването на контекста – именно това премахва скока на забавянето при първия 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 вече са в кеша.

Целият цикъл се побира в няколко десетки реда. По-долу е цикълът на генериране от минималния пример `examples/simple-chat/simple-chat.cpp` (функцията **generate**), съкратено, с добавени в коментарите номера на стъпките от тази статия.

```cpp
// Цикъл на генериране, съкратено от examples/simple-chat/simple-chat.cpp (ламбдата "generate").
// Номерата на стъпките са от тази статия, останалото е кодът на самия пример.
// BOS се добавя само за най-първия промпт в диалога
const bool is_first = llama_memory_seq_pos_max(llama_get_memory(ctx), 0) == -1;
// Стъпка 9: текст на промпта -> масив от токени. При извикване с tokens == NULL
// llama_tokenize връща минус броя на токените – така се разбира размерът на буфера.
const int n_prompt_tokens = -llama_tokenize(vocab, prompt.c_str(), prompt.size(), NULL, 0, is_first, true);
std::vector<llama_token> prompt_tokens(n_prompt_tokens);
if (llama_tokenize(vocab, prompt.c_str(), prompt.size(), prompt_tokens.data(), prompt_tokens.size(), is_first, true) < 0) {
    GGML_ABORT("failed to tokenize the prompt\n");
}
// Стъпка 10: токените се опаковат в батч – една последователност, позициите ги води llama_decode
llama_batch batch = llama_batch_get_one(prompt_tokens.data(), prompt_tokens.size());
llama_token new_token_id;
while (true) {
    // ... тук примерът още проверява дали батчът се събира в контекста
    // Стъпки 11–14: decode, подбатчове, process_ubatch; логитите попадат в буфера на контекста
    int ret = llama_decode(ctx, batch);
    if (ret != 0) {
        GGML_ABORT("failed to decode, ret = %d\n", ret);
    }
    // Стъпка 15: семплиране; -1 означава „логитите на последната позиция“
    new_token_id = llama_sampler_sample(smpl, ctx, -1);
    // това край на генерирането ли е?
    if (llama_vocab_is_eog(vocab, new_token_id)) {
        break;
    }
    // Стъпка 16: токен -> текст; отрицателно n означава, че буферът се е оказал малък
    char buf[256];
    int n = llama_token_to_piece(vocab, new_token_id, buf, sizeof(buf), 0, true);
    if (n < 0) {
        GGML_ABORT("failed to convert token to piece\n");
    }
    std::string piece(buf, n);
    printf("%s", piece.c_str());
    fflush(stdout);
    // следващият батч е един засемплиран токен, нататък – проход Decode
    batch = llama_batch_get_one(&new_token_id, 1);
}
```

## Стъпка 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 и EOS, и то само ако самият модел е настроен за тях;
- `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, токенизатор в стила на BERT;
- `LLAMA_VOCAB_TYPE_UGM` – Unigram, токенизатор в стила на T5;
- `LLAMA_VOCAB_TYPE_RWKV` – лакома токенизация по префиксно дърво (trie);
- `LLAMA_VOCAB_TYPE_PLAMO2` – алгоритъм на Ахо – Корасик с динамично програмиране;
- `LLAMA_VOCAB_TYPE_NONE` – моделът е публикуван изобщо без речник; няма с какво да се токенизира и опитът за токенизация просто завършва аварийно. Функцията `tokenizer_st_partition` разбива входния текст на фрагменти: обикновен текст и специални токени – специалният токен става един токен на изхода, а останалото отива в токенизатора по типа на речника. Флагът `parse_special` управлява само управляващите и служебните токени; токените, обявени в модела като потребителски, се обособяват винаги. Фрагментът `llama_vocab::impl::tokenize` е общата част и клонът за BPE; останалите клонове са устроени по същия начин:

```cpp
// Стъпка 9: текстът се разбива на фрагменти (обикновен текст и специални токени),
// и всеки фрагмент се токенизира според типа на речника
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. Call llama_vocab::init_tokenizer() first.");
    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()) {
        // ... клоновете SPM, WPM, UGM, RWKV и PLAMO2 са устроени по същия начин
        case LLAMA_VOCAB_TYPE_BPE:
            {
                const llm_tokenizer_bpe * tok_bpe = static_cast<const llm_tokenizer_bpe *>(tokenizer.get());
                std::unique_ptr<llm_tokenizer_bpe_session> session;
                // ... за моделите на токенизатора "hybriddna" и "whitespace" се взема собствен подклас на сесията
                session = std::make_unique<llm_tokenizer_bpe_session>(vocab, *tok_bpe);
                if (add_special) {
                    session->append_bos(output);
                }
                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 { // FRAGMENT_BUFFER_VARIANT_TYPE_TOKEN
                        session->append(fragment.token, output);
                    }
                }
                if (add_special) {
                    session->append_eos(output);
                    session->check_double_bos_eos(output);
                }
            } break;
        case LLAMA_VOCAB_TYPE_NONE:
            GGML_ABORT("fatal error");
    }
    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_batch_get_one`: той обвива един току-що избран токен и оставя равни на NULL и `pos`, и `seq_id`, за да продължи `llama_decode` сам позицията и да отнесе токена към последователност 0 – така правят `examples/simple` и `examples/simple-chat`. Вторият е веднъж да се задели батч чрез `llama_batch_init`, а преди всяко извикване да се изчиства чрез `common_batch_clear` и да се добавят токени чрез `common_batch_add`, като изрично се подават позицията и идентификаторите на последователностите – точно така трябва да прави всеки код, който обслужва няколко последователности едновременно.

Методът `init` на класа `llama_batch_allocr` (файл `src/llama-batch.cpp`), който `llama_context::decode` извиква за входящия батч преди всичко останало:

```cpp
// src/llama-batch.cpp, llama_batch_allocr::init(): проверява входния батч, после попълва
// всички масиви, които приложението е оставило NULL – преди всичко позициите и флаговете за изход
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);
    // ... пропуснато: n_seq_max трябва да се събира в LLAMA_MAX_SEQ, всеки seq_id трябва да е по-малък от n_seq_max,
    //     и всеки batch.token[i] трябва да е допустим id (< vocab.n_tokens()) – иначе return false ...
    // ... пропуснато: ако n_seq_id или seq_id са NULL, всеки токен принадлежи към последователност 0 ...
    if (!batch.pos) {
        pos.resize(batch.n_tokens);

        // началната позиция на всяка последователност се взема от позициите в паметта
        llama_pos p0[LLAMA_MAX_SEQ];
        for (uint32_t s = 0; s < n_seq_max; ++s) {
            if (!memory) {
                // ако няма памет -> започваме от 0
                p0[s] = 0;
            } else {
                p0[s] = memory->seq_pos_max(s) + 1;
            }
        }

        for (int32_t i = 0; i < batch.n_tokens; i++) {
            const llama_seq_id seq_id = batch.seq_id[i][0];
            pos[i] = p0[seq_id];
            // ... пропуснато: p0 се измества след тази позиция за всяка последователност на токена ...
        }
        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();
    }
    // ... пропуснато: ако logits са зададени, но е зададен output_all, липсващите изходи се включват принудително;
    //     после статистика на позициите по последователности, свързани последователности и проверки за съгласуваност ...
    return true;
}
```

**В кода:**

- проверка на токените за допустимост;
- при липса на `n_seq_id` и `seq_id` всеки токен принадлежи към точно една последователност с номер 0;
- при липса на `pos` всяка последователност продължава от мястото, на което е спряла в паметта (`memory->seq_pos_max(s) + 1`), и позициите се проставят по ред; ако изобщо няма памет, броенето започва от 0;
- при липса на `logits` логитите се искат само за последния токен (или за всички, ако е зададен `output_all`).

## От токените към ембедингите (случва се вътре в стъпка 13)

**Вътре в стъпка 13** токените на батча се превръщат в ембединги. В батча може да се подадат или ID на токени (**token**), или готови ембединги (**embd**), а в типичния случай приложението подава токени. Превръщането не е отделен проход преди обработката на батча: то е най-първият възел на изчислителния граф, който decode строи за всеки ubatch.

**За какво е нужно това превръщане:**

- моделът (слоевете на трансформъра) работи не с номера на токени, а с вектори от числа с фиксирана дължина – ембединги;
- извличането взема ID на всеки токен и чете съответния ред от ембединг-таблицата на модела, и този ред става вход на първия слой;
- без него графът нямаше да има върху какво да изчислява.

**Какво е ембединг-таблица:**

- това е матрица от тегла на модела с размер „размер на речника × размер на ембединга“; един ред е векторът от числа за един токен;
- в кода на C++ това е полето на модела `tok_embd`, а във файла GGUF – тензорът `token_embd.weight`; имената `embed_tokens` и `tok_embeddings` идват от изходните чекпойнти на PyTorch – скриптът за конвертиране ги съпоставя с `token_embd`, така че е безполезно да се търси нещо под тези имена вътре в `llama.cpp`;
- тя се създава при зареждането на модела, в списъка с тензори на конкретната архитектура (`load_arch_tensors`, извиква се от `load_tensors`);
- при изпълнението на графа за всеки токен на батча се чете редът с индекс **token\[i\]** и попада на позиция **i** от входа на графа;
- след това графът пресмята вече върху ембедингите (слоеве на трансформъра, внимание, feed-forward, логити).

**Къде е това в кода:** ембединг-таблицата е полето на модела `tok_embd`, а извличането от нея строи `llm_graph_context::build_inp_embd` (файл `src/llama-graph.cpp`), който всяка архитектура извиква още с първия ред на своя граф. Той създава два входни тензора – вектор I32 за ID на токените и матрица F32 за готовите ембединги – и прекарва токенния клон през `ggml_get_rows` по таблицата; в графа попадат и двата клона, а `ggml_build_forward_select` маркира за изчисляване само този, който съответства на съдържанието на ubatch – токени или готови вектори. Именно затова е нужен: топологията на графа остава една и съща от ubatch на ubatch и графът може да се преизползва. Самите ID ги записва във входния тензор `llm_graph_input_embd::set_input`, която `process_ubatch` (файл `src/llama-context.cpp`) извиква вече след като графът е построен и разположен, непосредствено преди изпълнението му. Тоест редът не се копира от decode на CPU: той се чете на бекенда, по време на изчисляването на графа, заедно с всичко останало – а тъй като ембединг-таблицата обикновено се съхранява квантувана, `ggml_get_rows` попътно разпакова всеки ред във float. При decode по един токен се чете точно един ред от таблицата.

**Извличането, както се строи в графа (`src/llama-graph.cpp`, `llm_graph_context::build_inp_embd`):**

```cpp
// src/llama-graph.cpp, llm_graph_context::build_inp_embd() — съкратено: премахнати са дебъг
// колбеците и проверките. Превръщането на токен във вектор не е memcpy вътре в decode, а възел
// на изчислителния граф, който се изпълнява на бекенда заедно с целия останал модел.
ggml_tensor * llm_graph_context::build_inp_embd(ggml_tensor * tok_embd) const {
    const int64_t n_embd_inp = hparams.n_embd_inp();

    auto inp = std::make_unique<llm_graph_input_embd>(n_embd_inp);

    inp->tokens = ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, ubatch.n_tokens);
    ggml_set_input(inp->tokens);

    inp->embd = ggml_new_tensor_2d(ctx0, GGML_TYPE_F32, n_embd_inp, ubatch.n_tokens);
    ggml_set_input(inp->embd);

    // избираме един от 2 входа според съдържанието на батча
    std::array<ggml_tensor *, 2> inps;

    // клонът с ембедингите на токените (ubatch.token != nullptr)
    {
        auto & cur = inps[0];

        cur = ggml_get_rows(ctx0, tok_embd, inp->tokens);

        // ... съкратено: незадължителна LoRA-корекция на ембединг-таблицата
        //     и падинг, който е нужен при n_embd_inp != n_embd
    }

    // клонът с готовите вектори (ubatch.embd != nullptr)
    {
        auto & cur = inps[1];

        cur = inp->embd;
    }

    ggml_tensor * cur = ggml_build_forward_select(gf, inps.data(), inps.size(), ubatch.token ? 0 : 1);

    // ... съкратено: view при n_embd_inp != n_embd и умножение по f_embedding_scale

    res->add_input(std::move(inp));

    return cur;
}
```

**Какво се случва и за какво е нужно:**

- за всеки индекс **i** в ubatch ID на токена **ubatch.token\[i\]** се записва във входния тензор на графа `inp_tokens`.

  **За какво е нужно:** графът се изпълнява на бекенда и ID му трябват там като негови собствени данни, а не като указател към батча, предаден от приложението.
- `ggml_get_rows` чете от таблицата `tok_embd` реда с този индекс – вектор с дължина `n_embd`, защото самата таблица е с ширина `n_embd`. При някои архитектури е нужен по-широк вход на графа, `n_embd_inp` – това са модели със слоеве deepstack и gemma4-assistant, която задава собствена стойност; там редът се допълва с падинг до тази ширина, а преди първия слой отново се стеснява до `n_embd`.

  **За какво е нужно:** този ред е ембедингът на токена – входът за първия слой на трансформъра.
- редовете за всички токени на ubatch се нареждат в матрица, която получава първият слой.

  **За какво е нужно:** слоевете обработват целия ubatch наведнъж; без този възел графът изобщо нямаше да има вход.

## Стъпки 11 и 12: Прекарване на батча през модела (вход в decode, подбатчове)

**Стъпки 11 и 12** – приложението извиква `llama_decode`, като му подава батч. Вътре батчът се проверява и подготвя (стъпка 11), след това се разбива на подбатчове (стъпка 12) и всеки подбатч се прекарва през модела (стъпка 13). Превръщането на идентификаторите на токените във вектори-ембединги не е отделна стъпка от страната на хоста: то е първият възел на изчислителния граф, затова става на стъпка 13 заедно с всичко останало. По-долу са входната точка, цикълът по подбатчовете и разбиването на батча.

**За какво е нужна функцията `llama_decode`:**

- тя прекарва един батч от токени през модела и попълва вътрешния буфер на контекста с логити – „сурови“ оценки за всеки възможен следващ токен;
- по тези логити приложението (чрез семплера) избира един следващ токен и при нужда отново вика decode с един нов токен в батча;
- така се повтаря до края на отговора (EOS) или до лимита.

Публичният API е `llama_decode` (в `src/llama-context.cpp`):

```cpp
// Стъпка 11: входна точка на decode. Публичната C-функция само предава батча
// на C++-контекста; цялата работа се извършва в llama_context::decode.
// src/llama-context.cpp
int32_t llama_decode(
        llama_context * ctx,
          llama_batch   batch) {
    const int ret = ctx->decode(batch);
    // ret == 1 означава „няма свободен слот в KV-cache за този батч“ – това е предупреждение,
    // а не срив, затова не се логва като грешка
    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`). Първо батчът се проверява. Ако този контекст изобщо няма модул за памет – например това е контекст само за ембединги – decode просто предава батча на `encode` и връща резултата. Иначе той извиква `balloc->init(...)`, резервира планировчика и прилага отложените обновявания на паметта (KV-cache). След това веднъж се извиква `memory->init_batch`: това единствено извикване разбива целия батч на подбатчове и връща контекст на паметта, в който лежат всички те наведнъж. Нататък цикълът върви по този контекст: взема поредния подбатч чрез `get_ubatch()`, прекарва го през `process_ubatch`, копира получените логити във вътрешния буфер на контекста и преминава към следващия подбатч чрез `next()`.

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

```cpp
// src/llama-context.cpp, llama_context::decode — съкратено
    memory_update(false);                   // прилагаме отложените измествания/копирания
    llama_memory_context_ptr mctx;
    // разбиваме ЦЕЛИЯ батч на подбатчове от не повече от n_ubatch токена, с едно извикване
    // ... при FAILED_PREPARE: веднъж оптимизираме кеша, опитваме пак, иначе връщаме 1
    mctx = memory->init_batch(*balloc, cparams.n_ubatch, output_all);
    if (!mctx) {
        return -2;
    }
    // ... резервираме изходния буфер за n_outputs_all реда логити
    do {
        const auto & ubatch = mctx->get_ubatch();
        // ... n_outputs – колко реда логити дава този подбатч
        ggml_status status;
        // стъпка 13: строим (или преизползваме) графа и го изпълняваме на CPU/GPU
        const auto * res = process_ubatch(ubatch, ctx_type_to_graph_type(cparams.ctx_type), mctx.get(), status);
        if (!res) {
            // ... връщаме назад позициите на този подбатч от модула за памет
            switch (status) {
                case GGML_STATUS_ABORTED:      return  2;
                case GGML_STATUS_ALLOC_FAILED: return -2;
                case GGML_STATUS_FAILED:       return -3;
                case GGML_STATUS_SUCCESS:      GGML_ABORT("should not happen");
            }
        }
        auto * t_logits = res->get_logits();
        // стъпка 14: взимаме логитите на този подбатч от бекенда в буфера на контекста –
        // пропуска се, ако всички изходни последователности имат бекенд-семплер
        if (logits.data && t_logits && n_outputs > 0 && needs_raw_logits(ubatch, sampling.samplers)) {
            ggml_backend_t backend_res = ggml_backend_sched_get_tensor_backend(sched.get(), t_logits);
            float * logits_out = logits.data + n_outputs_prev*n_vocab;
            // ... проверки, че тези редове се събират в изходния буфер
            ggml_backend_tensor_get_async(backend_res, t_logits, logits_out, 0, n_outputs*n_vocab*sizeof(float));
        }
        // ... ембедингите и изходите на бекенд-семплера се извличат по същия начин
        n_outputs_prev += n_outputs;
    } while (mctx->next());
    // след цикъла приложението чете логитите чрез llama_get_logits_ith
```

**Първо се проверява батчът:**

- че в него са подадени или токени, или готови ембединги;
- че този контекст наистина има модул за памет – ако го няма, decode веднага предава батча на `encode`.

След това батчът се инициализира чрез `balloc->init` – това е нужно, за да се попълнят позициите на токените и да се реши за кои позиции да се изчисляват логити (обикновено само за последната). Резервира се планировчикът и се обновява паметта (KV-cache).

Нататък `memory->init_batch` разбива батча на подбатчове с размер не повече от `n_ubatch` токена всеки и връща контекст на паметта, в който лежат всички те – така големият батч се разбива на части, които на свой ред се прекарват през модела. Цикълът ги взема един по един и за всеки вика `process_ubatch(...)`: вътре се строи изчислителен граф (ембединги, слоеве на трансформъра, изход към логити) – или се преизползва вече построен, ако формата на прохода с нищо не се е променила, – изпълнява се изчислението на CPU/GPU и се връщат тензори с логити: „сурови“, ненормирани оценки по речника.

`init_batch` е метод на модула за памет, затова всеки тип памет има своя реализация; за обикновения KV-cache това е `llama_kv_cache::init_batch` в `src/llama-kv-cache.cpp` (самият интерфейс е обявен в `src/llama-memory.h`):

```cpp
// src/llama-kv-cache.cpp, llama_kv_cache::init_batch — съкратено
// Стъпка 12: целият батч се реже на подбатчове тук, с едно извикване, преди да е стартиран който и да е от тях
llama_memory_context_ptr llama_kv_cache::init_batch(
            llama_batch_allocr & balloc,
            uint32_t n_ubatch,
            bool embd_all) {
    do {
        balloc.split_reset();

        std::vector<llama_ubatch> ubatches;
        while (true) {
            // всяко извикване взема поредните (не повече от) n_ubatch все още неизползвани токени на батча
            auto ubatch = n_stream == 1 ? balloc.split_simple(n_ubatch) : balloc.split_equal(n_ubatch, true, 0);
            if (ubatch.n_tokens == 0) {
                break;  // батчът е изчерпан
            }
            ubatches.push_back(std::move(ubatch));
        }

        if (balloc.get_n_used() < balloc.get_n_tokens()) {
            break;  // част от токените не са могли да се наредят в нито един подбатч
        }

        // намираме слот в KV-cache за всеки подбатч, преди да фиксираме който и да е
        auto sinfos = prepare(ubatches);
        if (sinfos.empty()) {
            break;
        }

        return std::make_unique<llama_kv_cache_context>(
                this, std::move(sinfos), std::move(ubatches));
    } while (false);

    // за този батч няма място – decode го превръща в код за връщане 1
    return std::make_unique<llama_kv_cache_context>(LLAMA_MEMORY_STATUS_FAILED_PREPARE);
}
```

**Какво прави `init_batch` и за какво е нужно:**

- с един проход реже целия батч на порции от не повече от `n_ubatch` токена всяка.

  **За какво е нужно:** едно извикване на `process_ubatch` обработва ограничен брой токени – така при дълъг промпт не нараства паметта за междинните тензори на графа.
- за всяка порция копира в `ubatch` токените, позициите и флаговете на изходите.

  **За какво е нужно:** `process_ubatch` получава готов подбатч и строи графа точно за него.
- преди да върне резултат, резервира слот в KV-cache за всеки подбатч и едва тогава фиксира резултата.

  **За какво е нужно:** ако кешът не побира целия батч, decode научава това, преди да е отработил който и да е подбатч, и може да съобщи на извикващия, вместо да остави в кеша половин батч.
- върнатият контекст на паметта съхранява списъка с подбатчове; цикълът в decode върви по него чрез `get_ubatch()` и `next()`.

  **За какво е нужно:** дългият промпт се обработва с няколко прохода през модела, без един-единствен огромен граф.

Цикълът по подбатчовете в `llama_context::decode` е устроен така: контекстът на паметта, върнат от `memory->init_batch`, подава по един подбатч чрез `get_ubatch()`, за всеки се вика `process_ubatch(ubatch, gtype, mctx, status)`, а `next()` преминава към следващия, докато подбатчовете свършат. Типът граф `gtype` не се избира за всеки подбатч – той следва от типа на контекста и е един и същ за всички проходи на този контекст. Между прохода по дълъг промпт и прохода с един токен има по-проста разлика: проходът по батч взема броя нишки за батч, а не за генериране, и при създаването на контекста `llama.cpp` резервира буферите за изчисления по най-лошия случай веднага и за двете форми. След всеки `process_ubatch` логитите за позициите, маркирани като изходни, се копират от бекенда във вътрешния буфер на контекста, откъдето ги чете `llama_get_logits_ith`, – но само ако поне една от тези позиции принадлежи към последователност без собствен бекенд-семплер: когато семплерите са закачени към всички, суровите логити изобщо не се копират. Да се строи графът наново за всеки подбатч би било разточително, затова `llama.cpp` преизползва предишния, ако параметрите, които напълно определят топологията му, не са се променили.

**Редът на извикванията при един decode (обобщение):**

- `llama_decode(ctx, batch)` → `ctx->decode(batch)`;
- в decode: проверка на батча (и, ако контекстът няма модул за памет, предаване на батча на `encode` и връщане), `balloc->init(...)`, резервиране на планировчика, прилагане на отложените обновявания на паметта (KV-cache);
- `memory->init_batch(...)` – едно извикване, което разбива целия батч на подбатчове и резервира слот в кеша за всеки; след това се резервира изходният буфер;
- цикъл: `get_ubatch()` подава поредния `ubatch`, вика се `process_ubatch(ubatch, ...)` – вътре `model.build_graph` (или преизползване на предишния граф), `res->set_inputs`, `graph_compute` – логитите се копират в буфера на контекста, а `next()` преминава към следващия подбатч;
- след цикъла приложението чете логитите чрез `llama_get_logits_ith`, семплерът връща следващия токен.

Логитите се копират в изходния буфер на контекста. Оттам ги чете `llama_get_logits_ith` – по тези числа семплерът избира следващия токен (например най-вероятния или по temperature/top_p).

Върната стойност на `llama_decode`: 0 – успех; 1 – в KV-cache не е намерен свободен слот за този батч (да се намали батчът или да се увеличи контекстът); 2 – изчислението е прекъснато от колбек; -1 – самият батч е некоректен; всичко под -1 е фатална грешка, най-често неуспешно заделяне на буфер. Положителният код е предупреждение, а не срив, но и 2 е положително, така че проверката само за отрицателна стойност ще пропусне прекъсването незабелязано – проверката трябва да е за всяка стойност, различна от 0. След прекъсване или фатална грешка вече отработилите подбатчове остават в паметта на контекста; докъде е стигнал батчът, може да се разбере чрез `llama_memory_seq_pos_min` и `llama_memory_seq_pos_max`. Минималните примери, които идват в комплекта с `llama.cpp`, просто смятат всеки ненулев код за фатален и спират. Приложението трябва да проверява върнатата стойност и при грешка да прекратява генерирането или да извежда съобщение за грешка.

## Стъпка 13а: Подготовка за прекарването на подбатча (памет, параметри на графа)

**Стъпка 13** е разбита в статията на три части: подготовка за прекарването на подбатча, построяване на графа и заделяне на буфери, запис на входовете и изпълнение на графа. Цялата работа по един подбатч се извършва в метода `llama_context::process_ubatch` (файл `src/llama-context.cpp`). Извиква се от `llama_context::decode` в цикъл. Разбиването на подбатчове става по-рано и само веднъж: `memory->init_batch` се вика с целия батч и връща обект на контекст на паметта, в който вече лежат всички подбатчове заедно с нужното им състояние на кеша. Нататък цикълът върви по тях – `mctx->get_ubatch()` връща текущия, `mctx->next()` преминава към следващия и връща false, когато са свършили, – и за всеки подбатч веднъж вика `process_ubatch`. Вътре стъпка по стъпка става следното: подготовка на паметта, решение дали да се преизползва вече построен граф, или да се строи наново, при нужда построяване на графа и заделяне на буфери, запис на входните данни в графа, изпълнение на графа на CPU/GPU. По-долу е първата част от стъпка 13: входната точка и подготовката.

**За какво е нужен изчислителният граф:**

- моделът (трансформърът) е верига от операции: ембединги, слоеве на вниманието (Q, K, V, softmax, претеглена сума), нормализации, feed-forward и т.н.;
- вместо всяка операция да се вика ръчно, двигателят строи граф – списък от възли (операции) и връзки между тях;
- след това планировчикът обхожда графа в топологичен ред и изпълнява операциите на CPU или GPU;
- така възлите могат автоматично да се разпределят по устройствата и графът да се преизползва при еднакви размери на батча.

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

```cpp
// src/llama-context.cpp, llama_context::process_ubatch — един подбатч се прекарва през модела;
// извиква се от llama_context::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()) {
        LLAMA_LOG_ERROR("%s: failed to apply memory context\n", __func__);
        ret = GGML_STATUS_FAILED;
        return nullptr;
    }

    // 2) резултатът от ПРЕДИШНОТО построяване: в него са графът и входните му тензори
    auto * res = gf_res_prev.get();
    auto * gf  = res->get_gf();

    // 3) всичко, от което зависи топологията на графа; графът може да се преизползва само тогава,
    //    когато тези параметри съвпадат с тези, с които е бил построен
    const auto gparams = graph_params(res, ubatch, mctx, gtype);

    // ... пропуснато: решението за преизползване, build_graph, set_inputs и graph_compute – вж. по-долу
}
```

**Какво се случва в началото на `process_ubatch` и за какво е нужно:**

- Извиква се `mctx->apply()`, ако е подаден контекст на паметта (`mctx`).

  **За какво е нужно:**
- контекстът на паметта обновява състоянието на KV-cache и на служебните буфери (например измества указателите към следващите свободни позиции); при построяването и изпълнението графът ще се обръща към тези буфери – те трябва да са в актуално състояние.
- `gf_res_prev` е резултатът от предишното построяване на графа: самият граф, входните му тензори и параметрите, с които е бил построен. При най-първото извикване е празен, затова графът се строи от нулата; нататък той става кандидат за преизползване.

  **За какво е нужно:**
- именно съхраняването на последния граф прави преизползването възможно – ако новите параметри съвпадат със запазените в него, същият граф може да се изпълни отново с нови стойности на входовете.
- в `gparams` се събира всичко, от което зависи формата на графа: архитектурата на модела и нейните хиперпараметри, параметрите на контекста, копие на текущия `ubatch` (колко токена има в него, как са групирани по последователности, има ли в него ID на токени или готови ембединги), типът на графа, планировчикът, заредените LoRA-адаптери и control vector, контекстът на паметта, семплерите по последователности и броят позиции, за които са нужни изходи.

  **За какво е нужно:**
- правилото е такова: два графа с еднакви `gparams` имат еднаква топология; именно това прави възможна проверката за преизползване по-долу, а същите стойности после задават размерите на възлите и на входните тензори при построяването на графа.

Ако `mctx->apply()` върне `false`, функцията веднага връща `nullptr` и в `ret` се записва `GGML_STATUS_FAILED` – извикващият код (`decode`) ще обработи грешката и ще прекрати цикъла по подбатчовете.

**Цитат по смисъл: какво прави `mctx->apply()` и какво влиза в** gparams **(по кода на `src/llama-context.cpp` и `src/llama-kv-cache.cpp`):**

```cpp
// mctx->apply() — фиксира състоянието на паметта, подготвено за този подбатч. За KV-cache това означава
// да се запишат позицията и id на последователността на всеки токен в отредените му клетки и да се преизчисли
// колко клетки ще трябва да прочете графът (src/llama-memory.h, src/llama-kv-cache.cpp)
if (mctx && !mctx->apply()) { ret = GGML_STATUS_FAILED; return nullptr; }

// graph_params() събира всичко, от което зависи топологията на графа: архитектурата, параметрите на модела
// и на контекста, копие на подбатча, типа на графа, планировчика, адаптерите, контекста на паметта,
// семплерите и n_outputs – проверката за преизползване сравнява точно този набор
const auto gparams = graph_params(res, ubatch, mctx, gtype);
```

## Стъпка 13б: Построяване на изчислителния граф и заделяне на буфери

**Стъпка 13 (продължение)** – построяване на графа и заделяне на буфери за него. След като паметта е подготвена и `gparams` е формиран, двигателят решава: може ли да се използва повторно вече построеният граф (същата форма на подбатча и същите параметри на построяване), или графът трябва да се построи наново и да се заделят буфери за него. Ако графът се използва повторно, веднага се преминава към записване на входовете и изпълнение. Ако не – извикват се `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)) {
        // ... пропуснато: при pipeline parallelism тук се синхронизира предишното, все още течащо изчисление,
        //     иначе set_inputs би презаписал тензорите, които то още чете
        n_reused++;  // брояч на повторните използвания (профилиращият инструмент го показва като "graphs reused")
    } else {
        res->reset();   // изхвърляме стария граф заедно с входните му тензори

        ggml_backend_sched_reset(sched.get());  // нулираме разбиването на планировчика и неговите заделяния
        ggml_backend_sched_set_eval_callback(sched.get(), cparams.cb_eval, cparams.cb_eval_user_data);

        gf = model.build_graph(gparams);  // ембединги → слоеве на трансформъра → логити

        if (!gf) {
            LLAMA_LOG_ERROR("%s: failed to initialize graph\n", __func__);
            ret = GGML_STATUS_FAILED;
            return nullptr;
        }

        // планировчикът разпределя графа по устройствата и заделя памет за всеки тензор
        if (!ggml_backend_sched_alloc_graph(sched.get(), gf)) {
            LLAMA_LOG_ERROR("%s: failed to allocate graph\n", __func__);
            ret = GGML_STATUS_ALLOC_FAILED;
            return nullptr;
        }
    }
```

**Стъпка по стъпка (какво се случва и за какво е нужно):**

- Проверката `res->can_reuse(gparams)`: еквивалентен ли е новият набор от параметри на този, с който е построен запазеният граф. Сравняват се формата на подбатча (колко токена има, как са групирани в последователности, дали идват като ID на токени или като готови ембединги), броят позиции, за които са поискани изходи, архитектурата и типът на графа, заредените адаптери и няколко флага на контекста; след това за всеки входен тензор на графа поотделно се пита дали още е подходящ.

  **За какво е нужно:**
- при генериране токен по токен всяко следващо извикване носи подбатч с точно същата форма – един токен, една последователност, един изход, – затова цялата проверка минава, графът не се строи наново и остава само да се запишат новите стойности във входовете му. Именно това прави генерирането бързо: иначе построяването на графа за голям модел би струвало милисекунди при всеки токен.
- Ако повторното използване е изключено или параметрите са се променили: `res->reset()` – изчистват се старите възли на графа и свързаните с него буфери.

  **За какво е нужно:**
- преди построяването на нов граф старият трябва да се нулира, иначе възлите и тензорите ще се натрупват.
- `ggml_backend_sched_reset(sched.get())` – планировчикът нулира състоянието си (разпределението на възлите по бекендите, резервираните буфери за този граф).

  **За какво е нужно:**
- планировчикът ще определя наново на кое устройство да се изпълнява всеки възел на новия граф и ще заделя памет за него.
- `gf = model.build_graph(gparams)` – строи се графът на GGML: възли за ембедингите и позициите, след това за всеки слой на трансформъра – внимание (Q, K, V, softmax, претеглена сума), нормализация, feed-forward, пак нормализация; накрая – изходен слой към логити (размерът на речника).

  **За какво е нужно:**
- графът описва кои операции да се изпълнят и в какъв ред; без него на планировчика няма какво да изпълнява. `llama_model::build_graph` в `src/llama-model.cpp` е само тънка обвивка: тя извиква строителя за конкретната архитектура, а след това добавя слоя за пулинг, слоевете за семплиране на устройството, ако има такива, и маркировката на изходните тензори. Слоевете ги подрежда именно строителят за архитектурата, а всяка архитектура има свой файл в `src/models/` – `llama.cpp`, `gemma3.cpp`, `qwen3.cpp` и още около 150.
- `ggml_backend_sched_alloc_graph(sched.get(), gf)` – планировчикът обхожда графа, за всеки тензор определя бекенда (CPU или GPU според разпределението на слоевете), заделя буфер на този бекенд (или използва повторно резервирания при създаването на контекста).

  **За какво е нужно:**
- без заделени буфери данните на графа няма къде да се съхраняват; при първия decode буферите се резервират тук (или при `graph_reserve` при създаването на контекста), а при следващите повторни използвания на графа ново заделяне не е нужно.

**Структура на графа по слоеве (схема):**

- вход: ембедингите на токените от подбатча плюс отделен тензор с позициите им; позициите не се добавят към ембедингите, използват се вътре във всеки слой;
- за всеки слой на трансформъра: нормализация на входа → блок за внимание (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_vocab` числа за всяка от тези позиции.

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

```cpp
// llama_model::build_graph (src/llama-model.cpp) връща ggml_cgraph – плосък списък от възли
// с техните зависимости; gparams вече фиксира формата на подбатча, типа на графа и контекста на паметта
ggml_cgraph * gf = model.build_graph(gparams);

// ggml_backend_sched_alloc_graph (ggml/include/ggml-backend.h) реже графа на вериги от възли,
// принадлежащи на един бекенд, и поставя всеки тензор в изчислителния буфер на този бекенд
bool ok = ggml_backend_sched_alloc_graph(sched.get(), gf);
if (!ok) { ret = GGML_STATUS_ALLOC_FAILED; return nullptr; }
```

Резервирането на буферите се прави един път, при създаването на контекста, в `graph_reserve` (`src/llama-context.cpp`). Там се създава фиктивен ubatch със зададен размер, за него се построява граф и от планировчика се иска да резервира за него достатъчно големи изчислителни буфери. Конструкторът го прави три пъти – с ubatch с размер на цял промпт, после с ubatch по един токен на последователност, после пак с промптовия, – така че буферите да са изчислени за най-лошия случай и по време на инференса нищо да не се заделя наново. Построеният тук граф не се запазва за повторно използване: `graph_reserve` нарочно нулира запазения предишен резултат – именно за да може първият истински 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);  // попълваме входните тензори на графа с този подбатч: ID на токени, позиции, индекси за записване в KV, маска на вниманието
    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 на токените (или готови ембединги – в по-редкия случай, когато извикващият е подал ембединги, а не токени; иначе търсенето в таблицата с ембединги само е възел на графа), позициите на токените, индексите на клетките в KV-cache, в които този подбатч трябва да запише своите ключове и стойности, маската на вниманието, която казва към кои запазени позиции всеки токен може да поглежда, и списъка с позициите, за които са нужни изходи.

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

**Цитат от `graph_compute` (метод на контекста, който изпълнява графа върху планировчика, `src/llama-context.cpp`):**

```cpp
// src/llama-context.cpp, llama_context::graph_compute – подава построения граф на планировчика
ggml_status llama_context::graph_compute(ggml_cgraph * gf, bool batched) {
    // "batched" означава, че в подбатча има повече от един токен: обработката на промпта взема броя нишки за батч,
    // генерирането токен по токен – по-малкия; при един токен излишните нишки не си струват
    int n_threads        = batched ? cparams.n_threads_batch : cparams.n_threads;
    ggml_threadpool_t tp = batched ? threadpool_batch        : threadpool;

    // ... пропуснато: предаване на избрания пул от нишки на CPU бекенда

    // задаваме броя нишки за всички бекенди
    for (const auto & set_n_threads_fn : set_n_threads_fns) {
        set_n_threads_fn.second(set_n_threads_fn.first, n_threads);
    }

    // едно извикване изпълнява целия граф: планировчикът върви по нарязаните от него вериги и пуска всяка
    // на своя бекенд. "_async" – тук той не чака GPU
    auto status = ggml_backend_sched_graph_compute_async(sched.get(), gf);
    if (status != GGML_STATUS_SUCCESS) {
        LLAMA_LOG_ERROR("%s: ggml_backend_sched_graph_compute_async failed with error %d\n", __func__, status);
    }

    return status;
}
```

**Какво прави `graph_compute` стъпка по стъпка и за какво е нужно:**

- избира броя нишки: `n_threads_batch`, ако в подбатча има повече от един токен, иначе `n_threads`, и подава съответния пул от нишки на CPU бекенда.

  **За какво е нужно:**
- обработката на дълъг промпт е голяма матрична работа, която се мащабира по ядрата; генерирането на един токен – не, и разпръскването му по всички ядра струва само синхронизация. Двете отделни настройки позволяват на потребителя да настрои и двата случая.
- извиква `ggml_backend_sched_graph_compute_async`. Планировчикът вече, на етапа на заделянето, е нарязал графа на вериги от последователни възли, принадлежащи на един бекенд; тук той изпълнява тези вериги една след друга, като вмъква копирания между устройствата там, където на дадена верига ѝ трябва тензор, изчислен от друго устройство.

  **За какво е нужно:**
- именно тук се изпълняват всички слоеве на трансформъра. Резултатът са изходните тензори на графа, запълнени с числа, сред тях и логитите: `n_vocab` стойности за всяка позиция, за която е поискан изход.
- връща статус, без да чака края на работата – това значи `_async` в името.

  **За какво е нужно:**
- докато GPU още е зает, извикващият код вече може да постави в опашката копирането на логитите от графа. Самото изчакване става по-късно, в `llama_context::synchronize`, който се извиква от всеки публичен четец на резултати – включително `llama_get_logits_ith`, – преди да върне на извикващия указателя.

Вторият аргумент на `graph_compute(res->get_gf(), ubatch.n_tokens > 1)` се нарича `batched` и просто показва дали в този подбатч има повече от един токен. Той избира броя нишки и пула от нишки: настройките за батч – за обработка на промпта, обикновените – за генериране токен по токен. Върнатата стойност е статусът на GGML; при успех `process_ubatch` връща `res`, а `decode` копира логитите от него във вътрешния буфер на контекста – за позициите, при които е вдигнат флагът за изход.

При повторно използване на графа (когато размерът на батча и типът на графа съвпадат с предишното извикване) графът не се строи наново и буферите не се заделят повторно – извикват се само `res->set_inputs(&ubatch)` и `graph_compute`. Това ускорява повторните извиквания на decode при генериране токен по токен: графът на Decode се строи един път при първия decode с един токен и след това се използва повторно. Броячът `n_reused` в контекста се увеличава при всяко повторно използване на графа – по него при профилиране може да се оцени делът на повторните използвания.

## Стъпки от 14 до 16: Логити, семплиране и токен обратно в текст

**Стъпки от 14 до 16** – след прогона на подбатча логитите за последната позиция се оказват в буфера на контекста (стъпка 14); по тях семплерът избира един следващ токен (стъпка 15). Тук, в края на раздела, се разглежда и стъпка 16 – превръщането на избрания токен обратно в текст. След `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` връща указател към реда с логити за дадена позиция в батча. Индексът може да е отрицателен – `-1` означава последната позиция, за която е искан изход, и именно него подава цикълът на генериране. Преди да върне каквото и да е, функцията изчаква бекендите и първо проверява дали семплерът не е сработил направо на бекенда, оставяйки собствени логити; обикновеният път е вторият – „суровите“ логити, изнесени от `decode`. Индекс, сочещ към позиция, за която не са искани логити, е грешка: в релийз билд функцията ще върне нулев указател.

```cpp
// Стъпка 14: src/llama-context.cpp – публичният аксесор.
// Връща указател към един ред с логити: n_vocab float, по един за всеки токен от речника.
// i може да е отрицателно: -1 – последната позиция, за която е искан изход.
float * llama_get_logits_ith(llama_context * ctx, int32_t i) {
    ctx->synchronize();   // чакаме бекендите да доизчислят графа

    float * res = nullptr;

    res = ctx->get_sampled_logits_ith(i);   // не е null само ако е сработил семплер на бекенда

    if (!res) {
        res = ctx->get_logits_ith(i);       // обикновеният път: „суровите“ логити, изнесени от decode
    }

    return res;
}

// src/llama-context.cpp – самото търсене на реда
float * llama_context::get_logits_ith(int32_t i) {
    output_reorder();

    try {
        if (logits.data == nullptr) {
            throw std::runtime_error("no logits");
        }

        // output_resolve_row превръща индекса в батча в номер на ред:
        // отрицателно i се отчита от края, иначе output_ids[i]; хвърля изключение,
        // ако за тази позиция не са искани логити
        const int64_t j = output_resolve_row(i);

        return logits.data + j*model.vocab.n_tokens();
    } catch (const std::exception & err) {
        LLAMA_LOG_ERROR("%s: invalid logits id %d, reason: %s\n", __func__, i, err.what());
        // ... пропуснато: дебъг билдът тук пада, релийз билдът връща nullptr
        return nullptr;
    }
}
```

По тези числа следващият токен се избира с помощта на **семплер** – компонент, който по логитите решава кой токен да изведе (например най-вероятния или случаен с оглед на temperature/top_p). В API (`include/llama.h`) извикването изглежда като `llama_sampler_sample(smpl, ctx, idx)`: подават му се веригата от семплери, контекстът и индексът на изходната позиция (`-1` – последната), логитите на тази позиция той чете сам, прилага към тях веригата и връща един токен. Той също така записва този токен в собствената история на веригата, така че извикващият код не трябва да го прави. При следващото извикване на `llama_decode` токенът се подава в батча като единствен нов; цикълът се повтаря, докато моделът не изведе токен за край на генерирането – това се проверява чрез `llama_vocab_is_eog` – или докато не сработи ограничението за дължина. Токенът се превръща обратно в текст чрез `llama_token_to_piece` / `llama_detokenize` (реализацията е в `src/llama-vocab.cpp`).

**Верига от семплери:**

- семплирането в `llama.cpp` е устроено като последователност от стъпки;
- първо към логитите може да се приложи изместване за повторенията (repeat penalty) – намаляване на вероятността на вече появилите се токени;
- след това идват отсичанията, които отхвърлят кандидати: top-k оставя k-те най-големи логита, top-p (nucleus) – само токените с натрупана вероятност до прага p, min-p отхвърля всичко, което е много под лидера;
- temperature идва по-близо до края – деление на логитите на число (temperature > 1 прави разпределението по-меко, \< 1 – по-остро), тоест тя преустройва това, което е останало след отсичанията;
- последен от останалите се избира един токен – например с максимален логит (алчен избор) или на случаен принцип с вероятности по softmax от логитите;
- самата верига – цикъл по регистрираните семплери, всеки от които или променя логитите, или избира токен, – се намира в `src/llama-sampler.cpp`; веригата по подразбиране за конзолните инструменти се събира по параметрите в `common/sampling.cpp`.

Реализацията на превръщането на токен в текст вече е **стъпка 16**; за нея отговарят методите `llama_vocab::impl::token_to_piece` и `llama_vocab::impl::detokenize` в `src/llama-vocab.cpp`. `token_to_piece` по ID на токен дава парче текст; за байтовите токени се извършва преобразуване в символ, за обикновените – взема се текстът от `id_to_token`. `detokenize` обхожда масива от токени, записва парчетата едно след друго в подадения буфер и следи колко място е останало, а за някои речници накрая прогонва и почистване на интервалите. API-то `llama_token_to_piece` (в `include/llama.h`) приема **речник** – не модел; речникът се получава чрез `llama_model_get_vocab` – след това токена, изходния буфер и размера му, колко водещи интервала да се пропуснат и дали да се извеждат специалните токени. Връща броя записани байтове или минус нужния размер, ако буферът е малък, и не записва завършваща нула – така че резултатът трябва да се проверява, което минималните примери и правят. За извеждане токен по токен обикновено се използва `llama_token_to_piece`; за събиране на пълен низ от масив с токени – `llama_detokenize`. Фрагмент:

```cpp
// Стъпка 16: src/llama-vocab.cpp – llama_vocab::impl::token_to_piece
// По 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 {
    static const int attr_special = LLAMA_TOKEN_ATTR_UNKNOWN | LLAMA_TOKEN_ATTR_CONTROL;
    const llama_token_attr attr = token_get_attr(token);
    if (!special && (attr & attr_special)) {
        return 0;   // управляващ токен, който не са искали да се извежда
    }

    // копираме символите на парчето в изходния текстов буфер
    // преди копирането пропускаме до 'lstrip' водещи интервала
    auto _try_copy = [=] (const char * token, size_t size) -> int32_t {
        // ... пропуснато: цикъл, пропускащ до 'lstrip' водещи интервала
        if (length < (int32_t)size) {
            return -(int32_t) size;   // буферът е малък: нужният размер с минус
        }
        memcpy(buf, token, size);
        return (int32_t) size;
    };

    // ... пропуснато: ако кешът token_to_piece е построен, текстът се взема от него

    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_WPM:
            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);
                    return _try_copy(result.data(), result.size());
                }
                // ... пропуснато: байтовите токени се превръщат в един символ чрез token_to_byte
                break;
            }
            // ... пропуснато: клонове за BPE (llama_decode_text), RWKV и PLAMO2
            default:
                GGML_ABORT("fatal error");
        }
    }

    return 0;
}
```

В кода се вижда: управляващ или неизвестен токен просто се пропуска, ако извеждането на специални токени не е искано; за речниците WPM, SPM и UGM обикновените токени се превръщат в текст чрез `llama_unescape_whitespace`, байтовите – чрез `token_to_byte`; клонът за BPE е устроен подобно, но обикновено минава през `llama_decode_text` (декодиране на escape последователности). Цялото копиране минава през един малък помощник и именно там се проверява размерът на буфера: ако парчето не се побира, не се записва нищо, а се връща нужният размер с минус. Функцията `llama_detokenize` в API-то извиква `vocab.detokenize`, която събира текста на цял масив от токени в подадения буфер.

Параметрите на семплирането не се подават в самото извикване: те се фиксират при сглобяването на веригата – `llama_sampler_chain_init`, после по един `llama_sampler_chain_add` за всяка стъпка (`llama_sampler_init_penalties`, `llama_sampler_init_top_p`, `llama_sampler_init_temp` и завършващия `llama_sampler_init_dist` или `llama_sampler_init_greedy`, който всъщност избира токена). Самото извикване приема само веригата, контекста и индекса на изходната позиция. Вътре то набира масив от кандидати от логитите на тази позиция, прогонва през него цялата верига, взема избрания от нея токен, записва го в историята на веригата и го връща. Ако семплерът е сработил на бекенда и токенът вече е избран, функцията веднага го връща, без да докосва веригата на CPU. Върнатият токен след това се подава на следващото извикване на `llama_decode` в батча като единствен нов токен.

## KV-cache и етапи Prefill / Decode (за справка)

В механизма на вниманието всеки токен получава вектор на заявката (Q), на ключа (K) и на стойността (V) – това са числови вектори, които моделът изчислява от своите тегла.

**За какво са нужни Q, K, V:**

- по тях се изчислява „вниманието“ – колко е важна всяка предишна позиция за текущата;
- резултатът е претеглена сума на стойностите V с тегла по Q и K.

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

**Prefill и Decode:**

- Prefill – при първия проход се обработват всички токени на промпта, за тях се изчисляват K и V и се записват в кеша;
- Decode – при следващите стъпки се обработва само един нов токен: за него се изчисляват Q, K, V; K и V първо се дописват в кеша и чак след това Q се умножава по всички ключове, които се намират в кеша, – включително по ключа на текущия токен, – а след маската и softmax – по всички стойности от кеша. Накрая получаваме един вектор на контекста за тази позиция, а след него feed-forward и т.н. Така не изчисляваме наново вниманието по цялата история на всяка стъпка, а само по новия токен – това дава ускорението при генерирането. Разделението на Prefill (много токени наведнъж, натоварване на изчислителните ядра) и Decode (един токен, натоварване на паметта) е характерно за `llama.cpp` и други двигатели.

Реализацията на KV-cache е класът `llama_kv_cache` в `src/llama-kv-cache.cpp`; коя клетка на коя позиция и на коя последователност принадлежи, се следи в `src/llama-kv-cells.h`, а общият интерфейс, през който контекстът се обръща към паметта – `init_batch`, `seq_pos_max` и останалите – е обявен в `src/llama-memory.h`. За всеки кеширан слой на модела се заделят буфери за ключове и стойности (размерът зависи от броя на KV главите, размера на главата и дължината на контекста). При Prefill в тези буфери се записват K и V за всички позиции на промпта; при Decode за новия токен се изчисляват само новите K и V и се дописват в края. Графът на модела при изпълнение се обръща към тези буфери през контекста на паметта, подаден му в параметрите на графа, и ги чете чрез `get_k` и `get_v`.

**Разположение на буферите на KV-cache:** за всеки слой на трансформъра се създават два тензора (или един обединен):

- един за ключовете;
- един за стойностите.

Размерността на всеки от тях е \[брой KV глави × размер на главата, брой клетки на кеша\]: един ред са ключовете (или стойностите) на една позиция за целия слой, а броят на клетките е дължината на контекста, за която е създаден кешът. Тензорите се наричат `cache_k_l0`, `cache_v_l0` и така нататък по номера на слоя – удобен низ за търсене в дъмп на графа, но в изходния код името се сглобява от форматиращ низ, затова там се търси `cache_`, а не цялото име. При създаването на контекста се извиква заделяне на памет за тези тензори на 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 × размер на една глава × брой KV глави × 2 (×2 – ключове и стойности), умножено по размера на един елемент. Тук лесно може да се сгреши в две неща. Първото: трябва да се брои броят на **KV** главите, а не броят на главите на вниманието – в моделите с grouped-query attention няколко глави на заявката делят една глава на ключовете и стойностите и кешът излиза в пъти по-малък, отколкото би се решило по самия брой глави. Второто: размерът на елемента не е фиксиран – типовете на кешовете K и V се задават поотделно (опциите **-ctk** и **-ctv**), и това е именно онова „по-агресивно квантуване“. Затова при ограничена памет се намалява `n_ctx` или се понижава точността на KV-cache. В `llama.cpp` при създаването на контекста буферите се заделят веднага за цялата дължина на контекста; при генерирането се запълват само позициите до текущата. Изключение са слоевете с плъзгащ се прозорец на вниманието: за тях кешът се заделя по размера на прозореца, а не за целия контекст.

Методът на паметта `seq_pos_max(s)` (обявен в `src/llama-memory.h`, реализиран за KV-cache в `src/llama-kv-cache.cpp`) връща максималната позиция, до която е запълнен кешът за последователността `s`. Ако приложението не е проставило позициите на токените само, вместо него ги попълва `src/llama-batch.cpp`: първият токен на батча получава `seq_pos_max(s) + 1`, останалите – позициите след него; така батчът винаги продължава последователността от първата свободна позиция в кеша. Клетките на кеша се маркират като заети в самото начало на `process_ubatch`, чрез извикване на `apply` на контекста на паметта, още преди изчисляването на графа; а ако изчислението не успее, decode отново премахва тези позиции от кеша.

## Обобщение: от файла до първия токен

Кратка последователност от стъпки от стартирането на приложението до появата на първия токен от отговора.

**За какво е полезно:**

- при дебъгване или изучаване на кода можете да се сверявате с този списък и да проверявате на коя стъпка сте;
- така е по-лесно да се намери в изходния код мястото, което съответства на етапа на зареждане или генериране.

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

- `llama_backend_init()` – стартиране на таймера, а ако още няма регистриран бекенд – зареждане на всички налични.

  **За какво е нужно:** един път при стартирането на приложението.
- `llama_model_load_from_file(path, params)` → `llama_model_load_from_file_impl` → проверка на бекенда, callback за прогреса, `llama_model_load(...)` – а вече вътре в него, между другото, се събира списъкът с устройствата за модела.

  **За какво е нужно:** да се зареди моделът от файла и да се получи указател към `llama_model`.
- Вътре в `llama_model_load`: `llama_model_loader` (отваряне на GGUF, индекс на тензорите), след това `llama_model_create` – той пита зареждача за архитектурата (`ml.get_arch()`) и създава обект от съответния клас модел, – а след него поред `load_hparams`, `load_vocab` (в т.ч. `vocab.load(ml, kv)`), `load_stats` и `load_tensors`.

  **За какво е нужно:** стъпка по стъпка да се попълни моделът с архитектура, хиперпараметри, речник и тегла.
- `llama_init_from_model(model, ctx_params)` – създаване на контекста: параметри `n_ctx`, `n_batch`, `n_ubatch`, инициализация на паметта (KV-cache), на планировчика, резерв на графовете за Prefill и Decode (в изходния код и в логовете те се наричат **pp** и **tg**). Старото име `llama_new_context_with_model` още работи, но е означено като остаряло.

  **За какво е нужно:** контекстът е нужен за извикването на `llama_decode`; без него не може да се генерира текст.

**Генериране (за всяко съобщение на потребителя и всеки нов токен в отговора):**

- `llama_tokenize(vocab, prompt, ...)` – промптът в токени; речникът предварително се взема от модела чрез `llama_model_get_vocab(model)`. При извикване с празен изходен буфер функцията връща минус нужния брой токени – така се разбира колко памет да се заделя.

  **За какво е нужно:** моделът работи само с числа (токени).
- Формиране на батч. Най-краткият път е `llama_batch_get_one(tokens, n)`: батчът взема токените както са, а логити само за последната позиция `llama.cpp` иска сама. Където е нужен по-фин контрол, примерите ползват помощниците на библиотеката **common** – `common_batch_clear` и след това `common_batch_add` за всеки токен, чийто последен аргумент казва дали са нужни логити за тази позиция. Тези две не са част от публичния API в `include/llama.h`; те живеят в common/common.h.

  **За какво е нужно:** едно извикване на decode приема един батч; логити са нужни само за последната позиция, за да се избере следващият токен.
- `llama_decode(ctx, batch)` → `ctx->decode(batch)` → `balloc->init` (проверка на батча и попълване на полетата, които извикващият не е попълнил), обновяване на паметта, `memory->init_batch` – той разбива батча на подбатчове – и след това цикъл по тези подбатчове: `process_ubatch` (apply, `build_graph`, `set_inputs`, `graph_compute`), копиране на логитите. Ако контекстът е създаден изобщо без памет, decode просто подава батча на `encode` и се връща.

  **За какво е нужно:** прогон на батча през модела и получаване на логитите във вътрешния буфер на контекста.
- `llama_get_logits_ith(ctx, i)` – указател към логитите на един ред от изхода. Неотрицателното i е индекс на токен в рамките на батча, който контекстът превежда в ред от изходния буфер през `output_ids`, а не номер сред изходите и не позиция в последователността: след Prefill от n токена, където флагът стои само на последния, неговите логити се искат под номер n-1, а под номер 0 ще се върне нулев указател, защото за онзи токен логити не са искани. Отрицателният индекс, обратно, се брои сред редовете на изхода, затова надеждният начин да се поиска последният изход е `-1`.

  **За какво е нужно:** по тях семплерът избира следващия токен.
- Семплерът избира следващия токен; `llama_token_to_piece(vocab, token, buf, size, ...)` го превръща в парче текст – върнатата дължина трябва да се провери, отрицателна стойност значи, че буферът е малък; извеждане; токенът отива в следващия батч; decode се повтаря до токена за край на генерирането (`llama_vocab_is_eog`) или до лимита.

  **За какво е нужно:** цикъл на генериране токен по токен до края на отговора.

Така пътят от файла на модела до първия токен от отговора минава през зареждача на GGUF, зареждането на архитектурата и хиперпараметрите, на речника и тензорите, създаването на контекст с KV-cache и планировчик, токенизацията на промпта, батча, decode, построяването на графа и изпълнението му, семплирането по логитите.

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

**Бележки за версиите и билда:** структурата на кода и имената на файловете в хранилището на `llama.cpp` се менят от версия на версия; всичко в тази статия – имена на файлове, сигнатури на функции и фрагменти код – е сверено с клона master при комита `8887a48f` и при вас може да се различава. При билд с GPU са нужни съответните бекенди (CUDA, Metal, Vulkan и т.н.) и променливи на средата; без тях двигателят работи само на CPU. Примерите за извиквания на API и имената на структурите са дадени по `include/llama.h`; при използване на C обвивки или други езици (Python, Go и т.н.) сигнатурите могат да се различават, но общият сценарий на зареждане и decode остава същият.

**Препоръки за изучаване на кода:** за да се разбере пътят на зареждане на модела, е удобно да се започне от `llama_model_load_from_file` в `src/llama.cpp` и да се мине по извикванията до `llama_model_load`, след това през `llama_model_create` (тук се определя архитектурата), `load_hparams`, `load_vocab`, `load_stats`, `load_tensors`. За пътя на decode – от `llama_decode` в `src/llama-context.cpp`, след това `ctx->decode`, `balloc->init`, `memory->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_meta`, `create_tensor`, `load_all_data`). Паметта и KV-cache – `src/llama-kv-cache.cpp` (`seq_pos_max`, `init_batch`, apply_ubatch), общият интерфейс – в `src/llama-memory.h`. При дебъгване е полезно да се поставят точки на прекъсване на входовете на `load_hparams`, `load_vocab`, `load_tensors` и на входовете на decode, `process_ubatch`, `build_graph`.

## Детайлизация на реализацията по файлове

По-долу е дадена кратка връзка между описаните в статията стъпки и конкретните файлове и функции на хранилището на `llama.cpp`.

**За какво е нужно:**

- при изучаване или дебъгване можете бързо да намерите нужния код по името на файла и на функцията;
- всяка точка показва какво да се търси във файла и защо е нужно.

**src/llama.cpp:**

- `llama_backend_init` – стартиране на таймера и зареждане на бекендите на ggml, които се доставят като отделни динамични библиотеки (бекендите, вкомпилирани в билда, се регистрират сами, без това извикване).

  **За какво е нужно:** един път при стартирането на приложението.
- `llama_model_load_from_file`, `llama_model_load_from_file_impl` – входната точка за зареждане на модела.

  **За какво е нужно:** приложението ги извиква, за да зареди модела от файл.
- `llama_model_load` (статична) – последователно извикване на `llama_model_create` (който избира архитектурата), `load_hparams`, `load_vocab`, `load_stats` и `load_tensors`.

  **За какво е нужно:** тук се създава зареждачът и стъпка по стъпка се попълва моделът. В същия файл са проверката на бекенда, callback-ът за прогреса и списъкът с устройствата (`llama_prepare_model_devices`); самият обект на модела се създава от `llama_model_create` в `src/llama-model.cpp`, този файл само го извиква.

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

- Класът `llama_model_loader`: конструкторът отваря GGUF чрез `gguf_init_from_file`, строи `weights_map` по списъка с тензорите от контекста на GGUF.

  **За какво е нужно:** по името на тензора после могат да се четат данните от файла.
- Методите `get_key`, `get_arch`, `get_tensor_meta`, `create_tensor`, `load_all_data` – четене на метаданни, описание на тензорите и четене на данните им.

  **За какво е нужно:** `load_hparams`, `load_vocab` и `load_tensors` ги извикват.
- `print_info` – извеждане на информация за файла. Поддръжка на няколко файла (splits) и mmap/direct_io.

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

- Класът `llama_model`: `load_hparams` (четене на ключовете на GGUF в hparams), `load_vocab` (извикване на `vocab.load(ml, kv)`), `load_tensors` (формиране на списъците с буфери за CPU/GPU, разпределяне на слоевете, създаване на тензори чрез `create_tensor` и четене на данните им чрез `ml.load_all_data`).

  **За какво е нужно:** тук се създават тензорите на модела според архитектурата и им се назначават буфери на CPU/GPU. Самата архитектура се избира по-рано, извън класа: `llama_model_create` я иска от зареждача чрез `ml.get_arch()` и създава обект от нужния тип. Всичко, което се отнася до една архитектура – нейните хиперпараметри, списъкът с тензори и графът – се намира в отделен файл в `src/models/`, зад методите `load_arch_hparams`, `load_arch_tensors` и `build_arch_graph`, които реализира всеки модел.

**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` – проверка на батча, `balloc->init`, обновяване на паметта, `memory->init_batch` (целият батч се разбива на подбатчове с едно извикване), след това цикъл по тези подбатчове (`process_ubatch`), копиране на логитите. За контекст, създаден без модул памет (например за ембединг модел), няма какво да се декодира и извикването веднага се пренасочва към `encode`.

  **За какво е нужно:** едно извикване на 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.h, `src/llama-kv-cache.cpp`:**

- Интерфейсът `llama_memory_i` (обявен в `src/llama-memory.h`) и основната му реализация – класът `llama_kv_cache` в `src/llama-kv-cache.cpp` – заделяне на буферите на KV-cache: по един тензор K и един тензор V за всеки слой, всеки за цялата дължина на контекста.

  **За какво е нужно:** в тях се съхраняват ключовете и стойностите на механизма на вниманието. Моделите с друг вид състояние (рекурентни, хибридни, с плъзгащ се прозорец) дават собствена реализация на същия интерфейс в съседен файл src/llama-memory-\*.cpp или src/llama-kv-cache-\*.cpp.
- `seq_pos_max(s)` – максималната позиция за последователността s.

  **За какво е нужно:** при формирането на батча позициите на новите токени се вземат като `seq_pos_max(s)` + 1.
- `init_batch` – разбиване на батча на подбатчове (ubatch) с размер не повече от `n_ubatch` и резервиране на място в KV-cache за всеки от тях; заетите позиции се обновяват в хода на обработката на подбатчовете. Планировчикът и графът се обръщат към буферите на KV-cache през параметрите на графа.

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

- Класът `llm_graph_context` – общи строителни блокове, от които файловете на отделните архитектури в `src/models/` сглобяват своите слоеве: `build_inp_embd` (стъпка 13, търсене на ембедингите на токените – `ggml_get_rows` по `tok_embd`), `build_norm`, `build_attn`, `build_ffn`.

  **За какво е нужно:** архитектурите да не повтарят едни и същи операции, а първият възел на всеки граф се строи тук.
- Класовете `llm_graph_input_*` и методите им `set_input` – записване на идентификаторите на токените, позициите и маските на вниманието във входните тензори на вече построения граф.

  **За какво е нужно:** `process_ubatch` ги извиква непосредствено преди `graph_compute`; именно това позволява същият граф да се използва повторно за следващия подбатч.

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

- `llama_sampler_chain_init`, `llama_sampler_chain_add` – създаване на верига от семплери и добавяне на стъпка към нея (наказание за повторения, top-k, top-p, min-p, температура, финален избор).

  **За какво е нужно:** веригата се сглобява един път, преди генерирането; за конзолните програми тя се строи по параметрите в `common/sampling.cpp`.
- `llama_sampler_sample` – стъпка 15: сам чете логитите на нужната позиция, прилага към тях цялата верига и връща един токен; `llama_sampler_accept` записва този токен в собствената история на веригата.

  **За какво е нужно:** тук числата от буфера на контекста се превръщат в следващия токен.

**GGML/GGUF (каталогът `ggml/` на същото хранилище):**

- `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_arch_graph` на конкретната архитектура, а после дописва общата „опашка“ (пулинг, изходна проекция). Самият граф на архитектурата се намира в отделен файл в `src/models/`, по един файл на архитектура. Поддръжката на нов модел се добавя с такъв файл, а не с промяна на `build_graph`; общата схема „ембединги → слоеве → логити“ се запазва.

При изучаване на кода е удобно да се търси по името на функцията или метода: `llama_backend_init`, `llama_model_load_from_file`, `llama_model_load`, `load_arch_hparams`, `load_arch_tensors`, `load_hparams`, `load_vocab`, `vocab.load`, `load_tensors`, `llama_init_from_model`, `llama_model_get_vocab`, `llama_decode`, `decode`, `process_ubatch`, `build_graph`, `build_arch_graph`, `graph_compute`, `llama_get_logits_ith`, `llama_tokenize`, `llama_token_to_piece`. По тях може да се проследи целият път от зареждането на модела до извеждането на токен. Най-късите пълни примери са `examples/simple/simple.cpp` и `examples/simple-chat/simple-chat.cpp`: в тях е показан типичният цикъл – зареждане на модела, създаване на контекст, токенизация, батч, decode, семплиране, извеждане; пълнофункционалните програми (CLI, сървър) се намират в `tools/`.

## Връзка между компонентите и потоци от данни

Накратко – как данните преминават между компонентите от файла на модела до извеждането на токен. **За какво е полезно:** при дебъгване или изучаване на кода можете да проследите откъде се взема всяка стойност и къде се предава.

<div style="overflow-x:auto;margin:1.5rem 0" tabindex="0" role="region">
<svg viewBox="0 0 830 470" width="100%" style="min-width:620px;max-width:830px;height:auto;display:block;margin:0 auto" role="img" aria-label="Връзки на компонентите: приложение, контекст, модел, KV-cache, граф и бекенди" xmlns="http://www.w3.org/2000/svg">
  <defs>
    <marker id="lc-components-head" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
      <path d="M0 0 L10 5 L0 10 z" fill="var(--color-fg-dim)"/>
    </marker>
    <marker id="lc-components-tail" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
      <path d="M0 0 L10 5 L0 10 z" fill="var(--color-fg-dim)"/>
    </marker>
  </defs>
  <rect x="40" y="20" width="240" height="64" rx="8" fill="var(--color-bg-elevated)" stroke="var(--color-link)" stroke-width="1.5"/>
  <text x="160" y="44" text-anchor="middle" fill="var(--color-link)" font-family="var(--font-sans)" font-size="14" font-weight="500">приложение</text>
  <text x="160" y="64" text-anchor="middle" fill="var(--color-fg-dim)" font-family="var(--font-sans)" font-size="12.5">промпт и параметри</text>
  <rect x="40" y="124" width="240" height="64" rx="8" fill="var(--color-bg-elevated)" stroke="var(--color-border)" stroke-width="1.5"/>
  <text x="160" y="148" text-anchor="middle" fill="var(--color-fg)" font-family="var(--font-mono)" font-size="14" font-weight="500">include/llama.h</text>
  <text x="160" y="168" text-anchor="middle" fill="var(--color-fg-dim)" font-family="var(--font-sans)" font-size="12.5">API на двигателя</text>
  <rect x="40" y="228" width="240" height="96" rx="8" fill="var(--color-bg-elevated)" stroke="var(--color-accent)" stroke-width="1.5"/>
  <text x="160" y="252" text-anchor="middle" fill="var(--color-accent)" font-family="var(--font-mono)" font-size="14" font-weight="500">llama_context</text>
  <text x="160" y="272" text-anchor="middle" fill="var(--color-fg-dim)" font-family="var(--font-sans)" font-size="12.5">balloc · memory · sched</text>
  <text x="160" y="289" text-anchor="middle" fill="var(--color-fg-dim)" font-family="var(--font-sans)" font-size="12.5">буфер за логити</text>
  <rect x="40" y="364" width="240" height="76" rx="8" fill="var(--color-bg-elevated)" stroke="var(--color-border)" stroke-width="1.5"/>
  <text x="160" y="388" text-anchor="middle" fill="var(--color-fg)" font-family="var(--font-mono)" font-size="14" font-weight="500">process_ubatch</text>
  <text x="160" y="408" text-anchor="middle" fill="var(--color-fg-dim)" font-family="var(--font-sans)" font-size="12.5">строи и стартира графа</text>
  <path d="M160 84 L160 124" fill="none" stroke="var(--color-fg-dim)" stroke-width="1.5" marker-end="url(#lc-components-head)"/>
  <path d="M160 188 L160 228" fill="none" stroke="var(--color-fg-dim)" stroke-width="1.5" marker-end="url(#lc-components-head)"/>
  <path d="M160 324 L160 364" fill="none" stroke="var(--color-fg-dim)" stroke-width="1.5" marker-end="url(#lc-components-head)"/>
  <rect x="360" y="124" width="250" height="84" rx="8" fill="var(--color-bg-elevated)" stroke="var(--color-border)" stroke-width="1.5"/>
  <text x="485" y="148" text-anchor="middle" fill="var(--color-fg)" font-family="var(--font-mono)" font-size="14" font-weight="500">llama_model</text>
  <text x="485" y="168" text-anchor="middle" fill="var(--color-fg-dim)" font-family="var(--font-sans)" font-size="12.5">hparams · vocab</text>
  <text x="485" y="185" text-anchor="middle" fill="var(--color-fg-dim)" font-family="var(--font-sans)" font-size="12.5">тензори с тегла</text>
  <rect x="360" y="252" width="250" height="76" rx="8" fill="var(--color-bg-elevated)" stroke="var(--color-border)" stroke-width="1.5"/>
  <text x="485" y="276" text-anchor="middle" fill="var(--color-fg)" font-family="var(--font-mono)" font-size="14" font-weight="500">llama_kv_cache</text>
  <text x="485" y="296" text-anchor="middle" fill="var(--color-fg-dim)" font-family="var(--font-sans)" font-size="12.5">K и V по слоеве и позиции</text>
  <rect x="360" y="364" width="250" height="76" rx="8" fill="var(--color-bg-elevated)" stroke="var(--color-border)" stroke-width="1.5"/>
  <text x="485" y="388" text-anchor="middle" fill="var(--color-fg)" font-family="var(--font-mono)" font-size="14" font-weight="500">llm_graph_context</text>
  <text x="485" y="408" text-anchor="middle" fill="var(--color-fg-dim)" font-family="var(--font-sans)" font-size="12.5">възли на графа, src/models/</text>
  <rect x="660" y="364" width="140" height="76" rx="8" fill="var(--color-bg-elevated)" stroke="var(--color-link)" stroke-width="1.5"/>
  <text x="730" y="388" text-anchor="middle" fill="var(--color-link)" font-family="var(--font-mono)" font-size="14" font-weight="500">ggml</text>
  <text x="730" y="408" text-anchor="middle" fill="var(--color-fg-dim)" font-family="var(--font-sans)" font-size="12.5">CPU · GPU</text>
  <path d="M280 250 L320 250 L320 166 L360 166" fill="none" stroke="var(--color-border)" stroke-width="1.5" stroke-dasharray="5 4"/>
  <path d="M280 290 L360 290" fill="none" stroke="var(--color-border)" stroke-width="1.5" stroke-dasharray="5 4"/>
  <path d="M280 402 L360 402" fill="none" stroke="var(--color-fg-dim)" stroke-width="1.5" marker-end="url(#lc-components-head)"/>
  <path d="M610 402 L660 402" fill="none" stroke="var(--color-fg-dim)" stroke-width="1.5" marker-end="url(#lc-components-head)"/>
  <path d="M485 328 L485 364" fill="none" stroke="var(--color-fg-dim)" stroke-width="1.5" marker-end="url(#lc-components-head)" marker-start="url(#lc-components-tail)"/>
  <text x="500" y="350" text-anchor="start" fill="var(--color-fg-dim)" font-family="var(--font-sans)" font-size="12" font-weight="400">четене и запис на K, V</text>
  <text x="330" y="214" text-anchor="start" fill="var(--color-fg-dim)" font-family="var(--font-sans)" font-size="12" font-weight="400">сочи към</text>
  <text x="320" y="282" text-anchor="middle" fill="var(--color-fg-dim)" font-family="var(--font-sans)" font-size="12" font-weight="400">притежава</text>
</svg>
</div>

**Зареждане (един път при стартиране или при смяна на модела):**

- Файлът GGUF → `llama_model_loader` (метаданни и индекс на тензорите `weights_map`).

  **За какво е нужно:** зареждачът дава достъп до полетата на GGUF и до данните на тензорите по име.
- `ml.get_arch()` → `llama_model_create` (типът на модела: LLaMA, Gemma и т.н.).

  **За какво е нужно:** от типа зависят имената на ключовете в GGUF.
- `load_hparams` (размерности и параметри: `n_ctx_train`, n_layer, `n_embd` и т.н.; работната дължина на контекста `n_ctx` във файла не се съхранява – избира се по-късно, при създаването на контекста, а по подразбиране се приема равна на `n_ctx_train`).

  **За какво е нужно:** от тях зависи „формата“ на модела и размерите на тензорите.
- `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` (model.vocab) → масив от токени → батч (token, pos, `seq_id`, logits).

  **За какво е нужно:** едно извикване на decode приема един батч.
- Батч → `llama_decode` → `balloc->init` (позиции от memory) → `memory->init_batch` (едно извикване: целият батч се разбива на подбатчове) → цикъл: ubatch → `process_ubatch` (`build_graph` по модела, `set_inputs`, `graph_compute` върху sched) → логитите се копират в буфера на контекста. Търсенето на ембедингите на токените не е отделен етап, а първият възел на графа, който строи `build_graph`.

  **За какво е нужно:** прогон на батча през модела и получаване на логитите.
- Логити → `llama_get_logits_ith` → семплер → следващ токен → `llama_token_to_piece` (model.vocab) → текст.

  **За какво е нужно:** по логитите се избира един токен и се превръща в текст за извеждане.
- Токенът се добавя към батча; при следващия decode в батча има един нов токен; memory обновява заетите позиции на KV-cache; цикълът се повтаря до EOS или до лимита.

**Обобщение на потоците от данни:**

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

Моделът и контекстът са централните обекти; зареждачът, `balloc`, `memory` и `sched` са спомагателни, свързани с контекста или с модела.

## Какво следва от това

Всичко описано се побира в две фази. Подготовката се случва един път: двигателят регистрира бекенда, отваря GGUF, чете от него архитектурата, размерите и речника, разполага теглата там, където ще се изчисляват, и строи около тях контекста. Генерирането е цикъл: текстът се превръща в токени, токените – в батч, батчът минава през графа, графът връща логити, семплерът избира един токен и този токен отива в следващия батч. Всичко останало в статията са подробности, надградени върху тези две основи.

Четири настройки променят поведението повече от останалите и сега е ясно защо. `n_ctx` определя колко KV-кеш се заделя веднага: паметта расте с дължината на контекста, броя на слоевете и броя на KV главите – и от тези три величини вие избирате само дължината на контекста. `n_gpu_layers` решава кои слоеве живеят в паметта на устройството; съпоставянето им се получава без копиране само там, където устройството умее да построи буфер върху паметта на хоста – на процесора винаги, на унифицирана памет като Apple Silicon също, на дискретна карта – никога. `-ctk` и `-ctv` задават точността на ключовете и стойностите поотделно: това е най-евтиният лост, когато не се побира именно кешът. А `--load-mode` определя как изобщо се чете файлът: съпоставя се в паметта, заключва се в паметта, и двете едновременно, чете се без буфериране или просто се чете – а при стойност по подразбиране `auto` двигателят избира сам.

Ако искате да четете не статията, а изходния код, най-краткият вход е `examples/simple-chat/simple-chat.cpp`: двеста реда, които минават целия път – от зареждането на модела до избора на токен. След това – `llama_context::decode` в `src/llama-context.cpp`, където живее цикълът по подбатчовете. Всички имена в статията са написани точно както в чекаута, посочен в предисловието, – могат да се търсят такива, каквито са.

На същия сайт: [Невронните мрежи с прости думи](/bg/neural-networks-basics/) обясняват какво са теглата и вниманието, които тук се изчисляват; [LoRA](/bg/lora-fine-tuning/) – за адаптерите, които се вливат в същите тегла на GGUF; [RAG и function calling](/bg/rag-and-function-calling/) – за онова, което обикновено се строи върху локален двигател; [Offline AI Launcher](/bg/offline-ai-launcher/) – същият този двигател на Android.

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