<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>GGML on kenji.blog</title><link>http://kenji.blog/ru/tags/ggml/</link><description>Recent content in GGML on kenji.blog</description><generator>Hugo -- gohugo.io</generator><language>ru</language><copyright>kenjinote</copyright><lastBuildDate>Fri, 11 Sep 2026 17:00:00 +0900</lastBuildDate><atom:link href="http://kenji.blog/ru/tags/ggml/index.xml" rel="self" type="application/rss+xml"/><item><title>Без Python! Я создал движок ИИ-вывода только на C++</title><link>http://kenji.blog/ru/p/building-ai-inference-engine-cpp-only/</link><pubDate>Fri, 11 Sep 2026 17:00:00 +0900</pubDate><guid>http://kenji.blog/ru/p/building-ai-inference-engine-cpp-only/</guid><description>&lt;img src="http://kenji.blog/p/building-ai-inference-engine-cpp-only/img/eyecatch.jpg" alt="Featured image of post Без Python! Я создал движок ИИ-вывода только на C++" />&lt;h2 id="1-введение-почему-стоит-отказаться-от-python-и-создать-движок-ии-вывода-на-c">1. Введение: Почему стоит отказаться от Python и создать движок ИИ-вывода на C++?
&lt;/h2>&lt;p>В современной разработке ИИ Python является стандартом де-факто. Благодаря мощным фреймворкам, таким как PyTorch и TensorFlow, можно создавать, обучать и запускать сложные нейронные сети всего несколькими строками кода. Однако за кулисами этих фреймворков стоят низкоуровневые языки, такие как C++ и CUDA, которые берут на себя тяжелые вычислительные процессы. Python играет лишь роль «клея».&lt;/p>
&lt;p>Так зачем же отказываться от Python и создавать движок ИИ-вывода исключительно на C++? Для этого есть несколько веских причин:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Максимальная производительность и низкая задержка&lt;/strong>: Полностью устраняются накладные расходы, связанные с GIL (Global Interpreter Lock) в Python и динамической типизацией. Особенно в системах, требующих работы в реальном времени, задержка в миллисекундах может быть критичной.&lt;/li>
&lt;li>&lt;strong>Простота развертывания&lt;/strong>: Настроить среду Python (огромное количество библиотек, ад зависимостей) на стороне конечного пользователя очень сложно. В случае C++ достаточно распространять один скомпилированный исполняемый бинарный файл (с расширением &lt;code>.exe&lt;/code> или ELF-бинарник) со статической линковкой.&lt;/li>
&lt;li>&lt;strong>Поддержка периферийных устройств (Edge Devices)&lt;/strong>: В средах с жесткими ограничениями ресурсов, таких как смартфоны, встраиваемые устройства или Raspberry Pi, нет возможности запускать среду выполнения Python, потребляющую гигабайты памяти.&lt;/li>
&lt;li>&lt;strong>Прямое управление оборудованием&lt;/strong>: C++ позволяет осуществлять низкоуровневый контроль, такой как выбор времени выделения памяти, явное использование SIMD-инструкций и оптимизация передачи данных в память GPU.&lt;/li>
&lt;/ol>
&lt;p>В этой статье мы подробно, погружаясь в технические глубины, рассмотрим процесс создания с нуля движка вывода для запуска больших языковых моделей (LLM) исключительно на C++, черпая огромное вдохновение в архитектуре библиотеки «GGML», разработанной Георгием Гергановым (Georgi Gerganov).&lt;/p>
&lt;hr>
&lt;h2 id="2-общая-архитектура-движка-вывода">2. Общая архитектура движка вывода
&lt;/h2>&lt;p>Процесс вывода ИИ по своей сути представляет собой «непрерывную серию огромных матричных вычислений». Для эффективного выполнения этой задачи движок вывода должен состоять из следующих компонентов:&lt;/p>
&lt;div class="mermaid">graph TD
A["Входные данные (Токены/Изображения)"] --> B["Управление тензорами"]
B --> C["Граф вычислений (DAG)"]
C --> D["Арена памяти и Аллокатор"]
C --> E["Планировщик и Пул потоков"]
E --> F["Бэкенд CPU (AVX2/ARM NEON)"]
E --> G["Бэкенд GPU (CUDA/Metal)"]
F --> H["Выходные результаты"]
G --> H&lt;/div>
&lt;ol>
&lt;li>&lt;strong>Управление тензорами (Tensor Management)&lt;/strong>: Управление структурами данных многомерных массивов и шагом (Stride) для каждого измерения.&lt;/li>
&lt;li>&lt;strong>Граф вычислений (Computation Graph)&lt;/strong>: Представление операций каждого слоя нейронной сети в виде направленного ациклического графа (DAG).&lt;/li>
&lt;li>&lt;strong>Арена памяти (Memory Arena)&lt;/strong>: Механизм предварительного выделения памяти, позволяющий избежать накладных расходов на динамическое выделение памяти (&lt;code>malloc&lt;/code> или &lt;code>new&lt;/code>).&lt;/li>
&lt;li>&lt;strong>Бэкенд (Backend)&lt;/strong>: Реализация вычислений (ядер), оптимизированная для конкретного оборудования, такого как CPU или GPU.&lt;/li>
&lt;/ol>
&lt;p>Мы будем собирать эти компоненты, используя мощные возможности C++ (шаблоны, адресная арифметика указателей, RAII и т.д.).&lt;/p>
&lt;hr>
&lt;h2 id="3-секреты-управления-памяти-арена-памяти-и-simd-выравнивание">3. Секреты управления памяти: Арена памяти и SIMD-выравнивание
&lt;/h2>&lt;p>Управление памятью в движке вывода — один из самых важных факторов, напрямую влияющих на производительность. Во время вывода, особенно при прохождении через каждый слой модели Transformer, генерируется огромное количество промежуточных тензоров. Если каждый раз выделять и освобождать их с помощью стандартного &lt;code>malloc&lt;/code>, фрагментация кучи и переключение контекста ОС приведут к фатальному падению скорости.&lt;/p>
&lt;p>Поэтому мы применим подход, называемый «&lt;strong>Арена памяти (Memory Arena)&lt;/strong>». Этот метод заключается в вычислении (или фиксированном задании) максимального объема памяти, необходимого в начале вывода, ее выделении одним блоком и последующем распределении памяти простым увеличением (инкрементом) указателя.&lt;/p>
&lt;h3 id="31-важность-выравнивания-alignment">3.1 Важность выравнивания (Alignment)
&lt;/h3>&lt;p>Современные процессоры поддерживают SIMD-инструкции (Single Instruction, Multiple Data). К ним относятся AVX2/AVX-512 от Intel/AMD и NEON от ARM. Эти инструкции обрабатывают 256 бит (32 байта) или 512 бит (64 байта) данных за один раз, но требуют, чтобы обрабатываемые данные в памяти были выровнены по определенной границе байтов (обычно 32 или 64 байта).&lt;/p>
&lt;p>Ниже приведен пример реализации арены памяти на C++ с учетом выравнивания.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-cpp" data-lang="cpp">&lt;span class="line">&lt;span class="cl">&lt;span class="cp">#include&lt;/span> &lt;span class="cpf">&amp;lt;cstdint&amp;gt;&lt;/span>&lt;span class="cp">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">#include&lt;/span> &lt;span class="cpf">&amp;lt;cstddef&amp;gt;&lt;/span>&lt;span class="cp">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">#include&lt;/span> &lt;span class="cpf">&amp;lt;stdexcept&amp;gt;&lt;/span>&lt;span class="cp">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">#include&lt;/span> &lt;span class="cpf">&amp;lt;iostream&amp;gt;&lt;/span>&lt;span class="cp">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">struct&lt;/span> &lt;span class="nc">MemoryArena&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">size_t&lt;/span> &lt;span class="n">size&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">size_t&lt;/span> &lt;span class="n">offset&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kt">uint8_t&lt;/span>&lt;span class="o">*&lt;/span> &lt;span class="n">data&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">MemoryArena&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">size_t&lt;/span> &lt;span class="n">size&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">:&lt;/span> &lt;span class="n">size&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">size&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="n">offset&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// Использование posix_memalign для POSIX систем и _aligned_malloc для Windows
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="cp">#ifdef _WIN32
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">&lt;/span> &lt;span class="n">data&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">static_cast&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="kt">uint8_t&lt;/span>&lt;span class="o">*&amp;gt;&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">_aligned_malloc&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">size&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">64&lt;/span>&lt;span class="p">));&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">#else
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">posix_memalign&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">reinterpret_cast&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="o">**&amp;gt;&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="n">data&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="mi">64&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">size&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">throw&lt;/span> &lt;span class="n">std&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">bad_alloc&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">#endif
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">~&lt;/span>&lt;span class="n">MemoryArena&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">#ifdef _WIN32
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">&lt;/span> &lt;span class="n">_aligned_free&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">data&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">#else
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">&lt;/span> &lt;span class="n">free&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">data&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">#endif
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kt">void&lt;/span>&lt;span class="o">*&lt;/span> &lt;span class="nf">allocate&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">size_t&lt;/span> &lt;span class="n">bytes&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">size_t&lt;/span> &lt;span class="n">alignment&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">64&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// Вычисление выравнивания (определение отступа)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="n">size_t&lt;/span> &lt;span class="n">pad&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">alignment&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">offset&lt;/span> &lt;span class="o">%&lt;/span> &lt;span class="n">alignment&lt;/span>&lt;span class="p">))&lt;/span> &lt;span class="o">%&lt;/span> &lt;span class="n">alignment&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">offset&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">pad&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">bytes&lt;/span> &lt;span class="o">&amp;gt;&lt;/span> &lt;span class="n">size&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">throw&lt;/span> &lt;span class="n">std&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">runtime_error&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;OOM: MemoryArena out of memory&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">offset&lt;/span> &lt;span class="o">+=&lt;/span> &lt;span class="n">pad&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kt">void&lt;/span>&lt;span class="o">*&lt;/span> &lt;span class="n">ptr&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">data&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">offset&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">offset&lt;/span> &lt;span class="o">+=&lt;/span> &lt;span class="n">bytes&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">ptr&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kt">void&lt;/span> &lt;span class="nf">reset&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">offset&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// Освобождение памяти — это просто возврат указателя (O(1))
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Таким образом, при создании тензора память всегда получается через эту арену. Просто вызывая &lt;code>reset()&lt;/code> после завершения каждого шага вывода (например, генерации каждого токена), можно мгновенно повторно использовать память.&lt;/p>
&lt;hr>
&lt;h2 id="4-магия-структуры-данных-тензоров-и-шагов-stride">4. Магия структуры данных тензоров и шагов (Stride)
&lt;/h2>&lt;p>Тензор — это концепция, обобщающая скаляры, векторы и матрицы. Важным моментом в реализации является то, что фактические данные располагаются в памяти в виде &lt;strong>одномерного непрерывного массива&lt;/strong>, но при этом существует понятие «шага (Stride)», позволяющее интерпретировать их как многомерные.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-cpp" data-lang="cpp">&lt;span class="line">&lt;span class="cl">&lt;span class="k">enum&lt;/span> &lt;span class="k">class&lt;/span> &lt;span class="nc">DataType&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">FP32&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">FP16&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">INT8&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// Для квантования
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="n">INT4&lt;/span> &lt;span class="c1">// Для квантования
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">struct&lt;/span> &lt;span class="nc">Tensor&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kt">int&lt;/span> &lt;span class="n">n_dims&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// Количество измерений
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kt">int64_t&lt;/span> &lt;span class="n">ne&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">4&lt;/span>&lt;span class="p">];&lt;/span> &lt;span class="c1">// Количество элементов в каждом измерении (Number of Elements)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="n">size_t&lt;/span> &lt;span class="n">nb&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">4&lt;/span>&lt;span class="p">];&lt;/span> &lt;span class="c1">// Шаг каждого измерения в байтах (Number of Bytes)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="n">DataType&lt;/span> &lt;span class="n">type&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// Тип данных
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kt">void&lt;/span>&lt;span class="o">*&lt;/span> &lt;span class="n">data&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// Указатель на полезную нагрузку
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// Для графа вычислений
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="k">enum&lt;/span> &lt;span class="nc">OpType&lt;/span> &lt;span class="n">op&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Tensor&lt;/span>&lt;span class="o">*&lt;/span> &lt;span class="n">src0&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Tensor&lt;/span>&lt;span class="o">*&lt;/span> &lt;span class="n">src1&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Шаг &lt;code>nb[i]&lt;/code> представляет собой расстояние в байтах в памяти между соседними элементами в измерении &lt;code>i&lt;/code>.
Например, если матрица с количеством элементов $M \times N$ (FP32, 1 элемент = 4 байта) хранится в формате Row-Major (по строкам), шаги будут следующими:&lt;/p>
&lt;ul>
&lt;li>&lt;code>nb[0]&lt;/code> = 4 (байта) : Перемещение по столбцам&lt;/li>
&lt;li>&lt;code>nb[1]&lt;/code> = $N \times 4$ (байта) : Перемещение по строкам&lt;/li>
&lt;/ul>
&lt;p>Используя это, можно реализовать такие операции, как «Транспонирование (Transpose)» или «Представление (View)» без копирования памяти, просто поменяв местами значения шагов. Это очень элегантно и быстро.&lt;/p>
&lt;hr>
&lt;h2 id="5-построение-графа-вычислений-dag-и-ленивые-вычисления">5. Построение графа вычислений (DAG) и ленивые вычисления
&lt;/h2>&lt;p>Подобно PyTorch и другим, наш движок вывода также использует концепцию ленивых вычислений (Lazy Evaluation), близкую к «Define-by-Run». То есть, в момент вызова функции вычисления сами вычисления не выполняются, а строится только граф (зависимости между узлами).&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-cpp" data-lang="cpp">&lt;span class="line">&lt;span class="cl">&lt;span class="n">Tensor&lt;/span>&lt;span class="o">*&lt;/span> &lt;span class="nf">tensor_add&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">MemoryArena&lt;/span>&lt;span class="o">&amp;amp;&lt;/span> &lt;span class="n">arena&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">Tensor&lt;/span>&lt;span class="o">*&lt;/span> &lt;span class="n">a&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">Tensor&lt;/span>&lt;span class="o">*&lt;/span> &lt;span class="n">b&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Tensor&lt;/span>&lt;span class="o">*&lt;/span> &lt;span class="n">out&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">create_tensor&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">arena&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">a&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">type&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">a&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">n_dims&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">a&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">ne&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">out&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">op&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">OpType&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">ADD&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">out&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">src0&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">a&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">out&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">src1&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">b&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">out&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Tensor&lt;/span>&lt;span class="o">*&lt;/span> &lt;span class="nf">tensor_mul_mat&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">MemoryArena&lt;/span>&lt;span class="o">&amp;amp;&lt;/span> &lt;span class="n">arena&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">Tensor&lt;/span>&lt;span class="o">*&lt;/span> &lt;span class="n">a&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">Tensor&lt;/span>&lt;span class="o">*&lt;/span> &lt;span class="n">b&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// b часто бывает транспонирован
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kt">int64_t&lt;/span> &lt;span class="n">ne&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="n">a&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">ne&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="n">b&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">ne&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Tensor&lt;/span>&lt;span class="o">*&lt;/span> &lt;span class="n">out&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">create_tensor&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">arena&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">a&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">type&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">ne&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">out&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">op&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">OpType&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">MUL_MAT&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">out&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">src0&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">a&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">out&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">src1&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">b&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">out&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Поток процесса вывода выглядит следующим образом:&lt;/p>
&lt;div class="mermaid">graph LR
A["Определение тензоров"] --> B["Построение графа через операции"]
B --> C["Топологическая сортировка"]
C --> D["Выделение памяти для выходных данных"]
D --> E["Выполнение узлов по порядку"]&lt;/div>
&lt;p>При оценке графа (прямой проход - forward pass) используется топологическая сортировка для последовательного выполнения узлов, начиная с тех, у которых нет зависимостей. Поскольку это только вывод, нет необходимости хранить градиенты для обратного распространения ошибки, что делает управление памятью очень простым.&lt;/p>
&lt;hr>
&lt;h2 id="6-ядро-математики-и-оптимизации-умножение-матриц-gemm">6. Ядро математики и оптимизации: Умножение матриц (GEMM)
&lt;/h2>&lt;p>Более 90% вычислительной нагрузки при выводе ИИ тратится на общее умножение матриц (GEMM: General Matrix Multiply). Механизм внимания (Attention) и полносвязные сети (Feed-Forward Networks, FFN), являющиеся ядром моделей Transformer, в конечном итоге сводятся к огромным матричным умножениям.&lt;/p>
&lt;p>Произведение $C = A B$ (размер $M \times N$) двух матриц $A$ (размер $M \times K$) и $B$ (размер $K \times N$) можно выразить математически следующим образом:&lt;/p>
$$
C_{i,j} = \sum_{k=0}^{K-1} A_{i,k} \cdot B_{k,j}
$$
&lt;p>Если реализовать это в виде простого тройного цикла, будут постоянно возникать промахи кэша, и производительность будет нулевой.&lt;/p>
&lt;h3 id="61-кэш-блокировка-на-cpu-и-оптимизация-simd">6.1 Кэш-блокировка на CPU и оптимизация SIMD
&lt;/h3>&lt;p>Базовые стратегии для ускорения GEMM на CPU следующие:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Разбиение циклов на тайлы (Кэш-блокировка - Cache Blocking)&lt;/strong>: Разделение матрицы на небольшие блоки, помещающиеся в кэш L1/L2, и вычисление по ним.&lt;/li>
&lt;li>&lt;strong>Упаковка данных (Data Packing)&lt;/strong>: Внутренняя перестановка данных для обеспечения последовательного паттерна доступа к памяти.&lt;/li>
&lt;li>&lt;strong>Использование SIMD&lt;/strong>: Использование инструкций FMA (Fused Multiply-Add), таких как &lt;code>_mm512_fmadd_ps&lt;/code> в AVX-512, для выполнения множества операций умножения-сложения за один тактовый цикл.&lt;/li>
&lt;/ol>
&lt;p>Ниже приведен пример упрощенного скалярного произведения векторов (Dot Product) с использованием C++ и SIMD Intrinsics.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-cpp" data-lang="cpp">&lt;span class="line">&lt;span class="cl">&lt;span class="cp">#include&lt;/span> &lt;span class="cpf">&amp;lt;immintrin.h&amp;gt;&lt;/span>&lt;span class="cp"> &lt;/span>&lt;span class="c1">// Для инструкций AVX
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// Быстрое скалярное произведение для FP32 с использованием AVX2
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kt">float&lt;/span> &lt;span class="nf">dot_product_avx2&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">const&lt;/span> &lt;span class="kt">float&lt;/span>&lt;span class="o">*&lt;/span> &lt;span class="n">a&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="k">const&lt;/span> &lt;span class="kt">float&lt;/span>&lt;span class="o">*&lt;/span> &lt;span class="n">b&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kt">int&lt;/span> &lt;span class="n">n&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">__m256&lt;/span> &lt;span class="n">sum256&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">_mm256_setzero_ps&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kt">int&lt;/span> &lt;span class="n">i&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// Обработка по 8 элементов за раз (256 бит = 32 байта = 8 * 4 байта)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="k">for&lt;/span> &lt;span class="p">(;&lt;/span> &lt;span class="n">i&lt;/span> &lt;span class="o">&amp;lt;=&lt;/span> &lt;span class="n">n&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="mi">8&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="n">i&lt;/span> &lt;span class="o">+=&lt;/span> &lt;span class="mi">8&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">__m256&lt;/span> &lt;span class="n">va&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">_mm256_loadu_ps&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">a&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">i&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">__m256&lt;/span> &lt;span class="n">vb&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">_mm256_loadu_ps&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">b&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">i&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// Инструкция FMA: sum256 = va * vb + sum256
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="n">sum256&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">_mm256_fmadd_ps&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">va&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">vb&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">sum256&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// Горизонтальное сложение значений в SIMD регистре
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kt">float&lt;/span> &lt;span class="n">result&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">8&lt;/span>&lt;span class="p">];&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">_mm256_storeu_ps&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">result&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">sum256&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kt">float&lt;/span> &lt;span class="n">dot&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">result&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">result&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">result&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">result&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">3&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">+&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">result&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">4&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">result&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">5&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">result&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">6&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">result&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">7&lt;/span>&lt;span class="p">];&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// Обработка остатка
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="k">for&lt;/span> &lt;span class="p">(;&lt;/span> &lt;span class="n">i&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="n">n&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="o">++&lt;/span>&lt;span class="n">i&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">dot&lt;/span> &lt;span class="o">+=&lt;/span> &lt;span class="n">a&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">i&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="n">b&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">i&lt;/span>&lt;span class="p">];&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">dot&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Даже эта небольшая хитрость позволяет увеличить скорость в несколько, а то и в десяток раз по сравнению с наивной реализацией.&lt;/p>
&lt;hr>
&lt;h2 id="7-преодоление-аппаратных-барьеров-интеграция-бэкендов-cuda-и-metal">7. Преодоление аппаратных барьеров: Интеграция бэкендов CUDA и Metal
&lt;/h2>&lt;p>Хотя чистая реализация на C++ работает довольно неплохо на CPU, для запуска гигантских моделей, таких как LLM, с приемлемой скоростью (например, генерация 20 и более токенов в секунду) вычислительные возможности GPU становятся необходимыми. Для этого мы внедряем в наш движок уровень абстракции бэкендов.&lt;/p>
&lt;h3 id="71-абстракция-бэкенда">7.1 Абстракция бэкенда
&lt;/h3>&lt;p>Используя полиморфизм в C++, мы можем переключать исполнителей операций (Executors).&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-cpp" data-lang="cpp">&lt;span class="line">&lt;span class="cl">&lt;span class="k">class&lt;/span> &lt;span class="nc">Backend&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">public&lt;/span>&lt;span class="o">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">virtual&lt;/span> &lt;span class="o">~&lt;/span>&lt;span class="n">Backend&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">default&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">virtual&lt;/span> &lt;span class="kt">void&lt;/span> &lt;span class="nf">alloc_buffer&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Tensor&lt;/span>&lt;span class="o">*&lt;/span> &lt;span class="n">t&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">virtual&lt;/span> &lt;span class="kt">void&lt;/span> &lt;span class="nf">free_buffer&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Tensor&lt;/span>&lt;span class="o">*&lt;/span> &lt;span class="n">t&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">virtual&lt;/span> &lt;span class="kt">void&lt;/span> &lt;span class="nf">copy_to_device&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Tensor&lt;/span>&lt;span class="o">*&lt;/span> &lt;span class="n">t&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">virtual&lt;/span> &lt;span class="kt">void&lt;/span> &lt;span class="nf">copy_to_host&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Tensor&lt;/span>&lt;span class="o">*&lt;/span> &lt;span class="n">t&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// Выполнение различных операций
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="k">virtual&lt;/span> &lt;span class="kt">void&lt;/span> &lt;span class="nf">compute_add&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Tensor&lt;/span>&lt;span class="o">*&lt;/span> &lt;span class="n">src0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">Tensor&lt;/span>&lt;span class="o">*&lt;/span> &lt;span class="n">src1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">Tensor&lt;/span>&lt;span class="o">*&lt;/span> &lt;span class="n">dst&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">virtual&lt;/span> &lt;span class="kt">void&lt;/span> &lt;span class="nf">compute_mul_mat&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Tensor&lt;/span>&lt;span class="o">*&lt;/span> &lt;span class="n">src0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">Tensor&lt;/span>&lt;span class="o">*&lt;/span> &lt;span class="n">src1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">Tensor&lt;/span>&lt;span class="o">*&lt;/span> &lt;span class="n">dst&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="72-реализация-бэкенда-nvidia-cuda">7.2 Реализация бэкенда NVIDIA CUDA
&lt;/h3>&lt;p>Для использования графических процессоров NVIDIA мы реализуем бэкенд с помощью расширения CUDA C++. Можно написать собственные ядра, но для умножения матриц лучше всего использовать «cuBLAS» — библиотеку высшего класса, предоставляемую самой NVIDIA.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-cpp" data-lang="cpp">&lt;span class="line">&lt;span class="cl">&lt;span class="cp">#include&lt;/span> &lt;span class="cpf">&amp;lt;cublas_v2.h&amp;gt;&lt;/span>&lt;span class="cp">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">#include&lt;/span> &lt;span class="cpf">&amp;lt;cuda_runtime.h&amp;gt;&lt;/span>&lt;span class="cp">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">class&lt;/span> &lt;span class="nc">CUDABackend&lt;/span> &lt;span class="o">:&lt;/span> &lt;span class="k">public&lt;/span> &lt;span class="n">Backend&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">private&lt;/span>&lt;span class="o">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">cublasHandle_t&lt;/span> &lt;span class="n">handle&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">public&lt;/span>&lt;span class="o">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">CUDABackend&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">cublasCreate&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">&amp;amp;&lt;/span>&lt;span class="n">handle&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">~&lt;/span>&lt;span class="n">CUDABackend&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">cublasDestroy&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">handle&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kt">void&lt;/span> &lt;span class="nf">compute_mul_mat&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Tensor&lt;/span>&lt;span class="o">*&lt;/span> &lt;span class="n">src0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">Tensor&lt;/span>&lt;span class="o">*&lt;/span> &lt;span class="n">src1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">Tensor&lt;/span>&lt;span class="o">*&lt;/span> &lt;span class="n">dst&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">override&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// В CUDA по умолчанию используется формат Column-Major, поэтому нужно быть осторожным с параметрами
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="k">const&lt;/span> &lt;span class="kt">float&lt;/span> &lt;span class="n">alpha&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mf">1.0f&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">const&lt;/span> &lt;span class="kt">float&lt;/span> &lt;span class="n">beta&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mf">0.0f&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kt">int&lt;/span> &lt;span class="n">m&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">src0&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">ne&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">];&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kt">int&lt;/span> &lt;span class="n">k&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">src0&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">ne&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">];&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kt">int&lt;/span> &lt;span class="n">n&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">src1&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">ne&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">];&lt;/span> &lt;span class="c1">// предполагается, что src1 транспонирован
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">cublasSgemm&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">handle&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">CUBLAS_OP_T&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">CUBLAS_OP_N&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">m&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">n&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">k&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">&amp;amp;&lt;/span>&lt;span class="n">alpha&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">(&lt;/span>&lt;span class="k">const&lt;/span> &lt;span class="kt">float&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="n">src0&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">data&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">k&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">(&lt;/span>&lt;span class="k">const&lt;/span> &lt;span class="kt">float&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="n">src1&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">data&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">k&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">&amp;amp;&lt;/span>&lt;span class="n">beta&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">(&lt;/span>&lt;span class="kt">float&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="n">dst&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">data&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">m&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">cudaDeviceSynchronize&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Передача данных (&lt;code>cudaMemcpy&lt;/code>) между памятью CUDA и памятью хоста (CPU) очень «тяжелая», поэтому критически важен дизайн, при котором все веса (тензоры весов) и промежуточные тензоры по возможности хранятся в VRAM (видеопамяти) на протяжении всего вывода.&lt;/p>
&lt;h3 id="73-бэкенд-apple-silicon-metal">7.3 Бэкенд Apple Silicon (Metal)
&lt;/h3>&lt;p>В последние годы процессоры Mac M1/M2/M3 (Apple Silicon) показывают превосходные результаты в качестве машин для вывода ИИ. Причина кроется в их «объединенной памяти» (Unified Memory). Поскольку CPU и GPU делят одну и ту же область памяти, высокозатратная передача данных между хостом и устройством по шине PCIe, как в случае с CUDA, описанным выше, становится совершенно не нужной.&lt;/p>
&lt;p>Для вызова Metal из C++ используется Objective-C++ (файлы &lt;code>.mm&lt;/code>) в качестве моста, либо библиотека &lt;code>metal-cpp&lt;/code>.
Мы описываем ядра с помощью Metal Compute Shader (написанных в файлах &lt;code>.metal&lt;/code> в стиле C++).&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-cpp" data-lang="cpp">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// Шейдер Metal (kernel.metal)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="cp">#include&lt;/span> &lt;span class="cpf">&amp;lt;metal_stdlib&amp;gt;&lt;/span>&lt;span class="cp">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">&lt;/span>&lt;span class="k">using&lt;/span> &lt;span class="k">namespace&lt;/span> &lt;span class="n">metal&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">kernel&lt;/span> &lt;span class="kt">void&lt;/span> &lt;span class="nf">mul_mat_kernel&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">device&lt;/span> &lt;span class="k">const&lt;/span> &lt;span class="kt">float&lt;/span>&lt;span class="o">*&lt;/span> &lt;span class="n">A&lt;/span> &lt;span class="p">[[&lt;/span>&lt;span class="n">buffer&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">)]],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">device&lt;/span> &lt;span class="k">const&lt;/span> &lt;span class="kt">float&lt;/span>&lt;span class="o">*&lt;/span> &lt;span class="n">B&lt;/span> &lt;span class="na">[[buffer(1)]]&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">device&lt;/span> &lt;span class="kt">float&lt;/span>&lt;span class="o">*&lt;/span> &lt;span class="n">C&lt;/span> &lt;span class="na">[[buffer(2)]]&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">constant&lt;/span> &lt;span class="n">uint3&lt;/span>&lt;span class="o">&amp;amp;&lt;/span> &lt;span class="n">dims&lt;/span> &lt;span class="na">[[buffer(3)]]&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">uint2&lt;/span> &lt;span class="n">gid&lt;/span> &lt;span class="na">[[thread_position_in_grid]]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">uint&lt;/span> &lt;span class="n">m&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">dims&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">x&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="n">uint&lt;/span> &lt;span class="n">k&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">dims&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">y&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="n">uint&lt;/span> &lt;span class="n">n&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">dims&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">z&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">uint&lt;/span> &lt;span class="n">row&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">gid&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">y&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="n">uint&lt;/span> &lt;span class="n">col&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">gid&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">x&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">row&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="n">m&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="n">col&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="n">n&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kt">float&lt;/span> &lt;span class="n">sum&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mf">0.0&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">uint&lt;/span> &lt;span class="n">i&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="n">i&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="n">k&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="o">++&lt;/span>&lt;span class="n">i&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">sum&lt;/span> &lt;span class="o">+=&lt;/span> &lt;span class="n">A&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">row&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="n">k&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">i&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="n">B&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">i&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="n">n&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">col&lt;/span>&lt;span class="p">];&lt;/span> &lt;span class="c1">// Упрощено
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">C&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">row&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="n">n&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">col&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">sum&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>В среде Apple Silicon также предоставляется библиотека оптимизации умножения матриц MPS (Metal Performance Shaders), поэтому при ее использовании в реальных задачах можно добиться потрясающей скорости вывода.&lt;/p>
&lt;hr>
&lt;h2 id="8-специфическая-обработка-для-моделей-transformer-attention-и-кэш-kv">8. Специфическая обработка для моделей Transformer: Attention и кэш KV
&lt;/h2>&lt;p>Передовые LLM, такие как LLaMA 2/3 и GPT, основаны на архитектуре Transformer. Для реализации этого на C++ необходимо создать «Scaled Dot-Product Attention», который описывается следующей формулой:&lt;/p>
$$
\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V
$$
&lt;p>Кроме того, при авторегрессионной (Autoregressive) генерации токенов необходимо сохранять результаты вычислений прошлых токенов (Key и Value). Это называется «&lt;strong>KV-кэшем (Key-Value Cache)&lt;/strong>».&lt;/p>
&lt;div class="mermaid">graph TD
T["Текущий токен"] --> Q["Запрос (Query)"]
T --> K["Ключ (Key)"]
T --> V["Значение (Value)"]
K --> KCache["Добавить в кэш KV"]
V --> VCache["Добавить в кэш KV"]
Q --> Dot1["Q * K_Cache^T"]
KCache --> Dot1
Dot1 --> Scale["Масштабирование (1/sqrt(d))"]
Scale --> Softmax["Softmax"]
Softmax --> Dot2["SoftmaxOut * V_Cache"]
VCache --> Dot2
Dot2 --> Out["Контекстный вектор"]&lt;/div>
&lt;p>Для KV-кэша также применяется стратегия предварительного выделения в арене пространства памяти, достаточного для максимальной длины контекста (например, 4096 или 8192 токена), и его использование по принципу кольцевого буфера. Это предотвращает необходимость перераспределения памяти на каждом шаге генерации.&lt;/p>
&lt;p>Кроме того, для позиционного кодирования (Positional Encoding) реализуется «RoPE (Rotary Position Embedding)», который стал мейнстримом в последние годы. Это метод встраивания информации о позиции в виде вектора вращения в комплексном пространстве, где ключом к производительности является оптимизация вызовов функций &lt;code>sin&lt;/code> и &lt;code>cos&lt;/code> в C++ (например, путем создания таблиц поиска).&lt;/p>
&lt;hr>
&lt;h2 id="9-экстремальная-оптимизация-посредством-квантования-моделей-quantization">9. Экстремальная оптимизация посредством квантования моделей (Quantization)
&lt;/h2>&lt;p>Если загрузить большую модель (например, модель LLaMA с 7 миллиардами параметров) в формате FP32 (32-битное число с плавающей запятой), то только веса займут около 28 ГБ памяти (VRAM). А если добавить к этому KV-кэш и буферы для вывода, объем легко превысит 30 ГБ, что делает выполнение на обычных потребительских видеокартах невозможным.&lt;/p>
&lt;p>Здесь на помощь приходит «&lt;strong>Квантование (Quantization)&lt;/strong>». Это истинная сила формата GGML.&lt;/p>
&lt;p>Квантование — это технология намеренного снижения точности весов.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>FP16 (16-бит)&lt;/strong>: Размер уменьшается вдвое. Потеря точности практически отсутствует.&lt;/li>
&lt;li>&lt;strong>INT8 (8-бит)&lt;/strong>: Размер уменьшается в 4 раза. Незначительная потеря точности.&lt;/li>
&lt;li>&lt;strong>INT4 (4-бит)&lt;/strong>: Размер уменьшается в 8 раз. Использование уникальных факторов блокировки и масштабирования позволяет осуществлять вывод с приемлемым качеством.&lt;/li>
&lt;/ul>
&lt;p>На стороне движка вывода сжатые в INT4 (или INT8) веса считываются из памяти и &lt;strong>развертываются (Dequantize) в FP16 или FP32 сразу после их загрузки в регистры CPU или GPU&lt;/strong>, после чего выполняются вычисления.&lt;/p>
&lt;p>Удивительно, но выгоднее уменьшить объем данных, считываемых из памяти, даже ценой увеличения количества вычислений. Это связано с тем, что в современном оборудовании узким местом при задачах вывода является не «вычислительная мощность (Compute Bound)», а «&lt;strong>пропускная способность памяти (Memory Bandwidth Bound)&lt;/strong>». Движок, реализованный на C++ с квантованием INT4, позволяет быстро запускать локальные LLM даже на MacBook Air с 8 ГБ объединенной памяти.&lt;/p>
&lt;hr>
&lt;h2 id="10-тюнинг-производительности-архитектура-numa-и-пул-потоков">10. Тюнинг производительности: Архитектура NUMA и пул потоков
&lt;/h2>&lt;p>При выполнении вывода с использованием CPU многопоточность обязательна. Однако простого запуска множества &lt;code>std::thread&lt;/code> будет недостаточно для достижения оптимальных результатов.&lt;/p>
&lt;p>Современные многопроцессорные серверы и высокопроизводительные процессоры, такие как Ryzen Threadripper, используют архитектуру &lt;strong>NUMA (Non-Uniform Memory Access)&lt;/strong>. Доступ ядра CPU к физически близкой к нему памяти (локальной памяти) осуществляется быстро, но доступ к памяти, привязанной к другому процессору, становится крайне медленным.&lt;/p>
&lt;p>В продвинутых движках вывода на C++ используются следующие техники:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Привязка потоков (Thread Pinning)&lt;/strong>: Закрепление каждого потока за определенным ядром CPU (установка Affinity) для предотвращения сброса кэша из-за переключения контекста.&lt;/li>
&lt;li>&lt;strong>NUMA-ориентированное выделение памяти&lt;/strong>: Выделение памяти на том же узле NUMA, где находится поток, обрабатывающий данные.&lt;/li>
&lt;li>&lt;strong>Пул потоков с кражей работы (Work-Stealing)&lt;/strong>: Реализация эффективного планировщика, который разбивает каждый узел графа вычислений на мелкие задачи, и свободные потоки автоматически «крадут» и выполняют эти задачи.&lt;/li>
&lt;/ol>
&lt;p>Полное использование этих методов позволяет удерживать загрузку CPU близко к 100% и достигать пропускной способности, близкой к теоретическому максимуму.&lt;/p>
&lt;hr>
&lt;h2 id="11-заключение-удовольствие-от-работы-с-ии-на-мускулах-c">11. Заключение: Удовольствие от работы с ИИ на «мускулах» C++
&lt;/h2>&lt;p>Python, безусловно, удобен. В области исследований и разработки, а также создания прототипов нет языка, способного сравниться с ним в продуктивности. Однако, как только вы переходите на этап, когда готовую модель необходимо «эффективно запускать в реальном мире на любых устройствах», наступает время для C++.&lt;/p>
&lt;p>Когда вы видите, как движок вывода, созданный путем прямого манипулирования массивами байтов в памяти, максимального использования регистров через SIMD-инструкции и борьбы за пропускную способность VRAM графического процессора, один за другим генерирует на консоли токены естественного текста — вы испытываете чувство глубокого удовлетворения, «чистую радость инженера», которую невозможно получить, просто вызвав &lt;code>model.generate()&lt;/code> во фреймворке Python.&lt;/p>
&lt;p>Технологии ИИ часто кажутся «черным ящиком», но, написав все своими руками на C++, от тензорных операций до выделения памяти, вы сможете глубоко понять истинные механизмы того, как «мыслит» LLM.&lt;/p>
&lt;p>Если вы обладаете базовыми знаниями C++ и глубоко интересуетесь современными технологиями ИИ, обязательно попробуйте создать свой собственный движок вывода. Исходный код GGML и llama.cpp станет для вас лучшим живым учебником.&lt;/p>
&lt;p>&lt;strong>Итак, отбросьте тяжелую среду выполнения Python и позвольте передовому ИИ работать на мощных мускулах C++!&lt;/strong>&lt;/p></description></item><item><title>Разработка небольших ИИ-моделей (таких как TinyLLaMA) на C++</title><link>http://kenji.blog/ru/p/cpp-small-ai-model-tinyllama-dev-guide/</link><pubDate>Fri, 11 Sep 2026 14:00:00 +0900</pubDate><guid>http://kenji.blog/ru/p/cpp-small-ai-model-tinyllama-dev-guide/</guid><description>&lt;img src="http://kenji.blog/p/cpp-small-ai-model-tinyllama-dev-guide/img/eyecatch.jpg" alt="Featured image of post Разработка небольших ИИ-моделей (таких как TinyLLaMA) на C++" />&lt;h1 id="руководство-по-разработке-небольших-ии-моделей-таких-как-tinyllama-на-c">Руководство по разработке небольших ИИ-моделей (таких как TinyLLaMA) на C++
&lt;/h1>&lt;p>В последние годы стремительно растет интерес к запуску больших языковых моделей (LLM) в локальных средах. В частности, небольшие модели, такие как TinyLLaMA (1,1 млрд параметров), могут осуществлять вывод с практической скоростью даже на периферийных устройствах с ограниченными ресурсами и обычных ноутбуках (включая среды Windows). В то время как разработка с использованием Python и PyTorch является мейнстримом, для достижения максимальной производительности и экономии памяти де-факто стандартом становится комбинация C++ и основанной на C тензорной библиотеки &amp;ldquo;ggml&amp;rdquo;.&lt;/p>
&lt;p>В этой статье будет представлено очень подробное пошаговое руководство по разработке для создания механизма вывода с нуля (или глубокого понимания внутренней структуры существующего llama.cpp) для загрузки TinyLLaMA и генерации текста с использованием C++.&lt;/p>
&lt;hr>
&lt;h2 id="1-почему-c-и-ggml">1. Почему C++ и ggml?
&lt;/h2>&lt;p>На этапе обучения ИИ Python имеет подавляющее преимущество благодаря своей гибкости и богатой экосистеме. Однако на этапах развертывания и &amp;ldquo;вывода&amp;rdquo; (Inference) C++ становится мощным выбором по следующим причинам:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Снижение накладных расходов&lt;/strong>: Можно полностью исключить глобальную блокировку интерпретатора (GIL) и накладные расходы времени выполнения Python.&lt;/li>
&lt;li>&lt;strong>Эффективность памяти и выделение арены&lt;/strong>: Поскольку выделение и освобождение памяти можно контролировать вручную, предотвращаются непредсказуемые всплески, вызванные сборкой мусора.&lt;/li>
&lt;li>&lt;strong>Прямой доступ к оборудованию&lt;/strong>: Можно напрямую вызывать встроенные функции SIMD (Intrinsics), такие как AVX-512, AVX2, ARM NEON, чтобы максимально использовать вычислительную мощность процессора.&lt;/li>
&lt;li>&lt;strong>Отсутствие зависимостей&lt;/strong>: ggml — это библиотека C/C++ с нулевыми зависимостями (Zero dependencies), которую можно легко собрать даже в среде MSVC на Windows, если есть компилятор.&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="2-общий-обзор-архитектуры">2. Общий обзор архитектуры
&lt;/h2>&lt;p>Общий поток конвейера вывода показан на диаграмме Mermaid ниже. Это серия процессов, начиная с введенного пользователем текста и заканчивая генерацией следующего токена.&lt;/p>
&lt;div class="mermaid">graph TD
A["Введенный пользователем текст"] --> B["Токенизатор BPE"]
B --> C["Массив ID токенов"]
C --> D["Поиск в слое Embedding"]
D --> E["Блоки Transformer"]
E --> F["RMSNorm"]
F --> G["Слой LM Head"]
G --> H["Массив логитов"]
H --> I["Модуль Sampler"]
I --> J["ID следующего токена"]
J --> K["Детокенизатор"]
K --> L["Фрагмент выходного текста"]
J -.-> |"Добавить в контекст"| C&lt;/div>
&lt;p>Поскольку это авторегрессионная модель, выведенный токен снова добавляется в контекст и циркулирует как входные данные для прогнозирования следующего токена (пунктирная линия на диаграмме).&lt;/p>
&lt;hr>
&lt;h2 id="3-формат-модели-и-отображение-памяти-mmap">3. Формат модели и отображение памяти (mmap)
&lt;/h2>&lt;p>Самым большим препятствием при работе с весами гигантских нейронных сетей являются дисковый ввод-вывод и потребление памяти. В реализации на C++ это решается с помощью &lt;strong>отображения памяти (mmap)&lt;/strong>.&lt;/p>
&lt;h3 id="31-механизм-отображения-памяти-и-реализация-в-windows">3.1 Механизм отображения памяти и реализация в Windows
&lt;/h3>&lt;p>С помощью mmap содержимое файла можно напрямую отобразить в виртуальное адресное пространство процесса.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Zero-copy (Нулевое копирование)&lt;/strong>: Данные считываются непосредственно с диска в страничный кэш ядра, и лишнее копирование в пользовательское пространство не происходит.&lt;/li>
&lt;li>&lt;strong>Загрузка по требованию (Page Fault)&lt;/strong>: В тот момент, когда процессор фактически обращается к этому адресу памяти, происходит ошибка страницы (page fault), и в физическую память загружается только необходимый фрагмент (обычно 4 КБ).&lt;/li>
&lt;/ul>
&lt;p>В среде Windows вместо POSIX &lt;code>mmap&lt;/code> используются API Win32 &lt;code>CreateFileMapping&lt;/code> и &lt;code>MapViewOfFile&lt;/code>.&lt;/p>
&lt;div class="mermaid">sequenceDiagram
participant OS["ОС Windows"]
participant RAM["Физическая память"]
participant App["Приложение C++"]
App->>OS: "CreateFileMapping / MapViewOfFile"
OS-->>App: "Указатель адреса виртуальной памяти"
App->>App: "Чтение тензорных данных по указателю"
OS->>RAM: "Ошибка страницы / Загрузка страницы с диска"
RAM-->>App: "Данные готовы для вычислений SIMD"&lt;/div>
&lt;h3 id="32-бинарная-структура-формата-gguf">3.2 Бинарная структура формата GGUF
&lt;/h3>&lt;p>&lt;strong>GGUF (GPT-Generated Unified Format)&lt;/strong>, преобразованный из таких форматов, как &lt;code>.safetensors&lt;/code> на Hugging Face, является идеальным форматом для вывода. Он имеет строгую бинарную структуру, как показано ниже:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Magic Bytes&lt;/strong>: &lt;code>0x46554747&lt;/code> (GGUF).&lt;/li>
&lt;li>&lt;strong>Version&lt;/strong>: Номер версии формата.&lt;/li>
&lt;li>&lt;strong>Tensor Count &amp;amp; Metadata Count&lt;/strong>: Количество тензоров и количество пар ключ-значение метаданных.&lt;/li>
&lt;li>&lt;strong>Metadata (Key-Value Pairs)&lt;/strong>: Ключи с префиксом длины строки и типизированные значения.&lt;/li>
&lt;li>&lt;strong>Tensor Info&lt;/strong>: Имя каждого тензора, количество измерений, тип данных (FP16, Q4_K и т.д.) и позиция смещения в файле.&lt;/li>
&lt;li>&lt;strong>Padding&lt;/strong>: Заполнение (padding), вставляемое для того, чтобы тензорные данные были выровнены по определенной границе (обычно 32 или 64 байта). Это важно для быстрого доступа к памяти с помощью инструкций SIMD (особенно AVX).&lt;/li>
&lt;li>&lt;strong>Tensor Data&lt;/strong>: Массив выровненных фактических данных весов.&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="4-математические-основы-tinyllama-и-алгоритмы-c">4. Математические основы TinyLLaMA и алгоритмы C++
&lt;/h2>&lt;p>TinyLLaMA включает в себя несколько продвинутых архитектурных хитростей для повышения эффективности. Мы объясним математические выражения, необходимые для их правильной реализации на C++.&lt;/p>
&lt;h3 id="41-rmsnorm-нормализация-среднеквадратичного-значения">4.1 RMSNorm (Нормализация среднеквадратичного значения)
&lt;/h3>&lt;p>Затраты на вычисления снижаются за счет отказа от центрирования по среднему значению из LayerNorm и выполнения только масштабирования дисперсии.&lt;/p>
$$ \text{RMSNorm}(x) = \frac{x}{\sqrt{\frac{1}{d}\sum_{i=1}^{d} x_i^2 + \epsilon}} \odot \gamma $$
&lt;p>Где $d$ — количество измерений, а $\gamma$ — обученный тензор масштабирования.
При реализации на C++ сначала быстро вычисляется сумма квадратов массива с помощью &lt;code>_mm256_fmadd_ps&lt;/code> из AVX2 и оптимизируется путем умножения на обратный квадратный корень (например, инструкция &lt;code>_mm256_rsqrt_ps&lt;/code>).&lt;/p>
&lt;h3 id="42-rope-rotary-position-embedding">4.2 RoPE (Rotary Position Embedding)
&lt;/h3>&lt;p>Это метод, который применяет информацию о позиции токена в виде вращения в тензорном пространстве. Его можно рассматривать как вращение на комплексной плоскости, и к соседней паре измерений $(x_1, x_2)$ вектора $x$ применяется следующее вращение:&lt;/p>
$$ \text{RoPE}(x, m) = \begin{pmatrix} x_{1} \cos(m\theta) - x_{2} \sin(m\theta) \\ x_{1} \sin(m\theta) + x_{2} \cos(m\theta) \end{pmatrix} $$
&lt;p>Где $m$ — абсолютный индекс позиции токена, а $\theta$ — предварительно вычисленная базовая частота. В ggml это выполняется параллельно просто путем добавления оператора &lt;code>ggml_rope&lt;/code> во время построения графа вывода.&lt;/p>
&lt;h3 id="43-grouped-query-attention-gqa">4.3 Grouped-Query Attention (GQA)
&lt;/h3>&lt;p>В обычном Multi-Head Attention (MHA) количество голов для Query, Key и Value одинаково. Однако TinyLLaMA использует &lt;strong>Grouped-Query Attention (GQA)&lt;/strong>, чтобы радикально снизить пропускную способность памяти и потребление кэша KV.&lt;/p>
$$ \text{Attention}(Q, K, V) = \text{softmax}\left(\frac{Q K^T}{\sqrt{d_k}}\right) V $$
&lt;p>В GQA несколько голов Query совместно используют одну голову Key/Value. В реализации C++ перед выполнением матричного умножения &lt;code>ggml_mul_mat&lt;/code> требуется операция широковещательной рассылки (broadcast) тензора KV в соответствии с количеством Query.&lt;/p>
&lt;h3 id="44-функция-активации-swiglu">4.4 Функция активации SwiGLU
&lt;/h3>&lt;p>В слоях Feed-Forward Network (FFN) вместо GELU используется SwiGLU.&lt;/p>
$$ \text{SwiGLU}(x) = \text{Swish}(x W_{\text{gate}}) \otimes (x W_{\text{up}}) $$
$$ \text{Swish}(z) = z \cdot \sigma(z) = z \cdot \frac{1}{1 + e^{-z}} $$
&lt;p>В вычислительном графе это выражается путем объединения оператора &lt;code>ggml_silu&lt;/code> и &lt;code>ggml_mul&lt;/code>.&lt;/p>
&lt;hr>
&lt;h2 id="5-построение-вычислительного-графа-и-управление-памятью-с-помощью-ggml">5. Построение вычислительного графа и управление памятью с помощью ggml
&lt;/h2>&lt;p>ggml использует подход &amp;ldquo;Define-and-Run&amp;rdquo; (определи и запусти), при котором строится статический вычислительный граф для вывода, а затем вычисляется (evaluate).&lt;/p>
&lt;h3 id="51-ggml_context-и-распределитель-арены-arena-allocator">5.1 ggml_context и распределитель арены (Arena Allocator)
&lt;/h3>&lt;p>Самая уникальная особенность ggml — это &amp;ldquo;выделение арены&amp;rdquo; (arena allocation), которое вообще не выполняет динамическое выделение памяти (&lt;code>malloc&lt;/code> или &lt;code>new&lt;/code>) внутри цикла вывода.
При инициализации выделяется огромная непрерывная область памяти (арена), и указатель этой области увеличивается (increment) каждый раз при вызове &lt;code>ggml_new_tensor&lt;/code> и т.д. Когда один шаг вывода завершен, достаточно сбросить указатель выделения в исходное положение, и выделение памяти для следующего шага вывода мгновенно завершается.&lt;/p>
&lt;h3 id="52-конкретный-пример-построения-графа">5.2 Конкретный пример построения графа
&lt;/h3>&lt;p>Для каждого шага вывода в памяти собирается следующий вычислительный граф.&lt;/p>
&lt;div class="mermaid">graph TD
A["Входные ID токенов"] --> B["Поиск Embed"]
B --> C["ggml_rms_norm"]
C --> D["Проекции Q / K / V"]
D --> E["Позиционное ggml_rope"]
E --> F["Сохранение в кэш KV"]
E --> G["Загрузка из кэша KV"]
G --> H["Self Attention"]
H --> I["Масштабирование и Softmax"]
I --> J["Вывод Attention"]
J --> K["Выходная проекция"]
K --> L["Добавление Residual"]&lt;/div>
&lt;hr>
&lt;h2 id="6-квантование-quantization-и-оптимизация-windows--simd">6. Квантование (Quantization) и оптимизация Windows / SIMD
&lt;/h2>&lt;p>Если обрабатывать TinyLLaMA (1.1B) в формате FP16, потребуется около 2,2 ГБ памяти, но с помощью 4-битного квантования (например, Q4_K) её можно радикально сжать примерно до 600 МБ.&lt;/p>
&lt;h3 id="61-архитектура-блочного-квантования">6.1 Архитектура блочного квантования
&lt;/h3>&lt;p>ggml не квантует весь тензор единообразно, а делает это по «блокам».
В формате &lt;code>Q4_0&lt;/code> 32 значения FP16 объединяются в один блок.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Масштабный коэффициент (Scale factor)&lt;/strong>: 1 значение FP16 (2 байта)&lt;/li>
&lt;li>&lt;strong>Квантованные данные&lt;/strong>: 32 4-битных значения (16 байт)
Это сводит к минимуму влияние локальных выбросов.&lt;/li>
&lt;/ul>
&lt;h3 id="62-ускорение-скалярного-произведения-с-помощью-avx2">6.2 Ускорение скалярного произведения с помощью AVX2
&lt;/h3>&lt;p>При сборке для новейших процессоров x86 в среде Windows использование флагов компилятора, таких как &lt;code>/arch:AVX2&lt;/code>, обеспечивает обработку SIMD в следующем потоке:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Загрузка&lt;/strong>: Загрузка 4-битных квантованных данных из памяти в 256-битный регистр AVX.&lt;/li>
&lt;li>&lt;strong>Распаковка&lt;/strong>: Развертывание 4-битных значений в Int8 или Int16 с помощью битовых масок и операций сдвига.&lt;/li>
&lt;li>&lt;strong>Деквантование (Обратное квантование)&lt;/strong>: Умножение на масштабный коэффициент для преобразования в число с плавающей запятой.&lt;/li>
&lt;li>&lt;strong>Операция FMA&lt;/strong>: Параллельное выполнение операций умножения с накоплением (multiply-add) со значениями активации с помощью &lt;code>_mm256_fmadd_ps&lt;/code> (Fused Multiply-Add).&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="7-детали-реализации-кэша-kv">7. Детали реализации кэша KV
&lt;/h2>&lt;p>В авторегрессионной генерации «кэш KV» является важной функцией для пропуска вычисления Key и Value для прошлых токенов.&lt;/p>
&lt;p>Ключевые моменты при реализации на C++:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Предварительное выделение тензора&lt;/strong>: Инициализируйте огромный тензор для кэша KV на максимальную длину контекста (например, 2048 токенов) (рекомендуется FP16).&lt;/li>
&lt;li>&lt;strong>Копирование со смещением&lt;/strong>: Когда выполняются вычисления для позиции токена $N$, векторы K и V, полученные на этом шаге, сохраняются в $N$-й строке тензора кэша KV с помощью &lt;code>ggml_cpy&lt;/code> и т.д.&lt;/li>
&lt;li>&lt;strong>Создание представления (view) во время Attention&lt;/strong>: При вычислении Attention создается «представление» (view), указывающее только на часть токенов от 0 до $N$, которое передается в матричное умножение.&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="8-токенизатор-bpe-и-декодирование">8. Токенизатор BPE и декодирование
&lt;/h2>&lt;p>Входная строка обрабатывается как последовательность байтов UTF-8 и сравнивается с заранее определенным словарем. В C++ для ускорения поиска по словарю реализуются алгоритмы с использованием &lt;strong>Trie-дерева (префиксного дерева)&lt;/strong> или очереди с приоритетом.&lt;/p>
&lt;p>Из логитов (logits), выводимых из LM Head, вероятности масштабируются с использованием параметра Temperature, кандидаты сужаются с помощью методов Top-K или Top-P (Nucleus Sampling), а затем случайные числа используются для определения конечного следующего токена.&lt;/p>
&lt;hr>
&lt;h2 id="9-настройка-проекта-c-среда-windows--powershell">9. Настройка проекта C++ (среда Windows / PowerShell)
&lt;/h2>&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-cmake" data-lang="cmake">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cmake_minimum_required&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">VERSION&lt;/span> &lt;span class="s">3.14&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="nb">project&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">TinyLLaMACpp&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="nb">set&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">CMAKE_CXX_STANDARD&lt;/span> &lt;span class="s">17&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># Настройки оптимизации и флага AVX2 для Windows (MSVC)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c">&lt;/span>&lt;span class="nb">if&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">MSVC&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span> &lt;span class="nb">add_compile_options&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">/O2&lt;/span> &lt;span class="s">/arch:AVX2&lt;/span> &lt;span class="s">/fp:fast&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span> &lt;span class="nb">add_link_options&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">/STACK:8388608&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="nb">else&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span> &lt;span class="nb">add_compile_options&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">-O3&lt;/span> &lt;span class="s">-march=native&lt;/span> &lt;span class="s">-ffast-math&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="nb">endif&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="nb">add_library&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">ggml&lt;/span> &lt;span class="s">OBJECT&lt;/span> &lt;span class="s">ggml/ggml.c&lt;/span> &lt;span class="s">ggml/ggml-alloc.c&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="nb">target_compile_definitions&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">ggml&lt;/span> &lt;span class="s">PRIVATE&lt;/span> &lt;span class="s">GGML_USE_AVX2&lt;/span> &lt;span class="s">GGML_USE_F16C&lt;/span> &lt;span class="s">GGML_USE_FMA&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="nb">add_executable&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">main&lt;/span> &lt;span class="s">main.cpp&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="nb">target_link_libraries&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">main&lt;/span> &lt;span class="s">ggml&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Пример команды сборки в PowerShell:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-powershell" data-lang="powershell">&lt;span class="line">&lt;span class="cl">&lt;span class="n">mkdir&lt;/span> &lt;span class="n">build&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd &lt;/span>&lt;span class="n">build&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">cmake&lt;/span> &lt;span class="p">..&lt;/span> &lt;span class="n">-G&lt;/span> &lt;span class="s2">&amp;#34;Visual Studio 17 2022&amp;#34;&lt;/span> &lt;span class="n">-A&lt;/span> &lt;span class="n">x64&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">cmake&lt;/span> &lt;span class="p">-&lt;/span>&lt;span class="n">-build&lt;/span> &lt;span class="p">.&lt;/span> &lt;span class="p">-&lt;/span>&lt;span class="n">-config&lt;/span> &lt;span class="n">Release&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;hr>
&lt;h2 id="10-заключение">10. Заключение
&lt;/h2>&lt;p>Реализация механизма вывода для небольших ИИ-моделей, таких как TinyLLaMA, с нуля с использованием C++ и ggml — это отличная возможность раскрыть черный ящик глубокого обучения и оценить красоту низкоуровневого управления аппаратным обеспечением. Давайте прокладывать путь в будущее периферийного ИИ (Edge AI), в полной мере наслаждаясь сутью системного программирования, такой как загрузка без копирования с использованием отображения памяти, оптимизация SIMD и создание кэша KV.&lt;/p></description></item></channel></rss>