Большинство бэкендов 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 заменили бэкенды insightface и распознавания речи Python в LocalAI. Ни один из них не работает быстрее того, что он заменил на CPU, и мы все равно выпустили их.
face-detect.cpp запускает всю цепочку insightface buffalo, то есть детектирование SCRFD, выравнивание по пяти ориентирам с помощью преобразования сходства до 112x112 и встраивание ArcFace, из одного самодостаточного GGUF без Python и без onnxruntime. Рамки детектора и ориентиры совпадают с insightface с точностью до 1 пикселя, а встраивание распознавания совпадает с косинусом 1.000000 при любом количестве потоков. На CPU он работает медленнее, чем onnxruntime: детектирование SCRFD выполняется примерно с коэффициентом 0.83 на одном потоке и 0.69 на восьми, а встраивание ArcFace — примерно с 0.61 и 0.84. Свёрточные ядра 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 — слабое место. Общие сверточные ядра и ядра внимания в ggml уступают оптимизированным cuDNN от NVIDIA на моделях с интенсивным использованием свертки, поэтому face-detect.cpp требует явного пути cuDNN для достижения паритета, и именно поэтому превосходство parakeet.cpp над NeMo на GPU составляет в среднем 1,25x, в то время как на CPU этот разрыв больше.
Оно также не масштабируется для всего. llama.cpp, vLLM, whisper.cpp, MLX и diffusers остаются обернутыми, поскольку эти проекты масштабны, быстро развиваются и уже хороши в своем деле. Мы пишем движок, когда у модели нет реализации на C++, когда зависимость от Python тяжелее самой модели или когда нужная нам вещь еще не существует. Остальное мы устанавливаем, как и все остальные.
Одна вещь сбивает с толку людей, впервые изучающих дерево исходного кода: ядро самого LocalAI написано на Go, а каждый бэкенд написан на том, что требуется экосистеме его модели, поэтому в одном репозитории рядом с Python находится C++.
Каждый из указанных выше движков хранит свой набор тестов производительности, проверки соответствия и методологию в собственном репозитории, включая неудачные запуски. Полный список приведен в таблице «Бэкенды, созданные нами» в README LocalAI.







