Llama İle Unreal Engine’de İnteraktif NPC’ler


Uzun zamandır Unreal Engine kullanarak geliştirdiğim bir oyun projesi var. Aslında bu oyun MineCraft’a benzer mantıkta geliştirdiğim bir oyundu. Bu hafta sonu oyuna NPC eklerken daha interaktif NPC’ler ekleyebilir miyim diye düşündüm ve aklıma Llama modellerini bunun için kullanmak geldi. Sanırım bu fikir bir çoğunun aklına gelmiştir. Ben de bu gün bu fikri gerçekledim. Gayet hızlı ve güzel çalışıyor. Kendi projelerine eklemek isteyenler için nasıl yaptığıma dair çok kısa bir özet vereyim.

1-) “llama.cpp” projesini derleme

Compile llama.cpp: llama.cpp projesi gguf tipindeki llama modellerini çalıştırmak için geliştirilmiş açık kaynak bir cpp projesi. Bu projeyi indirerek windows, linux veya mac bilgisayarınızda derleyebilirsiniz. llama.cpp’yi derlemek için gerekli olan araçlar “cmake” ve “microsof visual studio c++” komponentleridir.

git clone https://github.com/ggml-org/llama.cpp.git
cd  llama.cpp 
mkdir build & cd build
cmake ..
cmake --build . --config Debug

Bu işlem ile birlikte build/bin/Debug klasörü altında derlenmiş kütüphane ve çalıştırılabilir dosyaları görebilirsiniz. llama.cpp’yi kendi c++ projenize dahil ederek derleyebilmek için şu kütüphanelere ihtiyacınız var:

  • llama.lib
  • common.lib

Bunlar Linux’de “.a ” uzantısına sahiptirler. Aynı şekilde header dosyalarına ihtiyacınız var. Bu headerlar ise şunlar:

  • llama.h,
  • llama-cpp.h
  • ggml-cpu.h
  • ggml-backend.h
  • ggml-alloc.h
  • ggml.h

2-) llama.cpp kütüphanalerini Unreal Engine Projesine dahil etmek;

Unreal Engine c++ projesine dışarıdan kütüphane dahil etmek için “.buil.cs” uzantılı dosyayı kullanabilirsiniz. Statik kütüphaneleri ve header dosyalarının yerini şu şekilde gösteriyoruz.

// Add any include paths for the plugin
PublicIncludePaths.Add(Path.Combine(ModuleDirectory, "inc"));

// Add any import libraries or static libraries
PublicAdditionalLibraries.Add(Path.Combine(ModuleDirectory, "lib", "llama.lib"));
PublicAdditionalLibraries.Add(Path.Combine(ModuleDirectory, "lib", "common.lib"));

Burada ModuleDirectory, Unreal porojesinin içindeki “Source” klasörüdür. Bu klasöre “lib” ve “inc” isimli iki klasör oluşturup kütüphaneleri ve headerları bunların içine kopyalayın. Daha sonra “.project” dosyasını kullanarak visual studio proje dosyalarını yeniden oluşturun. Böylelikle artık llama.cpp’yi Unreal Engine projeniz içerisinde kullanabilirsiniz.

3-) Dinamik Kütüphaneler ve llama modeli

Öncelikle Unrel Engine üzerinde çalıştırdığınız proje binary dosyasının olduğu klasörde şu dll’leri kopyalamanız gerekiyor.

  • ggml.dll
  • ggml-base.dll
  • ggml-cpu.dll
  • llama.dll

Daha sonra oyunun projesi içerisinde yükleyeceğiniz modeli de buraya koyup buranın yolunu gösterebilirsiniz. 3b modeller yeterince iş görür. Android için derlemek isterseniz 1B modeller daha iyi olur. Bir de llama.cpp’yi arm64 için derlemelisiniz.

Llamayı kullanmak için yazdığım örnek llama servis sınıfını örnek alabilirsiniz:

LlamaExecutor.h

// LlamaExecutor.h
#ifndef LLAMA_EXECUTOR_H
#define LLAMA_EXECUTOR_H

#include "llama.h"
#include <string>
#include <vector>
#include <iostream>
#include <functional>


class LlamaExecutor {
public:
    LlamaExecutor();
    ~LlamaExecutor();


    void setOptions(const std::string& model_path, int ngl = 99, int n_ctx = 2048, float minP = 0.05f, float temp = 0.8f, int topK = 50, float topP = 0.9);
    void setCallBackFunction(std::function<void(const std::string&)> func);

    std::string generateResponse(const std::string& prompt);
    void chat(const std::string &userInput);

private:
    void initializeModel();
    void freeResources();

    std::string _modelPath;
    int _ngl;
    int _nCtx;
    float _minP;
    float _temp;
    int _topK;
    float _topP;

    llama_model* _model;
    llama_context* _ctx;
    llama_sampler* _smpl;
    const llama_vocab* _vocab;
    std::vector<llama_chat_message> _messages;
    std::function<void(const std::string&)> _responseCallbackFunction;

};

#endif // LLAMA_EXECUTOR_H

LlamaExecutor.cpp

// LlamaExecutor.cpp
#include "llama_executor.h"
#include <iostream>
#include <cstring>


LlamaExecutor::LlamaExecutor()
{
    _ngl = 99;
    _nCtx = 2048;
    _minP = 0.05f;
    _temp = 0.8f;
    _topK = 50;
    _topP = 0.9; 
    _model = nullptr;
    _ctx = nullptr;
    _smpl = nullptr;
    _vocab = nullptr;
}

LlamaExecutor::~LlamaExecutor() {
    freeResources();
}

