Привет,
давно хотел написать о популярном движке, или бэкенде, да запуска 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); }
Модель загружена, после запроса пользователя приступаем к генерации текста