Dev48
ЯЗЫК
  • О нас
  • Услуги
  • Индустрии
  • Технологии
  • Статьи
  • Контакты
Забронировать звонок
    Главная/Статьи/Pochemu my pishem sobstvennye dvizhki na c i c
Dev48

© 2026 · All rights reserved.

Почему мы пишем собственные движки на C и C++

Источник: LocalAI

Почему мы пишем собственные движки на C и C++

Источник: LocalAI

LocalAI — это движок ИИ с открытым исходным кодом. Запускайте любые модели: LLM, зрение, голос, изображения и видео на любом оборудовании. GPU не требуется.

25 сентября 2026 г.

Большинство бэкендов LocalAI являются обертками для чужих движков. llama.cpp, vLLM, whisper.cpp, stable-diffusion и MLX поддерживаются людьми, которые работают над этими моделями на постоянной основе, и создание обертки для них стоит нам лишь Dockerfile и gRPC-прослойки. Мы делаем это везде, где можем.

Восемнадцать наших бэкендов не используют сторонние обертки. Это порты на C или C++, которые мы написали с нуля, и каждый из них существует потому, что использование исходного движка означало бы необходимость поставлять то, что мы не можем себе позволить: многогигабайтную установку Python, стек только для CUDA, который не запустится на половине машин наших пользователей, или, в некоторых случаях, модель, для которой изначально не существует реализации на C++.

vllm.cpp: 66 МБ вместо 9,1 ГБ

Развертывание стека вывода на Python означает разрешение дерева зависимостей во время установки на целевой машине, с учетом версий CUDA и glibc, установленных на ней. Развертывание порта ggml означает копирование общей библиотеки и файла GGUF.

vllm.cpp — это наш порт архитектуры обслуживания V1 от vLLM на C++20. Установка vLLM создает виртуальное окружение размером 9,1 ГБ. Установка vllm.cpp создает бинарный файл размером 66 МБ. Он реализует те же функции, что и оригинал на Python, включая постраничный KV-кэш, непрерывную пакетную обработку, кэширование префиксов, планировщик и сэмплер, без использования Python, PyTorch и ggml при выводе.

Вопрос в том, как это влияет на пропускную способность. На NVIDIA GB10 при запуске Qwen3.6-27B в формате NVFP4, жадном поиске, замкнутом цикле, в сравнении с vLLM в его производственной конфигурации с графами, а не --enforce-eager:

Это ничья. Наш уровень шума от запуска к запуску составляет 0,5%, а при параллелизме от 2 до 32 — от 0,7% до 1,7%, поэтому эти пять пунктов находятся внутри диапазона шума или достаточно близки к нему, чтобы не иметь значения. Только случай с одним потоком, с показателем 4,5%, явно выходит за рамки. Вывод идентичен vLLM по каждому токену в любой точке кривой, а пиковое использование памяти хоста составляет 24,88 ГБ против 28,18 ГБ.

Размер установки сокращается с 9,1 ГБ до 66 МБ, а пропускная способность остается прежней, чего мы и добивались.

В сравнении с llama.cpp на CPU из того же файла GGUF, префилл работает в 1,18 раза быстрее (223,8 против 177,3 ток/с), декодирование — ничья в пределах собственного разброса llama.cpp, а токены точно соответствуют его жадному декодированию. В сравнении с MLX-LM на Apple M4, время до первого токена при префилле на 1,5% лучше, а общая пропускная способность в прогретом состоянии составляет 97,6% от MLX-LM — реальный разрыв в 2,4%, который полностью приходится на декодирование.

depth-anything.cpp быстрее на CPU

depth-anything.cpp — это порт Depth Anything 3 от ByteDance, который позволяет получить метрическую глубину в метрах из одной обычной фотографии, а также уверенность для каждого пикселя, внутренние и внешние параметры камеры и облако точек с обратной проекцией. На CPU он работает быстрее, чем PyTorch на той же модели.

Это на процессоре Ryzen 9 9950X3D при разрешении 504x336 с 16 потоками. Сборка на C++ выполняет ту же модель в 1,31 раза быстрее, использует 363 МБ оперативной памяти против 1328 МБ и загружается за 40 мс вместо 749 мс. Квантованная сборка q4_k представляет собой файл размером 99 МБ и остается практически без потерь качества. Вывод коррелирует с эталонным проходом 1.0 по результатам 37 тестов на соответствие.

Мы не написали ядро матричного умножения лучше, чем в PyTorch. Два позиционных эмбеддинга — UV-эмбеддинг головы DPT и бикубический позиционный эмбеддинг бэкбона — пересчитывались при каждом прямом проходе с помощью однопоточных скалярных циклов sin, cos и бикубической интерполяции, хотя они зависят только от геометрии входных данных и идентичны при каждом вызове. Их кэширование позволило устранить около 95 мс накладных расходов на стороне хоста за проход, что составляет большую часть разрыва. PyTorch строит те же эмбеддинги с помощью векторизованных операций и изначально не имел таких накладных расходов.

Тяжелые операции GEMM примерно равны, потому что все обращаются к одному и тому же классу ядер BLAS. Остается работа на стороне хоста, которую эталонная реализация на Python не удосужилась оптимизировать, плюс отсутствие необходимости загружать интерпретатор и фреймворк для вывода. На GPU ситуация возвращается к паритету: с бэкендом ggml CUDA и flash attention на GB10, depth-anything.cpp сравнивается с оптимизированным cuDNN от PyTorch на 47,3 мс за проход и выигрывает только в «холодном» старте, загружаясь в 1,75–2,9 раза быстрее.

