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

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++, и здесь буду приводить методы на этом языке и постараюсь объяснить, что в них происходит.
Понятно что не все знакомы с С++, но если плюс минус понимаете С подобный синтаксис, должно быть понятно.
Здесь идея в том, чтобы понять общие принципы, как все работает, что примерно происходит.

Не важные или повторяющиеся куски кода я убрал.

Буду писать методы по мере их вызова при инициализации модели и генерации текста.

Загрузка модели

  1. Инициализация бекенда, класс 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);
}

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