The OpenNET Project / Index page

[ новости /+++ | форум | теги | ]

Ускорение пересборки llama.cpp
При работе с llama.cpp имеется постоянная необходимость её пересобирать, так
как в отличие от ONNX Runtime GGUF-файлы не хранят сериализованный граф
вычислений, вместо этого процедура инференса вручную кодится в C++-коде, и за
счёт применения информации, которую в ONNX обычно не сериализуют (ONNX обычно
экспортируется автоматически, но знания можно туда встроить, если закодировать
конструирование ONNX-графа вручную), может быть достигнута большая
эффективность (по потреблению ресурсов) инференса.

Это приводит к тому, что для того, чтобы исполнять модель на llama.cpp
необходимы усилия программистов. В большинстве случаев каждая новая модель
обладает уникальной архитектурой, и в большинстве случаев её поддержка кодится
в проект сотрудниками компании-разработчика самой модели, если компания
нуждается в продвижении своих моделей (зачастую такие модели распространяются
под проприетарной лицензией, запрещающей коммерческое использование без
заключения договора, при этом они закрывают некоторые потребности тех, кому
нужен экономичный к ресурсам инференс).

Так как структура проекта не подразумевает динамической загрузки отдельно
собираемой разделяемой библиотеки с архитектурой модели, то при добавлении
моделей или возможностей приходится пересобирать весь проект. Это обосновано,
так как архитектура проекта и его API не являются стабильными и код одной
реализации модели переиспользуется в другой.

Однако проект имеет некоторые проблемы. Которые, впрочем, "никому" "не мешают",
так как нейросети (стереотипно "тяжёлое" приложение, не смотря на то, что
существуют мелкие нейросети размером в несколько мебибайт) запускать на
"калькуляторе", "обогревателе" и "музейном хламе со свалки" никто "в здравом
уме" (со слов "здравомыслящих") не будет. Проблемы заключаются в том, что
проект долго компилируется и долго линкуется, и при этом имеет тенденцию
требовать пересборки и перелинковки при малейших вроде-бы безобидных
изменениях, вроде изменения переменной "CMAKE_INSTALL_LIBDIR". Я
проанализировал и исправил некоторые, но не все, причины подобного поведения.
Причины заключаются в том, что в проекте использованы приёмы, являющиеся
антипаттернами, которые было бы неплохо разобрать, чтобы вы так не делали.

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

2. Проект имеет в нескольких местах "add_library(${TARGET} STATIC". Это
приводит к тому, что некоторые куски кода линкуются статически в некоторые
другие куски кода. Это, конечно, должно приводить к более быстрому коду при
линковке с LTO, но линковка с LTO тот ещё тормоз, а инструментов в проекте
мнооогооо, и никто и не задавался вопросом, нужна ли конкретно вон там
максимальная производительность, достигаемая инлайном и стиранием границы между
модулями, или всё же heavy lifting у нас делает GGML, и основная часть
вычислений жрётся инференсом, а оптимизация консольной обёртки вокруг GGML даст
очень немного.
При этом хардкод STATIC а равно SHARED является антипаттерном, так как в
проектах на CMake это решение принимается теми людьми, кто исходник собирает в
бинарники, и выставляется через стандартную переменную CMake
"BUILD_SHARED_LIBS". Поэтому целесообразно там стереть STATIC, и при
"set(BUILD_SHARED_LIBS ON)" это нам срежет время линковки. Поскольку библтотеки
теперь могут быть SHARED, добавляем их установку (патч).


3. В проекте присутствует код

   cmake
   target_compile_definitions(ggml-base PRIVATE
       GGML_VERSION="${GGML_INSTALL_VERSION}"
       GGML_COMMIT="${GGML_BUILD_COMMIT}"
   )


При этом "GGML_BUILD_COMMIT" и "GGML_INSTALL_VERSION" динамически генерятся при
запуске CMake. "ninja" автоматически определяет, какие объектные файлы
нуждаются в перекомпиляции, и изменение аргументов вызова компилятора - это
показание к перекомпиляции. Что приводит к тому, что при любом коммите
"ggml-base" пересобирается полностью, а линкующие его бинари -
перелинковываются, а от "ggml-base" транзитивно зависит почти весь проект.

Указанные макроопределения используются ровно в одних местах - в реализациях
методов, возвращающих указанные значения, дальнейший доступ к этим значениям
идёт исключительно через методы. Имеет смысл вынести указанные методы в
отдельную библиотеку. Линковать я её бы, разумеется, предпочёл бы в
соответствии с "BUILD_SHARED_LIBS", но так как смысл этих методов - быть
прибитыми к файлу библиотеки ggml-base гвоздями, то вот тут как раз хардкодим "STATIC"
(патч).

4. В проекте присутствует
   cmake ()
   target_compile_definitions(ggml PUBLIC GGML_BACKEND_DIR="${GGML_BACKEND_DIR}")
  
, при этом макроопределение "GGML_BACKEND_DIR" за пределами библиотеки "ggml"
не используется. Использование "PUBLIC" приводит к тому, что все бинари,
которые линкуют "ggml", получат в командной строке вызова своего компилятора
это определение. Соответственно, если вы потрогаете эту переменную, то это
приведёт к почти полной перекомпиляции проекта. При этом это макроопределение
используется в коде ровно в одном месте:
"search_paths.push_back(fs::u8path(GGML_BACKEND_DIR));", и поэтому "PUBLIC"
нужно смело менять на "PRIVATE".

По-хорошему путь вообще не должен хардкодиться, а должен определяться
относительно бинарей. В проекте используется поиск в директории бинарей, но
путь относительно неё не конфигурируется, а задание "GGML_BACKEND_DIR"
используется для того, чтобы shared-библиотеки бэкендов легли не в "/usr/bin".
Подход в некоторой степени странный. Логичнее сделать конфигурируемым путь
относительно директории бинарника, тогда при изменении префикса пересобирать
библиотеку не придётся, ибо относительный путь останется тем же (патч).


5. В директории "common" в "CMakeLists.txt" есть кусочек

   cmake
   set(TEMPLATE_FILE "${CMAKE_CURRENT_SOURCE_DIR}/build-info.cpp.in")
   set(OUTPUT_FILE   "${CMAKE_CURRENT_BINARY_DIR}/build-info.cpp")
   configure_file(${TEMPLATE_FILE} ${OUTPUT_FILE})

   set(TARGET build_info)
   add_library(${TARGET} OBJECT ${OUTPUT_FILE})


, где файл шаблона

   c++
   int LLAMA_BUILD_NUMBER = @LLAMA_BUILD_NUMBER@;
   char const *LLAMA_COMMIT = "@LLAMA_BUILD_COMMIT@";
   char const *LLAMA_COMPILER = "@BUILD_COMPILER@";
   char const *LLAMA_BUILD_TARGET = "@BUILD_TARGET@";

Проблема такого решения в том, что при каждом запуске CMake пересоздаётся файл
"${OUTPUT_FILE}", а от него уже транзитивно зависят все инструменты, что
приведёт к их перелинковке. Ninja не считает хеши файлов, он определяет
изменения файлов по атрибутам уровня файловой системы, за хешами - к "ccache".
В то же время Ninja отслеживает изменения в командной строке компилятора по
контенту, поэтому в данном случае предпочтительнее не пересоздавать файл
исходного кода, а использовать макроопределения. Ещё я заменил OBJECT на
STATIC. OBJECT в CMake поддерживается не особо официально, долгое время он был
вообще недокументированой внутренней возможностью, о которой, тем не менее, все
знали, и по-прежнему ломается в очень многих случаях (патч).

6. В бэкэндах, связанных с OpenCL, с помощью питоньего скрипта генерятся
заголовочные файлы с хардкодом исходного кода OpenCL-ядер. Во-первых это
трогает файлы при каждом запуске CMake, что приведёт к пересборке, во-вторых
данную проблему можно решить исключительно с помощью CMake, использование
Python здесь излишне. В-третьих хардкод ядер в бинарник - это антипаттерн. У
них есть опция ядра не хардкодить, но они забыли их установить!

