Перейти к содержимому

LLama.cpp AI LLM Engine — Как это работает

Привет,
давно хотел написать о популярном движке, или бэкенде, да запуска 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-model.cpp

void llama_backend_init(void) {

    // Включает систему замера времени внутри ggml (особенно важно для Windows).
    // Это нужно для точных измерений времени работы модели, например, чтобы узнать, сколько занял инференс.
    ggml_time_init();
    {

        // Создаём параметры для инициализации "рабочего окружения" (контекста) ggml.
        // 0 — не выделяем память под тензоры, NULL — не передаём свой буфер, false — не используем многопоточность.
        struct ggml_init_params params = { 0, NULL, false };

        // Инициализируем временный контекст ggml.
        // Он нужен, чтобы подготовить внутренние таблицы и вспомогательные структуры, например, для работы с 16-битными числами (float16).
        // Это делается один раз и ускоряет работу модели в будущем.
        struct ggml_context * ctx = ggml_init(params);
        
        // Сразу удаляем временный контекст, так как он больше не нужен.
        // Подготовленные таблицы уже сохранены глобально и будут использоваться дальше.
        ggml_free(ctx);
    }
}
long load_model(auto path_to_model) {
  
    // Получаем параметры по умолчанию для загрузки модели.
    llama_model_params model_params = llama_model_default_params();

    // Загружаем модель из файла по указанному пути, используя заданные параметры.
    auto model = llama_model_load_from_file(path_to_model, model_params);

    // Освобождаем временно выделенную C-строку после использования.
    env->ReleaseStringUTFChars(filename, path_to_model);

    // Возвращаем указатель на модель как целое число (jlong), чтобы Java могла использовать его как "дескриптор".
    return reinterpret_cast<jlong>(model);
}
// Файл llama.cpp

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) {

    // Инициализирует систему измерения времени (важно для профилирования и корректной работы в Windows).
    ggml_time_init();

    // Создаёт объект модели, в который будет загружена информация из файла.
    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;

        // Если устройства не указаны — перебирает все доступные устройства (GPU, RPC и т. д.).
        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:
              
                    // Если устройство — GPU, определяет, является ли оно RPC-сервером.
                    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", оставляет только указанную видеокарту (main_gpu).
    if (params.split_mode == LLAMA_SPLIT_MODE_NONE) {
        if (params.main_gpu < 0) {
          
            // Если не указана видеокарта — очищает список устройств (будет использоваться CPU).
            model->devices.clear();
        } else {
          
            // Проверяет корректность указанного GPU.
            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;
            }
            
            // Оставляет только выбранный GPU.
            ggml_backend_dev_t main_gpu = model->devices[params.main_gpu];
            model->devices.clear();
            model->devices.push_back(main_gpu);
        }
    }

    // Загружает саму модель из файла (тензоры, веса и т. д.).
    const int status = llama_model_load(path_model, splits, *model, params);

    // Если всё успешно — возвращает загруженную модель.
    return model;
}
// Файл llama.cpp

static int llama_model_load(const std::string & fname, std::vector<std::string> & splits, llama_model & model, llama_model_params & params) {
  
    // Обнуляем время загрузки (оно будет рассчитано позже, при первом запуске модели).
    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;

        // Загружаем архитектуру модели (например, количество слоёв, тип attention и т.д.).
        try {
            model.load_arch(ml);
        } catch(const std::exception & e) {
            throw std::runtime_error("error loading model architecture: " + std::string(e.what()));
        }

        // Загружаем гиперпараметры модели (размеры слоёв, embedding и прочее).
        try {
            model.load_hparams(ml);
        } catch(const std::exception & e) {
            throw std::runtime_error("error loading model hyperparameters: " + std::string(e.what()));
        }

        // Загружаем словарь (токенизация, ID токенов, специальная лексика).
        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;
        }

        // Загружаем тензоры модели (веса слоёв, embeddings и т. д.).
        // Если не удалось — возвращаем -2 (ошибка или отмена).
        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;
}
// Файл llama-model.cpp

// Загружает тензоры модели (веса, 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;
}
long new_context(
        jlong jmodel,
        jint jctx_size,
        jint n_threads,
        jint n_threads_batch,
        jint availableRamThresholdMb,
        jobject lowRamCallback
) {
  
    // Преобразуем числовой идентификатор модели обратно в указатель, чтобы работать с загруженной моделью.
    auto model = reinterpret_cast<llama_model *>(jmodel);

    // Берём стандартные настройки для создания нового рабочего контекста модели.
    llama_context_params ctx_params = llama_context_default_params();

    // Указываем, на сколько токенов назад модель может "оглядываться", то есть видеть в текущем контексте.
    ctx_params.n_ctx = static_cast<uint32_t>(jctx_size);

    // Устанавливаем количество потоков, с помощью которых модель будет выполнять основные вычисления.
    ctx_params.n_threads = n_threads;

    // Указываем число потоков, которые будут использоваться при обработке входных данных в виде пакета токенов.
    ctx_params.n_threads_batch = n_threads_batch;

    // Создаём рабочее окружение (контекст), где модель будет выполнять генерацию текста с учётом всех указанных параметров.
    llama_context *context = llama_new_context_with_model(model, ctx_params);

    // Возвращаем адрес созданного контекста как число, чтобы его можно было использовать или передать дальше.
    return reinterpret_cast<jlong>(context);
}
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);
}

Модель загружена, после запроса пользователя приступаем к генерации текста