Два случая, где мы медленнее

face-detect.cpp и voice-detect.cpp заменили бэкенды LocalAI на Python — insightface и распознавание речи. Ни один из них не работает быстрее того, что он заменил на CPU, но мы все равно их выпустили.

face-detect.cpp запускает всю цепочку insightface buffalo: детектирование SCRFD, выравнивание по пяти ориентирам с помощью аффинного преобразования до 112x112 и эмбеддинг ArcFace из одного автономного GGUF без Python и onnxruntime. Рамки детектора и ориентиры совпадают с insightface с точностью до 1 пикселя, а косинус эмбеддинга распознавания равен 1.000000 при любом количестве потоков. На CPU он медленнее, чем onnxruntime: детектирование SCRFD работает примерно на 0,83x при одном потоке и 0,69x при восьми, эмбеддинг ArcFace — примерно на 0,61x и 0,84x. Ядра свертки MLAS в onnxruntime находятся на пике производительности FMA, а пользовательский путь AVX2 Winograd сократил разрыв, но не закрыл его. На GPU перенаправление тех же сверток через cuDNN сокращает время SCRFD с 14,8 мс до 6,4 мс, что приводит к паритету с torch-cuDNN.

У voice-detect.cpp вместо этого есть результат по памяти. Верификация WeSpeaker достигает пика около 62 МБ в нашем бинарном файле против примерно 334 МБ для пути на Python, torch и onnxruntime только для CPU, что примерно в 5,4 раза меньше, при идентичном вердикте и косинусе эмбеддинга 1.000000. В целом на CPU они показывают результаты в пределах 10–15% друг от друга, меняясь лидерством в зависимости от модели и количества потоков, а на GPU сверточные энкодеры соответствуют эталону.

Для биометрического конвейера мы предпочитаем точное совпадение скорости. Эмбеддинг, который отличается в четвертом десятичном знаке, меняет решения о верификации при пороговом значении, и каждый зарегистрированный шаблон в развертывании пришлось бы пересчитывать. Точное соответствие insightface позволяет заменить бэкенд без повторной регистрации пользователей.

Как мы это делаем

Каждый порт проходит через четыре одинаковых этапа.

Сначала конвертируем веса в один GGUF вместе с токенизатором, словарем и любой встроенной вспомогательной моделью, чтобы развертывание модели сводилось к копированию файла.

Затем портируем граф и проверяем его покомпонентно относительно эталонных тензоров, выгруженных из оригинальной реализации. depth-anything.cpp имеет 37 тестов ctest, охватывающих предобработку, бэкбон, внимание, голову DPT, глубину, позу, голову лучей, решатель лучей и экспортеры. parakeet.cpp проверяет соответствие транскрипции с NeMo с WER 0. face-detect.cpp проверяет расстояние до рамок и ориентиров в пикселях, а также косинус эмбеддинга. Пропустите этот шаг, и вы узнаете, что порт неверен, спустя месяцы, от пользователя, по модели, о которой вы уже забыли.

В-третьих, оптимизируем с помощью профилировщика, и только после того, как пройдены проверки на паритет. В parakeet.cpp выигрыш дало кэширование прямого прохода LSTM сети предсказаний, который составлял 97% времени декодирования преобразователя и был по большей части избыточным. В depth-anything.cpp это были два позиционных эмбеддинга, упомянутых выше. Ни то, ни другое не было переписыванием ядра, и ни то, ни другое не обнаружилось бы без работающей базовой линии для профилирования.

В последнюю очередь обеспечьте плоский C ABI. LocalAI открывает общую библиотеку через purego и вызывает этот ABI напрямую, поэтому в пути обслуживания нет подпроцессов, нет переходов gRPC к серверу Python и нет интерпретатора.

Что требуется для поддержки

Каждый движок — это отдельный репозиторий со своей CI, набором тестов, скриптом конвертации GGUF и базовыми показателями паритета, а разработчики вышестоящих проектов продолжают выпускать контрольные точки, требующие доработки конвертера.

Ядра GPU — слабое место. Универсальные ядра свертки и внимания CUDA в ggml отстают от оптимизированных cuDNN от NVIDIA в моделях с интенсивным использованием сверток, поэтому face-detect.cpp нуждается в явном пути cuDNN для достижения паритета, и именно поэтому преимущество parakeet.cpp на GPU по сравнению с NeMo составляет в среднем 1,25 раза, в то время как на CPU этот разрыв больше.

Кроме того, это решение не масштабируется на всё. llama.cpp, vLLM, whisper.cpp, MLX и diffusers остаются обернутыми, потому что эти проекты крупные, быстро развивающиеся и уже хорошо справляются со своими задачами. Мы пишем движок, когда у модели нет реализации на C++, когда зависимость от Python тяжелее самой модели или когда того, что нам нужно, еще не существует. Остальное мы устанавливаем, как и все остальные.

Одна вещь, которая сбивает с толку людей, впервые читающих дерево каталогов: ядро LocalAI написано на Go, а каждый бэкенд написан на том, что требуется экосистеме его модели, поэтому C++ соседствует с Python в одном и том же репозитории.

Каждый движок выше хранит свой набор тестов, проверки на паритет и методологию в своем собственном репозитории, включая те запуски, которые не дали результата. Полный список находится в таблице «Бэкенды, созданные нами» в файле README проекта LocalAI.

← Все статьи