void LlamaExecutor::setOptions(const std::string & modelPath, int ngl, int n_ctx, float minP, float temp, int topK, float topP) {
   
    _modelPath = modelPath;
    _ngl = ngl;
    _nCtx = n_ctx;
    _minP = minP;
    _temp = temp;
    _topK = topK;
    _topP = topP; 

    initializeModel();

}

void LlamaExecutor::setCallBackFunction(std::function<void(const std::string &)> func) {
    _responseCallbackFunction = func;
}

void LlamaExecutor::initializeModel() {

    llama_log_set([](enum ggml_log_level level, const char * text, void * /* user_data */) {
        if (level >= GGML_LOG_LEVEL_ERROR) {
            fprintf(stderr, "%s", text);
        }
    }, nullptr);

    llama_model_params model_params = llama_model_default_params();
    model_params.n_gpu_layers = _ngl;
    _model = llama_model_load_from_file(_modelPath.c_str(), model_params);
    if (!_model) {
        throw std::runtime_error("Error: Unable to load model");
    }
    _vocab = llama_model_get_vocab(_model);
    llama_context_params ctx_params = llama_context_default_params();
    ctx_params.n_ctx = _nCtx;
    ctx_params.n_batch = _nCtx;
    _ctx = llama_init_from_model(_model, ctx_params);
    if (!_ctx) {
        throw std::runtime_error("Error: Failed to create llama_context");
    }
    _smpl = llama_sampler_chain_init(llama_sampler_chain_default_params());
    llama_sampler_chain_add(_smpl, llama_sampler_init_min_p(_minP, 1));
    llama_sampler_chain_add(_smpl, llama_sampler_init_temp(_temp));
    llama_sampler_chain_add(_smpl, llama_sampler_init_top_k(_topK));
    llama_sampler_chain_add(_smpl, llama_sampler_init_top_p(_topP, 1));

    llama_sampler_chain_add(_smpl, llama_sampler_init_dist(LLAMA_DEFAULT_SEED));
}

std::string LlamaExecutor::generateResponse(const std::string& prompt) {
    std::string response;
    const bool is_first = llama_get_kv_cache_used_cells(_ctx) == 0;
    const int n_prompt_tokens = -llama_tokenize(_vocab, prompt.c_str(), prompt.size(), NULL, 0, is_first, true);
    std::vector<llama_token> prompt_tokens(n_prompt_tokens);
    llama_tokenize(_vocab, prompt.c_str(), prompt.size(), prompt_tokens.data(), prompt_tokens.size(), is_first, true);
    llama_batch batch = llama_batch_get_one(prompt_tokens.data(), prompt_tokens.size());
    llama_token new_token_id;
    while (true) {
        if (llama_decode(_ctx, batch)) {
            throw std::runtime_error("Error: Failed to decode");
        }
        new_token_id = llama_sampler_sample(_smpl, _ctx, -1);
        if (llama_vocab_is_eog(_vocab, new_token_id)) {
            break;
        }
        char buf[256];
        int n = llama_token_to_piece(_vocab, new_token_id, buf, sizeof(buf), 0, true);
        if(_responseCallbackFunction)
            _responseCallbackFunction( std::string(buf, n));

        response += std::string(buf, n);
        batch = llama_batch_get_one(&new_token_id, 1);
    }
    if(_responseCallbackFunction)
        _responseCallbackFunction("<end>");

    return response;
}

void LlamaExecutor::chat(const std::string &userInput) {
    std::vector<char> formatted(llama_n_ctx(_ctx));
    int prev_len = 0;

    if (userInput.empty()) return;
    _messages.push_back({"user", strdup(userInput.c_str())});
    const char* tmpl = llama_model_chat_template(_model, nullptr);
    int new_len = llama_chat_apply_template(tmpl, _messages.data(), _messages.size(), true, formatted.data(), formatted.size());
    std::string prompt(formatted.begin() + prev_len, formatted.begin() + new_len);
    _messages.push_back({"assistant", strdup(generateResponse(prompt).c_str())});
    prev_len = new_len;

}

void LlamaExecutor::freeResources() {
    for (auto& msg : _messages) {
        free(const_cast<char*>(msg.content));
    }
    llama_sampler_free(_smpl);
    llama_free(_ctx);
    llama_model_free(_model);
}

main.cpp

#include "llama_executor.h"

void callBack(const std::string &responsePiece){
    std::cout << responsePiece << std::endl;
}

int main(int argc, char** argv) {
    if (argc < 3) {
        std::cerr << "Usage: " << argv[0] << " -m <model_path> [-c context_size] [-ngl n_gpu_layers]" << std::endl;
        return 1;
    }
    std::string model_path;
    int ngl = 99, n_ctx = 2048;
    
    for (int i = 1; i < argc; ++i) {
        if (std::string(argv[i]) == "-m" && i + 1 < argc) {
            model_path = argv[++i];
        } else if (std::string(argv[i]) == "-c" && i + 1 < argc) {
            n_ctx = std::stoi(argv[++i]);
        } else if (std::string(argv[i]) == "-ngl" && i + 1 < argc) {
            ngl = std::stoi(argv[++i]);
        }
    }


    if (model_path.empty()) {
        std::cerr << "Error: Model path required" << std::endl;
        return 1;
    }
    try {
        LlamaExecutor executor;
        executor.setOptions(model_path, ngl, n_ctx);
        executor.setCallBackFunction(callBack);
        executor.chat("hello how are you?");

    } catch (const std::exception& e) {
        std::cerr << "Exception: " << e.what() << std::endl;
        return 1;
    }
    return 0;
}

Önerilen makaleler

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir