Привет,
давно хотел написать о популярном движке, или бэкенде, да запуска LLM моделей- LLama.cpp
https://en.wikipedia.org/wiki/Llama.cpp
https://github.com/ggml-org/llama.cpp
LLama.cpp используется во множестве приложений, таких как:
LM Studio
https://lmstudio.ai
https://github.com/lmstudio-ai
Ollama
https://ollama.com
https://github.com/ollama/ollama
GPT4All
https://www.nomic.ai/gpt4all
https://github.com/nomic-ai/gpt4all
и других.
LLama.cpp работает с форматом AI LLM моделей .gguf
https://huggingface.co/docs/hub/gguf
в этом формате можно найти все популярные модели на HuggingFace
https://huggingface.co/models?library=gguf&sort=trending
Сегодня постараюсь показать, как он работает и что в нем происходит.
LLama.cpp как понятно из названия написана на C++, и здесь буду приводить методы на этом языке и постараюсь объяснить, что в них происходит.
Понятно что не все знакомы с С++, но если плюс минус понимаете С подобный синтаксис, должно быть понятно.
Здесь идея в том, чтобы понять общие принципы, как все работает, что примерно происходит.
Не важные или повторяющиеся куски кода я убрал.
Буду писать методы по мере их вызова при инициализации модели и генерации текста.
Загрузка модели
- Инициализация бекенда, класс llama.cpp
// Инициализирует бэкенд Llama.cpp перед работой с моделями.
// Настраивает таймеры, подготавливает внутренние структуры GGML и проверяет, что базовая инициализация проходит без ошибок.
void llama_backend_init(void) {
// Инициализирует все внутренние таймеры и измерители времени в библиотеке.
// Это нужно, чтобы корректно замерять производительность и время работы операций.
ggml_time_init();
{
// Параметры для создания временного контекста GGML:
// 0 — без выделения памяти, NULL — нет пользовательского буфера, false — без подсчета использования памяти.
struct ggml_init_params params = { 0, NULL, false };
// Создаёт временный контекст GGML, чтобы убедиться, что базовая инициализация прошла успешно.
struct ggml_context * ctx = ggml_init(params);
// Освобождает память, занятую временным контекстом, так как он больше не нужен.
ggml_free(ctx);
}
}// Создаёт и настраивает новый контекст GGML для работы с памятью и вычислениями.
// Выделяет или использует уже переданный буфер памяти, выравнивает его, сохраняет параметры и готовит структуру для хранения объектов.
struct ggml_context * ggml_init(struct ggml_init_params params) {
// Показывает, что это первый вызов функции с момента запуска программы.
static bool is_first_call = true;
// Начинает критическую секцию — защищает код от одновременного выполнения в разных потоках.
ggml_critical_section_start();
// При первом вызове инициализирует систему отсчёта времени (особенно нужно для Windows).
if (is_first_call) {
// я не знаю, чего вызывается 2й раз после вызова из llama_backend_init
ggml_time_init();
is_first_call = false;
}
// Завершает критическую секцию.
ggml_critical_section_end();
// Выделяет память под структуру контекста GGML.
struct ggml_context * ctx = GGML_MALLOC(sizeof(struct ggml_context));
// Если передан размер памяти 0, задаёт минимальное выравненное значение.
if (params.mem_size == 0) {
params.mem_size = GGML_MEM_ALIGN;
}
// Определяет итоговый размер памяти с учётом выравнивания, если буфер не передан извне.
const size_t mem_size = params.mem_buffer ? params.mem_size : GGML_PAD(params.mem_size, GGML_MEM_ALIGN);
// Заполняет структуру контекста начальными значениями.
*ctx = (struct ggml_context) {
/*.mem_size =*/ mem_size, // Размер выделенной памяти
/*.mem_buffer =*/ params.mem_buffer ? params.mem_buffer : ggml_aligned_malloc(mem_size), // Буфер памяти (внешний или выделенный здесь)
/*.mem_buffer_owned =*/ params.mem_buffer ? false : true, // Флаг, что память принадлежит контексту
/*.no_alloc =*/ params.no_alloc, // Запрет автоматического выделения памяти
/*.n_objects =*/ 0, // Количество объектов в контексте
/*.objects_begin =*/ NULL, // Начало списка объектов
/*.objects_end =*/ NULL, // Конец списка объектов
};
// Проверка, что буфер памяти успешно выделен.
GGML_ASSERT(ctx->mem_buffer != NULL);
// Проверка, что буфер памяти выровнен по нужным границам.
GGML_ASSERT_ALIGNED(ctx->mem_buffer);
// Возвращает готовый контекст.
return ctx;
}// Инициализирует внутреннюю систему измерения времени для высокоточных замеров.
// Запоминает частоту таймера и момент старта программы, чтобы избежать переполнения при вычислениях.
void ggml_time_init(void) {
LARGE_INTEGER t;
// Получает частоту высокоточного таймера (количество тиков в секунду) и сохраняет её.
QueryPerformanceFrequency(&t);
timer_freq = t.QuadPart;
// Получает текущее значение счётчика таймера и сохраняет его как время запуска программы.
// Это уменьшает риск переполнения при дальнейших вычислениях времени.
QueryPerformanceCounter(&t);
timer_start = t.QuadPart;
}2. Загрузка модели, класс llama.cpp
// Загружает модель LLaMA из файла по указанному пути с заданными параметрами.
// Создаёт пустой список для разбиения модели на части и передаёт всё в функцию-реализацию загрузки.
struct llama_model * llama_model_load_from_file(
const char * path_model, // Путь к файлу модели
struct llama_model_params params) { // Параметры загрузки модели
// Пустой список строк, используется если модель не разбивается на несколько файлов
std::vector<std::string> splits = {};
// Вызывает основную функцию загрузки модели с пустым списком частей
return llama_model_load_from_file_impl(path_model, splits, params);
}// Загружает модель LLaMA с учётом параметров, выбранных устройств и режима распределения по GPU.
// Настраивает обратный вызов прогресса, выбирает устройства для вычислений, затем вызывает функцию чтения модели с диска.
static struct llama_model * llama_model_load_from_file_impl(
const std::string & path_model, // Путь к файлу модели
std::vector<std::string> & splits, // Список частей модели (если модель разделена на несколько файлов)
struct llama_model_params params) { // Параметры загрузки
// Инициализирует систему измерения времени.
ggml_time_init();
// Проверка: если требуется загрузить не только словарь, но бэкенды не инициализированы — вернуть ошибку.
if (!params.vocab_only && ggml_backend_reg_count() == 0) {
LLAMA_LOG_ERROR("%s: no backends are loaded. hint: use ggml_backend_load() or ggml_backend_load_all() to load a backend before calling this function\n", __func__);
return nullptr;
}
// Переменная для отображения прогресса загрузки.
unsigned cur_percentage = 0;
// Если не передан обратный вызов прогресса — создать стандартный.
if (params.progress_callback == NULL) {
params.progress_callback_user_data = &cur_percentage;
params.progress_callback = [](float progress, void * ctx) {
unsigned * cur_percentage_p = (unsigned *) ctx;
unsigned percentage = (unsigned) (100 * progress);
while (percentage > *cur_percentage_p) {
*cur_percentage_p = percentage;
LLAMA_LOG_CONT(".");
if (percentage >= 100) {
LLAMA_LOG_CONT("\n");
}
}
return true;
};
}
// Создаёт объект модели с переданными параметрами.
llama_model * model = new llama_model(params);
// Создание списка устройств для работы модели.
if (params.devices) {
// Если устройства явно переданы — добавить их в модель.
for (ggml_backend_dev_t * dev = params.devices; *dev; ++dev) {
model->devices.push_back(*dev);
}
} else {
std::vector<ggml_backend_dev_t> rpc_servers;
// Использовать все доступные устройства.
for (size_t i = 0; i < ggml_backend_dev_count(); ++i) {
ggml_backend_dev_t dev = ggml_backend_dev_get(i);
switch (ggml_backend_dev_type(dev)) {
case GGML_BACKEND_DEVICE_TYPE_CPU:
case GGML_BACKEND_DEVICE_TYPE_ACCEL:
// Пропускаем CPU — они обрабатываются отдельно.
break;
case GGML_BACKEND_DEVICE_TYPE_GPU:
ggml_backend_reg_t reg = ggml_backend_dev_backend_reg(dev);
if (ggml_backend_reg_name(reg) == std::string("RPC")) {
rpc_servers.push_back(dev);
} else {
model->devices.push_back(dev);
}
break;
}
}
// Добавляем RPC-сервера в начало списка.
if (!rpc_servers.empty()) {
model->devices.insert(model->devices.begin(), rpc_servers.begin(), rpc_servers.end());
}
}
// Если выбран режим работы с одним GPU — оставить только главное устройство.
if (params.split_mode == LLAMA_SPLIT_MODE_NONE) {
if (params.main_gpu < 0) {
model->devices.clear();
} else {
if (params.main_gpu >= (int)model->devices.size()) {
LLAMA_LOG_ERROR("%s: invalid value for main_gpu: %d (available devices: %zu)\n", __func__, params.main_gpu, model->devices.size());
llama_model_free(model);
return nullptr;
}
ggml_backend_dev_t main_gpu = model->devices[params.main_gpu];
model->devices.clear();
model->devices.push_back(main_gpu);
}
}
// Логируем информацию о выбранных устройствах и объёме доступной памяти.
for (auto * dev : model->devices) {
size_t free, total; // NOLINT
ggml_backend_dev_memory(dev, &free, &total);
LLAMA_LOG_INFO("%s: using device %s (%s) - %zu MiB free\n", __func__, ggml_backend_dev_name(dev), ggml_backend_dev_description(dev), free/1024/1024);
}
// Загружаем модель с диска.
const int status = llama_model_load(path_model, splits, *model, params);
GGML_ASSERT(status <= 0);
if (status < 0) {
if (status == -1) {
LLAMA_LOG_ERROR("%s: failed to load model\n", __func__);
} else if (status == -2) {
LLAMA_LOG_INFO("%s: cancelled model load\n", __func__);
}
llama_model_free(model);
return nullptr;
}
// Возвращаем готовый объект модели.
return model;
}// Загружает модель LLaMA из файла с диска и инициализирует все её данные.
// Читает архитектуру, гиперпараметры, словарь и тензоры; при включённом флаге vocab_only — загружает только словарь.
static int llama_model_load(
const std::string & fname, // Путь к файлу модели
std::vector<std::string> & splits, // Список дополнительных частей модели
llama_model & model, // Объект модели для заполнения данными
llama_model_params & params) { // Параметры загрузки
// Сбрасываем время загрузки (оно будет пересчитано после первого eval)
model.t_load_us = 0;
// Объект-таймер, автоматически измеряющий время выполнения
time_meas tm(model.t_load_us);
// Запоминаем момент начала загрузки
model.t_start_us = tm.t_start_us;
try {
// Создаём загрузчик модели, который управляет чтением данных (с поддержкой mmap и проверок)
llama_model_loader ml(fname, splits, params.use_mmap, params.check_tensors,
params.kv_overrides, params.tensor_buft_overrides);
// Выводим информацию о файле модели
ml.print_info();
// Указываем, загружаем ли мы только словарь
model.hparams.vocab_only = params.vocab_only;
// Загружаем архитектуру модели (тип слоёв, структура сети)
try {
model.load_arch(ml);
} catch(const std::exception & e) {
throw std::runtime_error("error loading model architecture: " + std::string(e.what()));
}
// Загружаем гиперпараметры модели (размер слоёв, количество токенов и др.)
try {
model.load_hparams(ml);
} catch(const std::exception & e) {
throw std::runtime_error("error loading model hyperparameters: " + std::string(e.what()));
}
// Загружаем словарь токенов
try {
model.load_vocab(ml);
} catch(const std::exception & e) {
throw std::runtime_error("error loading model vocabulary: " + std::string(e.what()));
}
// Загружаем статистику модели (например, размеры и количество тензоров)
model.load_stats(ml);
// Выводим сводную информацию о модели
model.print_info();
// Если нужно только загрузить словарь — пропускаем тензоры
if (params.vocab_only) {
LLAMA_LOG_INFO("%s: vocab only - skipping tensors\n", __func__);
return 0;
}
// Загружаем тензоры (веса слоёв, матрицы эмбеддингов и т.д.)
if (!model.load_tensors(ml)) {
return -2; // код для "загрузка отменена"
}
} catch (const std::exception & err) {
// Если на любом этапе возникла ошибка — логируем её и возвращаем -1
LLAMA_LOG_ERROR("%s: error loading model: %s\n", __func__, err.what());
return -1;
}
// Код 0 — успешная загрузка
return 0;
}
// Загружает тензоры модели (веса, bias, embeddings) и распределяет их по устройствам (CPU/GPU)
bool llama_model::load_tensors(llama_model_loader & ml) {
const auto & split_mode = params.split_mode; // Режим разделения слоёв между устройствами
const auto & n_gpu_layers = params.n_gpu_layers; // Сколько слоёв поместить на GPU
const auto & use_mlock = params.use_mlock; // Блокировать память от выгрузки в swap
const auto & tensor_split = params.tensor_split; // Явное задание долей тензоров по GPU
const int n_layer = hparams.n_layer; // Общее количество слоёв в модели
LLAMA_LOG_INFO("%s: loading model tensors, this can take a while... (mmap = %s)\n", __func__, ml.use_mmap ? "true" : "false");
// Формирует список доступных типов буферов (памяти) для CPU и GPU
pimpl->cpu_buft_list = make_cpu_buft_list(devices);
for (auto * dev : devices) {
buft_list_t buft_list = make_gpu_buft_list(dev, split_mode, tensor_split);
buft_list.insert(buft_list.end(), pimpl->cpu_buft_list.begin(), pimpl->cpu_buft_list.end());
pimpl->gpu_buft_list.emplace(dev, std::move(buft_list));
}
// Распределяет объёмы тензоров между устройствами по available memory или tensor_split[]
std::vector<float> splits(n_devices());
// ... (подсчёт и нормализация сумм)
// Определяет, какие слои отправлять на GPU, а какие оставить на CPU
const int i_gpu_start = std::max((int) hparams.n_layer - n_gpu_layers, 0);
const int act_gpu_layers = devices.empty() ? 0 : std::min(n_gpu_layers, (int)n_layer + 1);
// Присваивает каждому слою устройство (CPU или конкретный GPU), учитывая распределение и режим
auto get_layer_buft_list = [&](int il) -> llama_model::impl::layer_dev {
if (il < i_gpu_start || (il - i_gpu_start) >= act_gpu_layers) {
return {cpu_dev, &pimpl->cpu_buft_list};
}
int layer_gpu = std::upper_bound(...) - splits.begin();
return {devices.at(layer_gpu), &pimpl->gpu_buft_list.at(devices.at(layer_gpu))};
};
// Устанавливает устройство и буферы для input, output и каждого слоя
pimpl->dev_input = { cpu_dev, &pimpl->cpu_buft_list };
pimpl->dev_output = get_layer_buft_list(n_layer);
pimpl->dev_layer.resize(n_layer);
for (int il = 0; il < n_layer; ++il) {
pimpl->dev_layer[il] = get_layer_buft_list(il);
}
// Создаёт ggml-контексты для разных типов буферов (каждый контекст управляет своими тензорами)
std::map<ggml_backend_buffer_type_t, ggml_context *> ctx_map;
auto ctx_for_buft = [&](ggml_backend_buffer_type_t buft) {
// ... создаёт новый ggml_context при необходимости
};
// Создаёт и размещает тензоры модели (веса, bias и т. д.)
auto create_tensor = [&](const LLM_TN_IMPL & tn, const std::initializer_list<int64_t> & ne, int flags) {
ggml_tensor * t_meta = ml.get_tensor_meta(tn.str().c_str());
// ... пропускает неиспользуемые, выбирает подходящий буфер (CPU/GPU), создаёт тензор
return ml.create_tensor(...);
};
// Повторяющиеся слои инициализируются в `layers`
layers.resize(n_layer);
switch (arch) {
// Здесь я приведу пример для свежей модели от Google - Gemma 3n,
// но для каждой модели и варианта архитектуры есть свой case, я насчитал в сумме около 80ти
case LLM_ARCH_GEMMA3N: {
// Извлекаем параметры архитектуры: altup — дополнительные направления, laurel — ранжированные матрицы
const int64_t n_altup = hparams.n_altup;
const int64_t laurel_rank = hparams.laurel_rank;
const int64_t n_embd_altup = hparams.n_embd_altup;
// Загружаем выходной слой модели (если не найден — используем входной embedding повторно)
output = create_tensor(tn(LLM_TENSOR_OUTPUT, "weight"), {n_embd, n_vocab}, TENSOR_NOT_REQUIRED);
if (output == NULL) {
output = create_tensor(tn(LLM_TENSOR_TOKEN_EMBD, "weight"), {n_embd, n_vocab}, TENSOR_DUPLICATED);
}
// Загружаем embedding для входных токенов (обычный и по-слойный вариант)
tok_embd = create_tensor(tn(LLM_TENSOR_TOKEN_EMBD, "weight"), {n_embd, n_vocab}, 0);
tok_embd_per_layer = create_tensor(tn(LLM_TENSOR_PER_LAYER_TOKEN_EMBD, "weight"), {n_embd_altup * n_layer, n_vocab}, 0);
// Загружаем специальные матрицы проекций для механизма AltUp (улучшенная активация слоёв)
altup_proj = create_tensor(tn(LLM_TENSOR_ALTUP_PROJ, "weight"), {n_embd, n_embd, n_altup - 1}, 0);
altup_unembd_proj = create_tensor(tn(LLM_TENSOR_ALTUP_UNEMBD_PROJ, "weight"), {n_embd, n_embd, n_altup - 1}, 0);
per_layer_model_proj = create_tensor(tn(LLM_TENSOR_PER_LAYER_MODEL_PROJ, "weight"), {n_embd, n_embd_altup * n_layer}, 0);
per_layer_proj_norm = create_tensor(tn(LLM_TENSOR_PER_LAYER_PROJ_NORM, "weight"), {n_embd_altup}, 0);
// Загружаем финальную нормализацию для выходного слоя
output_norm = create_tensor(tn(LLM_TENSOR_OUTPUT_NORM, "weight"), {n_embd}, 0);
// Цикл по каждому слою модели (например, 28 слоёв)
for (int i = 0; i < n_layer; ++i) {
auto & layer = layers[i];
// Нормализация перед вниманием
layer.attn_norm = create_tensor(tn(LLM_TENSOR_ATTN_NORM, "weight", i), {n_embd}, 0);
// Загружаем матрицы для внимания: Q, K, V — запрос, ключ, значение; O — выход
layer.wq = create_tensor(tn(LLM_TENSOR_ATTN_Q, "weight", i), {n_embd, n_embd_head_k * n_head}, 0);
layer.wk = create_tensor(tn(LLM_TENSOR_ATTN_K, "weight", i), {n_embd, n_embd_k_gqa}, 0);
layer.wv = create_tensor(tn(LLM_TENSOR_ATTN_V, "weight", i), {n_embd, n_embd_v_gqa}, 0);
layer.wo = create_tensor(tn(LLM_TENSOR_ATTN_OUT, "weight", i), {n_embd_head_k * n_head, n_embd}, 0);
// Дополнительные нормализации в блоке внимания
layer.attn_q_norm = create_tensor(tn(LLM_TENSOR_ATTN_Q_NORM, "weight", i), {n_embd_head_k}, 0);
layer.attn_k_norm = create_tensor(tn(LLM_TENSOR_ATTN_K_NORM, "weight", i), {n_embd_head_k}, 0);
layer.attn_post_norm = create_tensor(tn(LLM_TENSOR_ATTN_POST_NORM, "weight", i), {n_embd}, 0);
// Блок feed-forward (полносвязный слой)
layer.ffn_norm = create_tensor(tn(LLM_TENSOR_FFN_NORM, "weight", i), {n_embd}, 0);
layer.ffn_gate = create_tensor(tn(LLM_TENSOR_FFN_GATE, "weight", i), {n_embd, n_ff}, 0);
layer.ffn_up = create_tensor(tn(LLM_TENSOR_FFN_UP, "weight", i), {n_embd, n_ff}, 0);
layer.ffn_down = create_tensor(tn(LLM_TENSOR_FFN_DOWN, "weight", i), {n_ff, n_embd}, 0);
layer.ffn_post_norm = create_tensor(tn(LLM_TENSOR_FFN_POST_NORM, "weight", i), {n_embd}, 0);
// Проекции, специфичные для AltUp и Laurel — улучшенные механизмы маршрутизации и нормализации
layer.per_layer_inp_gate = create_tensor(tn(LLM_TENSOR_PER_LAYER_INP_GATE, "weight", i), {n_embd, n_embd_altup}, 0);
layer.per_layer_proj = create_tensor(tn(LLM_TENSOR_PER_LAYER_PROJ, "weight", i), {n_embd_altup, n_embd}, 0);
layer.per_layer_post_norm = create_tensor(tn(LLM_TENSOR_PER_LAYER_POST_NORM, "weight", i), {n_embd}, 0);
layer.altup_correct_coef = create_tensor(tn(LLM_TENSOR_ALTUP_CORRECT_COEF, "weight", i), {n_altup, n_altup}, 0);
layer.altup_correct_scale = create_tensor(tn(LLM_TENSOR_ALTUP_CORRECT_SCALE, "weight", i), {n_embd}, 0);
layer.altup_predict_coef = create_tensor(tn(LLM_TENSOR_ALTUP_PREDICT_COEF, "weight", i), {n_altup, n_altup * n_altup}, 0);
layer.altup_router = create_tensor(tn(LLM_TENSOR_ALTUP_ROUTER, "weight", i), {n_embd, n_altup}, 0);
layer.altup_router_norm = create_tensor(tn(LLM_TENSOR_ALTUP_ROUTER_NORM, "weight", i), {n_embd}, 0);
// Laurel: обучаемая маршрутизация через две матрицы и нормализацию
layer.laurel_l = create_tensor(tn(LLM_TENSOR_LAUREL_L, "weight", i), {n_embd, laurel_rank}, 0);
layer.laurel_r = create_tensor(tn(LLM_TENSOR_LAUREL_R, "weight", i), {laurel_rank, n_embd}, 0);
layer.laurel_post_norm = create_tensor(tn(LLM_TENSOR_LAUREL_POST_NORM, "weight", i), {n_embd}, 0);
}
} break;
// Выкидываем ошибку, если архитектура не известна
default: throw std::runtime_error("unknown architecture");
}
// Если хотя бы один тензор пришлось разместить не в том буфере (например, на CPU вместо GPU), логируем это
if (n_moved_tensors > 0) {
LLAMA_LOG_DEBUG("%s: tensor '%s' (%s) (and %d others) cannot be used with preferred buffer type %s, using %s instead\n",
__func__, first_moved_tensor->name, ggml_type_name(first_moved_tensor->type), n_moved_tensors - 1,
ggml_backend_buft_name(first_moved_from_buft), ggml_backend_buft_name(first_moved_to_buft));
}
// Сообщаем, что все тензоры описаны и готовы к загрузке
ml.done_getting_tensors();
// Настраиваем отображения памяти (mmap), возможно с блокировкой (mlock), чтобы не выгружались из RAM
ml.init_mappings(true, use_mlock ? &pimpl->mlock_mmaps : nullptr);
pimpl->mappings.reserve(ml.mappings.size());
// Готовим к созданию буферов под веса модели
std::vector<std::pair<ggml_context *, llama_buf_map>> ctx_bufs;
ctx_bufs.reserve(ctx_map.size());
const size_t n_max_backend_buffer = ctx_map.size() * ml.files.size();
pimpl->bufs.reserve(n_max_backend_buffer);
// Для каждого типа памяти (буфера), в котором создавались тензоры
for (auto & it : ctx_map) {
ggml_backend_buffer_type_t buft = it.first;
ggml_context * ctx = it.second;
// Пропускаем контексты, в которых нет тензоров
if (ggml_get_first_tensor(ctx) == nullptr) {
continue;
}
llama_buf_map buf_map;
buf_map.reserve(n_max_backend_buffer);
// Определяем, к какому устройству относится этот буфер (например, CPU или GPU)
ggml_backend_dev_t dev = ggml_backend_buft_get_device(buft);
if (!dev) {
// Обход бага: если у CPU-буфера нет устройства, назначаем вручную
dev = ggml_backend_dev_by_type(GGML_BACKEND_DEVICE_TYPE_CPU);
if (!dev) {
throw std::runtime_error(format("%s: no CPU backend found", __func__));
}
}
// Узнаём возможности устройства (например, поддерживает ли оно загрузку из mmap-буфера)
ggml_backend_dev_props props;
ggml_backend_dev_get_props(dev, &props);
bool buffer_from_host_ptr_supported = props.caps.buffer_from_host_ptr;
bool is_default_buft = buft == ggml_backend_dev_buffer_type(dev);
// Если можно напрямую отдать mmap-область устройству — создаём буфер из неё
if (ml.use_mmap && use_mmap_buffer && buffer_from_host_ptr_supported && is_default_buft) {
for (uint32_t idx = 0; idx < ml.files.size(); idx++) {
void * addr = nullptr;
size_t first, last;
ml.get_mapping_range(&first, &last, &addr, idx, ctx);
if (first >= last) {
continue;
}
const size_t max_size = ggml_get_max_tensor_size(ctx);
ggml_backend_buffer_t buf = ggml_backend_dev_buffer_from_host_ptr(dev, (char *) addr + first, last - first, max_size);
if (buf == nullptr) {
throw std::runtime_error(format("unable to allocate %s buffer", ggml_backend_buft_name(buft)));
}
pimpl->bufs.emplace_back(buf);
buf_map.emplace(idx, buf);
}
} else {
// Иначе выделяем обычный буфер под все тензоры для этого контекста
ggml_backend_buffer_t buf = ggml_backend_alloc_ctx_tensors_from_buft(ctx, buft);
if (buf == nullptr) {
throw std::runtime_error(format("unable to allocate %s buffer", ggml_backend_buft_name(buft)));
}
pimpl->bufs.emplace_back(buf);
// Если используется блокировка памяти и буфер размещён в RAM — блокируем его от выгрузки системой
if (use_mlock && ggml_backend_buffer_is_host(buf)) {
pimpl->mlock_bufs.emplace_back(new llama_mlock);
auto & mlock_buf = pimpl->mlock_bufs.back();
mlock_buf->init (ggml_backend_buffer_get_base(buf));
mlock_buf->grow_to(ggml_backend_buffer_get_size(buf));
}
// Один и тот же буфер используется для всех частей файла (если без mmap)
for (uint32_t idx = 0; idx < ml.files.size(); idx++) {
buf_map.emplace(idx, buf);
}
}
// Проверка на случай, если ни один буфер не был создан
if (pimpl->bufs.empty()) {
throw std::runtime_error("failed to allocate buffer");
}
// Устанавливаем флаг, что буфер содержит веса (важно для планировщика операций в ggml)
for (auto & buf : buf_map) {
ggml_backend_buffer_set_usage(buf.second, GGML_BACKEND_BUFFER_USAGE_WEIGHTS);
}
// Связываем контекст с его буферами
ctx_bufs.emplace_back(ctx, buf_map);
}
// Если используется GPU, логируем, сколько слоёв перенесено на него
if (llama_supports_gpu_offload()) {
const int n_gpu = std::min(n_gpu_layers, int(hparams.n_layer));
LLAMA_LOG_INFO("%s: offloading %d repeating layers to GPU\n", __func__, n_gpu);
if (n_gpu_layers > (int) hparams.n_layer) {
LLAMA_LOG_INFO("%s: offloading output layer to GPU\n", __func__);
}
LLAMA_LOG_INFO("%s: offloaded %d/%d layers to GPU\n", __func__, std::min(n_gpu_layers, hparams.n_layer + 1), hparams.n_layer + 1);
}
// Выводим размер каждого выделенного буфера (например, сколько памяти занято на GPU)
for (auto & buf : pimpl->bufs) {
LLAMA_LOG_INFO("%s: %12s model buffer size = %8.2f MiB\n", __func__, ggml_backend_buffer_name(buf.get()), ggml_backend_buffer_get_size(buf.get()) / 1024.0 / 1024.0);
}
// Собираем все тензоры в map по имени, чтобы можно было к ним быстро обращаться
for (auto & ctx : pimpl->ctxs) {
for (auto * cur = ggml_get_first_tensor(ctx.get()); cur != NULL; cur = ggml_get_next_tensor(ctx.get(), cur)) {
tensors_by_name.emplace_back(ggml_get_name(cur), cur);
}
}
// Загружаем бинарные данные всех тензоров в выделенные буферы
for (auto & it : ctx_bufs) {
ggml_context * ctx = it.first;
auto & bufs = it.second;
if (!ml.load_all_data(ctx, bufs, use_mlock ? &pimpl->mlock_mmaps : NULL, params.progress_callback, params.progress_callback_user_data)) {
return false;
}
}
// Если используется mmap, сохраняем все отображения файла
if (use_mmap_buffer) {
for (auto & mapping : ml.mappings) {
pimpl->mappings.emplace_back(std::move(mapping));
}
}
// Успешная загрузка всех тензоров завершена
return true;
}3. Создание контекста для загруженной модели, класс llama-context.cpp
// Конструктор llama_context создаёт окружение для инференса на основе загруженной модели.
// Здесь настраиваются параметры контекста, выбираются устройства (CPU/GPU), выделяется память,
// создаётся планировщик вычислений и резервируются буферы для выполнения инференса.
llama_context::llama_context(
const llama_model & model, // Загруженная модель LLaMA
llama_context_params params) // Параметры запуска инференса
// Сохраняем ссылку на модель и создаём менеджер выделения батчей
: model(model),
balloc(std::make_unique<llama_batch_allocr>(model.hparams.n_pos_per_embd())) {
// Копируем времена старта и загрузки модели для статистики
t_start_us = model.t_start_us;
t_load_us = model.t_load_us;
// Упрощённый доступ к гиперпараметрам модели
const auto & hparams = model.hparams;
// Устанавливаем максимальное число последовательностей (не больше LLAMA_MAX_SEQ)
cparams.n_seq_max = std::max(1u, params.n_seq_max);
if (cparams.n_seq_max > LLAMA_MAX_SEQ) { // LLAMA_MAX_SEQ 64
throw std::runtime_error("n_seq_max must be <= " + std::to_string(LLAMA_MAX_SEQ));
}
// Копируем числовые и логические параметры из входных настроек
cparams.n_threads = params.n_threads;
cparams.n_threads_batch = params.n_threads_batch;
cparams.yarn_ext_factor = params.yarn_ext_factor;
cparams.yarn_attn_factor = params.yarn_attn_factor;
cparams.yarn_beta_fast = params.yarn_beta_fast;
cparams.yarn_beta_slow = params.yarn_beta_slow;
cparams.defrag_thold = params.defrag_thold;
cparams.embeddings = params.embeddings;
cparams.offload_kqv = params.offload_kqv;
cparams.flash_attn = params.flash_attn;
cparams.no_perf = params.no_perf;
cparams.pooling_type = params.pooling_type;
cparams.warmup = false;
// Настройка размера контекста и параметров RoPE с подстановкой значений по умолчанию
cparams.n_ctx = params.n_ctx == 0 ? hparams.n_ctx_train : params.n_ctx;
cparams.rope_freq_base = params.rope_freq_base == 0.0f ? hparams.rope_freq_base_train : params.rope_freq_base;
cparams.rope_freq_scale = params.rope_freq_scale == 0.0f ? hparams.rope_freq_scale_train : params.rope_freq_scale;
// Определение оригинального размера контекста для YaRN
cparams.n_ctx_orig_yarn = params.yarn_orig_ctx != 0 ? params.yarn_orig_ctx :
hparams.n_ctx_orig_yarn != 0 ? hparams.n_ctx_orig_yarn :
hparams.n_ctx_train;
// Настройка обратных вызовов
cparams.cb_eval = params.cb_eval;
cparams.cb_eval_user_data = params.cb_eval_user_data;
// Определение типа масштабирования RoPE, если он не задан
auto rope_scaling_type = params.rope_scaling_type;
if (rope_scaling_type == LLAMA_ROPE_SCALING_TYPE_UNSPECIFIED) {
rope_scaling_type = hparams.rope_scaling_type_train;
}
if (rope_scaling_type == LLAMA_ROPE_SCALING_TYPE_NONE) {
cparams.rope_freq_scale = 1.0f;
}
if (cparams.yarn_ext_factor < 0.0f) {
cparams.yarn_ext_factor = rope_scaling_type == LLAMA_ROPE_SCALING_TYPE_YARN ? 1.0f : 0.0f;
}
// Корректируем коэффициент внимания YaRN
cparams.yarn_attn_factor *= hparams.rope_attn_factor;
// Если pooling_type не указан, берём из модели или ставим NONE
if (cparams.pooling_type == LLAMA_POOLING_TYPE_UNSPECIFIED) {
cparams.pooling_type = hparams.pooling_type == LLAMA_POOLING_TYPE_UNSPECIFIED
? LLAMA_POOLING_TYPE_NONE
: hparams.pooling_type;
}
// Настройка типа внимания (causal или нет)
if (params.attention_type == LLAMA_ATTENTION_TYPE_UNSPECIFIED) {
cparams.causal_attn = hparams.causal_attn;
} else {
cparams.causal_attn = params.attention_type == LLAMA_ATTENTION_TYPE_CAUSAL;
}
// Ограничение размера батча при causal attention
cparams.n_batch = cparams.causal_attn
? std::min(cparams.n_ctx, params.n_batch)
: params.n_batch;
// Минимальный размер батча для избежания выхода за границы KQ-mask
if (cparams.n_batch < GGML_KQ_MASK_PAD) {
LLAMA_LOG_WARN("%s: n_batch is less than GGML_KQ_MASK_PAD - increasing to %d\n", __func__, GGML_KQ_MASK_PAD);
cparams.n_batch = GGML_KQ_MASK_PAD;
}
cparams.n_ubatch = std::min(cparams.n_batch,
params.n_ubatch == 0 ? params.n_batch : params.n_ubatch);
// Прочие флаги
cparams.op_offload = params.op_offload;
cparams.kv_unified = params.kv_unified;
// Проверка поддержки set_rows для неунифицированного KV-кэша
{
const char * LLAMA_SET_ROWS = getenv("LLAMA_SET_ROWS");
supports_set_rows = LLAMA_SET_ROWS ? (atoi(LLAMA_SET_ROWS) != 0) : supports_set_rows;
if (!supports_set_rows && !cparams.kv_unified) {
LLAMA_LOG_WARN("%s: non-unified KV cache requires ggml_set_rows() - forcing unified KV cache\n", __func__);
cparams.kv_unified = true;
}
}
// Проверка переменной окружения на запрет переиспользования графа
{
const char * LLAMA_GRAPH_REUSE_DISABLE = getenv("LLAMA_GRAPH_REUSE_DISABLE");
graph_reuse_disable = LLAMA_GRAPH_REUSE_DISABLE ? (atoi(LLAMA_GRAPH_REUSE_DISABLE) != 0) : graph_reuse_disable;
if (graph_reuse_disable) {
LLAMA_LOG_WARN("%s: graph reuse disabled\n", __func__);
}
}
// Логирование ключевых параметров
const uint32_t n_ctx_per_seq = cparams.n_ctx / cparams.n_seq_max;
LLAMA_LOG_INFO("%s: n_seq_max = %u\n", __func__, cparams.n_seq_max);
LLAMA_LOG_INFO("%s: n_ctx = %u\n", __func__, cparams.n_ctx);
LLAMA_LOG_INFO("%s: n_ctx_per_seq = %u\n", __func__, n_ctx_per_seq);
LLAMA_LOG_INFO("%s: n_batch = %u\n", __func__, cparams.n_batch);
LLAMA_LOG_INFO("%s: n_ubatch = %u\n", __func__, cparams.n_ubatch);
LLAMA_LOG_INFO("%s: causal_attn = %d\n", __func__, cparams.causal_attn);
LLAMA_LOG_INFO("%s: flash_attn = %d\n", __func__, cparams.flash_attn);
LLAMA_LOG_INFO("%s: kv_unified = %s\n", __func__, cparams.kv_unified ? "true" : "false");
LLAMA_LOG_INFO("%s: freq_base = %.1f\n", __func__, cparams.rope_freq_base);
LLAMA_LOG_INFO("%s: freq_scale = %g\n", __func__, cparams.rope_freq_scale);
// GPU backends: инициализируем все задействованные устройства для инференса
if (!hparams.vocab_only) {
// Добавляем GPU из списка model.devices
for (auto * dev : model.devices) {
ggml_backend_t backend = ggml_backend_dev_init(dev, nullptr);
if (backend == nullptr) {
throw std::runtime_error(format("failed to initialize %s backend", ggml_backend_dev_name(dev)));
}
backends.emplace_back(backend);
}
// Добавляем ACCEL backend (например, BLAS)
for (size_t i = 0; i < ggml_backend_dev_count(); ++i) {
ggml_backend_dev_t dev = ggml_backend_dev_get(i);
if (ggml_backend_dev_type(dev) == GGML_BACKEND_DEVICE_TYPE_ACCEL) {
ggml_backend_t backend = ggml_backend_dev_init(dev, nullptr);
if (backend == nullptr) {
throw std::runtime_error(format("failed to initialize %s backend", ggml_backend_dev_name(dev)));
}
backends.emplace_back(backend);
}
}
// Инициализируем CPU backend
backend_cpu = ggml_backend_init_by_type(GGML_BACKEND_DEVICE_TYPE_CPU, nullptr);
if (backend_cpu == nullptr) {
throw std::runtime_error("failed to initialize CPU backend");
}
backends.emplace_back(backend_cpu);
// Создаём список функций для управления количеством потоков в каждом backend
for (auto & backend : backends) {
ggml_backend_dev_t dev = ggml_backend_get_device(backend.get());
ggml_backend_reg_t reg = dev ? ggml_backend_dev_backend_reg(dev) : nullptr;
if (reg) {
auto ggml_backend_set_n_threads_fn =
(ggml_backend_set_n_threads_t) ggml_backend_reg_get_proc_address(reg, "ggml_backend_set_n_threads");
if (ggml_backend_set_n_threads_fn) {
set_n_threads_fns.emplace_back(backend.get(), ggml_backend_set_n_threads_fn);
}
}
}
// Устанавливаем callback на прерывание выполнения
llama_set_abort_callback(this, params.abort_callback, params.abort_callback_data);
// Резервируем буфер для вывода результатов (динамически может увеличиваться в инференсе)
{
if ((uint32_t) output_reserve(params.n_seq_max) < params.n_seq_max) {
throw std::runtime_error("failed to reserve initial output buffer");
}
LLAMA_LOG_INFO("%s: %10s output buffer size = %8.2f MiB\n", __func__,
ggml_backend_buffer_name (buf_output.get()),
ggml_backend_buffer_get_size(buf_output.get()) / 1024.0 / 1024.0);
}
}
// Инициализация модуля памяти (KV-cache и вспомогательные буферы)
if (!hparams.vocab_only) {
llama_memory_params params_mem = {
/*.type_k =*/ params.type_k,
/*.type_v =*/ params.type_v,
/*.swa_full =*/ params.swa_full,
};
memory.reset(model.create_memory(params_mem, cparams));
}
// Подготовка backend'ов к работе (буферы и планировщик)
if (!hparams.vocab_only) {
LLAMA_LOG_DEBUG("%s: enumerating backends\n", __func__);
backend_buft.clear();
backend_ptrs.clear();
// Настройка буферов для каждого backend'а
for (auto & backend : backends) {
auto * buft = ggml_backend_get_default_buffer_type(backend.get());
auto backend_type = ggml_backend_dev_type(ggml_backend_get_device(backend.get()));
// Для CPU backend используем host buffer от первого GPU для ускорения передачи данных
if (backend_type == GGML_BACKEND_DEVICE_TYPE_CPU && !model.devices.empty()) {
auto * dev = model.devices[0];
auto * host_buft = ggml_backend_dev_host_buffer_type(dev);
if (host_buft) {
buft = host_buft;
}
}
backend_buft.push_back(buft);
backend_ptrs.push_back(backend.get());
}
LLAMA_LOG_DEBUG("%s: backend_ptrs.size() = %zu\n", __func__, backend_ptrs.size());
const size_t max_nodes = this->graph_max_nodes();
LLAMA_LOG_DEBUG("%s: max_nodes = %zu\n", __func__, max_nodes);
// Создаём объекты для хранения результатов вычислений графа
gf_res_prev.reset(new llm_graph_result(max_nodes));
gf_res_reserve.reset(new llm_graph_result(max_nodes));
// Проверка необходимости pipeline parallelism (параллельная обработка слоёв на разных устройствах)
bool pipeline_parallel =
model.n_devices() > 1 &&
model.params.n_gpu_layers > (int) model.hparams.n_layer &&
model.params.split_mode == LLAMA_SPLIT_MODE_LAYER &&
cparams.offload_kqv &&
!model.has_tensor_overrides();
// Проверка, что все устройства поддерживают async compute и events
if (pipeline_parallel) {
for (auto & backend : backends) {
auto dev_type = ggml_backend_dev_type(ggml_backend_get_device(backend.get()));
if (dev_type == GGML_BACKEND_DEVICE_TYPE_CPU) continue;
auto * dev = ggml_backend_get_device(backend.get());
ggml_backend_dev_props props;
ggml_backend_dev_get_props(dev, &props);
if (!props.caps.async || !props.caps.events) {
pipeline_parallel = false;
break;
}
}
}
// Создаём планировщик вычислений для всех backend'ов
sched.reset(ggml_backend_sched_new(
backend_ptrs.data(), backend_buft.data(),
backend_ptrs.size(), max_nodes,
pipeline_parallel, cparams.op_offload
));
if (pipeline_parallel) {
LLAMA_LOG_INFO("%s: pipeline parallelism enabled (n_copies=%d)\n",
__func__, ggml_backend_sched_get_n_copies(sched.get()));
}
}
// Резервируем worst-case графы для инференса
if (!hparams.vocab_only && memory) {
const uint32_t n_seqs = cparams.kv_unified ? 1 : cparams.n_seq_max;
const uint32_t n_tokens = std::min(cparams.n_ctx, cparams.n_ubatch);
LLAMA_LOG_DEBUG("%s: worst-case: n_tokens = %d, n_seqs = %d, n_outputs = %d\n",
__func__, n_tokens, n_seqs, n_outputs);
int n_splits_pp = -1, n_nodes_pp = -1;
int n_splits_tg = -1, n_nodes_tg = -1;
// Инициализируем полный KV-cache
const auto mctx = memory->init_full();
if (!mctx) {
throw std::runtime_error("failed to initialize KV cache");
}
cross.v_embd.clear();
// Резервируем граф для prompt processing
{
auto * gf = graph_reserve(n_tokens, n_seqs, n_tokens, mctx.get());
if (!gf) {
throw std::runtime_error("failed to allocate compute pp buffers");
}
n_splits_pp = ggml_backend_sched_get_n_splits(sched.get());
n_nodes_pp = ggml_graph_n_nodes(gf);
}
// Резервируем граф для token generation
{
auto * gf = graph_reserve(n_seqs, n_seqs, n_seqs, mctx.get());
if (!gf) {
throw std::runtime_error("failed to allocate compute tg buffers");
}
n_splits_tg = ggml_backend_sched_get_n_splits(sched.get());
n_nodes_tg = ggml_graph_n_nodes(gf);
}
// Ещё раз резервируем pp-граф, чтобы избежать realloc в процессе инференса
{
auto * gf = graph_reserve(n_tokens, n_seqs, n_tokens, mctx.get());
if (!gf) {
throw std::runtime_error("failed to allocate compute pp buffers");
}
}
// Логируем размеры буферов для каждого backend
for (size_t i = 0; i < backend_ptrs.size(); ++i) {
ggml_backend_t backend = backend_ptrs[i];
ggml_backend_buffer_type_t buft = backend_buft[i];
size_t size = ggml_backend_sched_get_buffer_size(sched.get(), backend);
if (size > 1) {
LLAMA_LOG_INFO("%s: %10s compute buffer size = %8.2f MiB\n", __func__,
ggml_backend_buft_name(buft), size / 1024.0 / 1024.0);
}
}
// Логируем количество узлов и сплитов графа
if (n_nodes_pp == n_nodes_tg) {
LLAMA_LOG_INFO("%s: graph nodes = %d\n", __func__, n_nodes_pp);
} else {
LLAMA_LOG_INFO("%s: graph nodes = %d (with bs=%d), %d (with bs=1)\n",
__func__, n_nodes_pp, n_tokens, n_nodes_tg);
}
if (n_splits_pp == n_splits_tg) {
LLAMA_LOG_INFO("%s: graph splits = %d\n", __func__, n_splits_pp);
} else {
LLAMA_LOG_INFO("%s: graph splits = %d (with bs=%d), %d (with bs=1)\n",
__func__, n_splits_pp, n_tokens, n_splits_tg);
}
}
}// TODO
long new_batch(
jint n_tokens, // сколько токенов мы хотим передать модели
jint embd, // если 0 — передаём просто ID слов, если больше 0 — передаём готовые вектора (эмбеддинги)
jint n_seq_max, // максимальное количество "потоков" (например, если один токен относится сразу к нескольким фразам)
jint availableRamThresholdMb, // (не используется в этой версии, можно удалить)
jobject lowRamCallback // (тоже не используется здесь)
) {
// Создаём новую структуру, в которую мы запишем всё, что хотим передать модели:
// список токенов или эмбеддингов, их позиции, принадлежность к фразам и флаги для вывода результата.
llama_batch *batch = new llama_batch{
0, // Пока что указываем, что токенов 0 — позже можно будет задать это вручную
nullptr, // Здесь будет список ID слов (если не передаются вектора)
nullptr, // Здесь будут эмбеддинги (если передаются готовые вектора)
nullptr, // Здесь будут позиции токенов в тексте (например, 0, 1, 2...)
nullptr, // Количество "потоков" (цепочек), к которым относится каждый токен
nullptr, // Сами ID этих цепочек
nullptr // Флаги: нужно ли получать ответ (логиты) для каждого токена
};
// Если пользователь передаёт эмбеддинги (вектора чисел), создаём под них память.
// Иначе создаём массив, в который запишем просто ID слов.
if (embd) {
batch->embd = (float *) malloc(sizeof(float) * n_tokens * embd);
} else {
batch->token = (llama_token *) malloc(sizeof(llama_token) * n_tokens);
}
// Для каждого токена создаём ячейку, куда запишется его позиция в тексте — 0, 1, 2 и т.д.
batch->pos = (llama_pos *) malloc(sizeof(llama_pos) * n_tokens);
// Создаём массив, в который будет записано, сколько "потоков" (фраз) связан с каждым токеном.
batch->n_seq_id = (int32_t *) malloc(sizeof(int32_t) * n_tokens);
// Для каждого токена создаём список всех потоков (цепочек), к которым он относится.
batch->seq_id = (llama_seq_id **) malloc(sizeof(llama_seq_id *) * n_tokens);
for (int i = 0; i < n_tokens; ++i) {
batch->seq_id[i] = (llama_seq_id *) malloc(sizeof(llama_seq_id) * n_seq_max);
}
// Создаём массив флагов, где будет указано: нужно ли для этого токена получить логиты (предсказания модели).
batch->logits = (int8_t *) malloc(sizeof(int8_t) * n_tokens);
// Возвращаем указатель на нашу структуру в виде обычного числа — его можно будет использовать в других местах.
return reinterpret_cast<jlong>(batch);
}long new_sampler() {
// Получаем стандартные настройки для цепочки сэмплеров — это объект, который управляет тем,
// как модель выбирает следующее слово или токен в тексте.
auto sparams = llama_sampler_chain_default_params();
// Отключаем сбор статистики производительности — это ускоряет работу, если метрики не нужны.
sparams.no_perf = true;
// Создаём новую цепочку сэмплеров с этими настройками.
llama_sampler *smpl = llama_sampler_chain_init(sparams);
// Добавляем в цепочку самый простой способ генерации — greedy-сэмплер (всегда выбирает токен с максимальной вероятностью).
llama_sampler_chain_add(smpl, llama_sampler_init_greedy());
// Возвращаем указатель на сэмплер в виде числа, чтобы им можно было пользоваться в других частях программы.
return reinterpret_cast<jlong>(smpl);
}Модель загружена, после запроса пользователя приступаем к генерации текста