7. "common" сливает в одну либу кучу всякой всячины, часть из которой которая
нужна далеко не во всех инструментах, а часть нужна абсолютно во всех, напр.
управление контекстом, и вебсервер и парсинг аргументов командной строки в
конфиг, и контролируемая генерация - всё собрано в этой библиотеке,
превращённой в монструозную помойку размером в 37 мегабайт отстрипанных,
которая до наших патчей ещё статически линковалась в каждый инструмент. В
результате изменение вещи нужной для меньшинства инструментов приводит к
перелинковке всех инструментов.

8. Ещё имеет смысл запачить сборку ненужных бэкендов (она не настраивается
гибко, и не мне это исправлять).

===

Другие патчи исправляют ошибки и добавляют улучшения, обсуждать их не имеет
смысла, так как с антипаттернами, влияющими на скорость сборки, они не связаны.
Апстрим всех патчей, как всегда, на вас, все что можно идёт под Unlicense (но
содержат код, производный от оригинала под MIT), можете вообще от своего имени.

Единственное, что надо отметить: вы не можете просто взять и переместить
библиотеки в другую директорию, как это сделано в Дебиане, из-за ошибки CMake:
https://gitlab.kitware.com/cmake/cmake/-/issues/22621 . Дело в том, что по
факту CMake не поддерживает установку библиотек в какие-либо директории,
которые и так не находятся в RPATH, потому что машинерия CMake, которая патчит
RPATH в бинарях при установке, не пытается собирать RPATH из путей установки
линкуемых библиотек.

Соответственно все библиотеки и приложения, которые залинковали библиотеку,
устанавливаемую не в место по умолчанию, её не найдут. В CMake нет опции,
позволяющей это поправить, все опции, которые вы можете нагуглить для верси 4.2
делают другие вещи. Это определённо баг, так как функциональность установки
CMake вместе с CPack должна собирать работоспособные пакеты, а место установки
- оно определяется не в скрипте сборки, а собирающим.

Можно данную проблему поправить на уровне костыльного CMake-кода, ставящего
RPATH вручную, если он не совпадает с "CMAKE_INSTALL_LIBDIR", но определённо
это должно не так работать, и в этом проекте для этого придётся править код
каждого инструмента, скорее всего апстрим такое не примет.

В Дебиане проблему решают (см.
https://salsa.debian.org/deeplearning-team/llama.cpp и
https://salsa.debian.org/deeplearning-team/ggml.git ) собирая GGML из
отдельного репозитория (что в общем-то было бы правильно, но...), и для каждого
вызова CMake вручную ставя RPATH в "debian/rules", но проблема в том, что
отдельный репозиторий для GGML теперь не является основным, а основной "ggml"
живёт в репозитории llama.cpp и иногда синхронизируется в ggml-евский и
"whisper.cpp" копированием кода ("whisper.cpp" давно пора удалить, так как
поддержка новых мультимодальных моделей (включая модели для TTS и ASR)
завозятся в "llama.cpp"). Это неправильно и грязно, но это не мне решать, раз
разрабам llama так удобно - то пусть в монорепе держат. Но в таком случае
по-хорошему репозиторий "ggml" не мешало бы либо просто удалить, чтобы сломать
всем скрипты сборки и делом довести до сведения, откуда надо теперь ggml
ставить, либо заменить на CMake скрипт, использующий "FetchProject". А пока
есть шанс, что в Дебиане llama будет слинкована с неподходящим ggml.

Все патчи можно скачать единым архивом. Архив имеет 2 директории, в одной
патчи для ускорения пересборки, а в другой - другие патчи, исправляющие
некоторые проблемы и потенциальные проблемы в скриптах сборки, и имеющие
некоторое отношение к пакетированию. Желателен апстрим всех. Ещё желателен
рефактор ggml-части с разносом файлов исходников по директориям.
 
27.01.2026 , Автор: Аноним , Источник: https://github.com/ggml-org/llama.c...
Ключи: llama, build, ai
Раздел:    Корень / Программисту и web-разработчику / C/C++, сборка, отладка

 Добавить комментарий
Имя:
E-Mail:
Заголовок:
Текст:




Партнёры:
PostgresPro
Inferno Solutions
Hosting by Hoster.ru
Хостинг:

Закладки на сайте
Проследить за страницей
Created 1996-2026 by Maxim Chirkov
Добавить, Поддержать, Вебмастеру