上篇文章我們本地編譯運行了 llama.cpp,跑通了本地模型的運行。但是用的是 llama.cpp 自帶的調(diào)用模型和運行程序,今天,我們不使用 llama.cpp 自帶的 main 程序,我們要自己寫代碼調(diào)用 libllama 庫。
0. 系列文章
- · 【端側(cè)AI 與 C++】1. llama.cpp源碼編譯與本地運行
1. 準(zhǔn)備工作
(1)直接在 llama.cpp 根目錄(你可以自己選擇喜歡的目錄)下創(chuàng)建一個新目錄 HelloAI:
mkdir HelloAI

(2)在 HelloAI 目錄下,創(chuàng)建 main.cpp 主程序文件和 CMakeLists.txt 文件。
(3)main.cpp 和 CMakeLists.txt 文件寫入最小化代碼,先保證可運行。
#include <iostream>
int main(int argc, char** argv)
{
std::cout << "Hello World!" << std::endl;
return 0;
}
cmake_minimum_required(VERSION 3.10)
project(HelloAI)
# 設(shè)置C++標(biāo)準(zhǔn)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 添加可執(zhí)行文件
add_executable(HelloAI main.cpp)
cd HelloAI
mkdir build
cd build
cmake ..
make
./HelloAI
運行結(jié)果如下:

至此,準(zhǔn)備工作完成,剩下的任務(wù)就是往里面加代碼,與 llama.cpp 聯(lián)通 !2. 編寫 main.cpp
這是一個最小化的推理代碼。它做了三件事:
- · 加載模型 -> 將文本轉(zhuǎn)為數(shù)字(Tokenize) -> 循環(huán)預(yù)測下一個數(shù)字。
2.1 加載模型
把模型文件從硬盤加載到內(nèi)存中,準(zhǔn)備好計算資源。
(1)模型路徑(上一篇文章中咱們自己下載的模型)
(2)獲取模型默認(rèn)參數(shù)
(3)加載模型文件
std::string model_path = "xxx/models/qwen2.5-0.5b-instruct-q4_k_m.gguf"; // 模型文件絕對路徑
llama_model_params model_params = llama_model_default_params(); // 獲取默認(rèn)模型參數(shù)
// 從文件加載模型,返回模型指針
llama_model* model = llama_model_load_from_file(model_path.c_str(), model_params);
if (!model) { // 模型加載失敗檢查
std::cerr << "Failed to load model: " << model_path << std::endl;
return 1;
}
2.2 將文本轉(zhuǎn)化為數(shù)字(tokenization)
把你寫的中文 Prompt 轉(zhuǎn)換成模型能理解的 Token 序列(整數(shù)數(shù)組)。
(1)獲取模型詞匯表: 模型不認(rèn)識“你好”,它只認(rèn)識數(shù)字。你需要拿到模型的“字典”指針,后面用來把文字轉(zhuǎn)換成數(shù)字。
(2)對 Prompt 進行 Token 化
// 獲取模型的詞匯表(用于tokenization)
const llama_vocab* vocab = llama_model_get_vocab(model);
// 首先獲取token數(shù)量
const int n_prompt = -llama_tokenize(vocab, prompt.c_str(), prompt.size(), nullptr, 0, true, true);
std::vector<llama_token> prompt_tokens(n_prompt); // 存儲token序列的vector
// 執(zhí)行tokenization
if (llama_tokenize(vocab, prompt.c_str(), prompt.size(), prompt_tokens.data(), prompt_tokens.size(), true, true) < 0)
{
std::cerr << "Failed to tokenize prompt" << std::endl;
return 1;
}
2.3 準(zhǔn)備推理
2.3.1 創(chuàng)建推理上下文: 推理的“工作區(qū)”
上下文是模型“思考”時的臨時記憶區(qū)。
- · 設(shè)置 ctx_params.n_ctx(比如 2048),決定模型能記多少東西。
- · 設(shè)置 ctx_params.n_batch,決定一次處理多少 token。
- · 使用 llama_init_from_model 創(chuàng)建上下文對象 ctx。
llama_context_params ctx_params = llama_context_default_params(); // 默認(rèn)上下文參數(shù)
ctx_params.n_ctx = 2048; // 上下文窗口大小(最大token數(shù))
ctx_params.n_batch = n_prompt; // 批處理大小(提升推理效率)
ctx_params.no_perf = false; // 啟用性能統(tǒng)計
// 從模型創(chuàng)建推理上下文
llama_context* ctx = llama_init_from_model(model, ctx_params);
if (!ctx) { // 上下文創(chuàng)建失敗檢查
std::cerr << "Failed to create context" << std::endl;
return 1;
}
2.3.2 配置采樣器:規(guī)定模型“怎么說話”
采樣器決定模型如何選擇下一個詞(是選概率最大的,還是隨機一點?)。
代碼中使用的是 Greedy (貪婪) 策略:永遠(yuǎn)只選概率最大的那個詞(最穩(wěn),但可能缺乏創(chuàng)造力)。
auto sparams = llama_sampler_chain_default_params(); // 默認(rèn)采樣器參數(shù)
sparams.no_perf = false; // 啟用性能統(tǒng)計
llama_sampler* smpl = llama_sampler_chain_init(sparams); // 初始化采樣器鏈
// 添加貪婪采樣策略(每次選擇概率最高的token)
llama_sampler_chain_add(smpl, llama_sampler_init_greedy());
2.4 正式推理
這是最核心的部分。輸入 -> 模型計算 -> 吐出一個詞 -> 再把這個詞輸進去 -> 循環(huán)。
2.4.1 準(zhǔn)備第一批數(shù)據(jù) (Batch)
使用 llama_batch_get_one 把剛才轉(zhuǎn)換好的 prompt_tokens 打包成一個批次。
llama_batch batch = llama_batch_get_one(prompt_tokens.data(), prompt_tokens.size());
2.4.2 處理編碼器模型 (可選,本文案例可以不要)
注:大部分聊天模型(如 Llama, Qwen)不需要這一步,只有 T5 等架構(gòu)需要。
如果模型有 Encoder,先跑 llama_encode,然后準(zhǔn)備 Decoder 的起始 Token。
// 如果是編碼器-解碼器架構(gòu)的模型
if (llama_model_has_encoder(model))
{
// 先編碼prompt
if (llama_encode(ctx, batch))
{
std::cerr << "llama_encode failed" << std::endl;
return1;
}
// 獲取解碼起始token
llama_token decoder_start_token_id = llama_model_decoder_start_token(model);
if (decoder_start_token_id == LLAMA_TOKEN_NULL)
{
// 如果模型沒有指定,使用BOS(begin of sequence) token
decoder_start_token_id = llama_vocab_bos(vocab);
}
// 準(zhǔn)備解碼階段的batch
batch = llama_batch_get_one(&decoder_start_token_id, 1);
}
2.3.4 主推理循環(huán) - 生成文本
直到達到生成的長度限制 n_predict。
// 循環(huán)直到生成足夠token或遇到結(jié)束標(biāo)記
for (int n_pos = 0; n_pos + batch.n_tokens < n_prompt + n_predict; )
{
n_pos += batch.n_tokens; // 更新已處理token位置
}
具體步驟:
2.3.4.1 解碼 (Decode)
if (llama_decode(ctx, batch))
{
std::cerr << "llama_decode failed" << std::endl;
return 1;
}
2.3.4.2 采樣 (Sample)
調(diào)用 llama_sampler_sample 挑出下一個 Token (new_token_id)。
new_token_id = llama_sampler_sample(smpl, ctx, -1);
2.3.4.3 檢查結(jié)束 (Check End)
如果生成的 Token 是“結(jié)束符”(EOG),就跳出循環(huán)。
if (llama_vocab_is_eog(vocab, new_token_id)) {
break; // 遇到結(jié)束標(biāo)記則終止生成
}
2.3.4.4 更新Batch
把這個新生成的 Token 放入 batch,作為下一次推理的輸入。
batch = llama_batch_get_one(&new_token_id, 1);
然后進入下一次循環(huán)。
3. 完整代碼與運行結(jié)果
3.1 CMakeLists.txt 文件加入 llama.cpp 相關(guān)內(nèi)容
這里就不細(xì)講了,不是本文重點。
文件中重點關(guān)注:
- · 如果你跟我一樣是mac電腦,庫的后綴名是
.dylib
cmake_minimum_required(VERSION 3.10)
project(HelloAI)
set(CMAKE_CXX_STANDARD 17)
# 設(shè)置 llama.cpp 的路徑
set(LLAMA_DIR "${CMAKE_SOURCE_DIR}/../../llama.cpp")
# 頭文件路徑
include_directories(${LLAMA_DIR}/include)
include_directories(${LLAMA_DIR}/ggml/include)
# 庫文件路徑 (根據(jù)你的編譯結(jié)果,可能會變)
link_directories(${LLAMA_DIR}/build/src)
link_directories(${LLAMA_DIR}/build/ggml/src)
link_directories(${LLAMA_DIR}/build/common) # 有時候需要 common
add_executable(HelloAI main.cpp)
# 鏈接庫
# 這里使用 glob 暴力鏈接所有 ggml 相關(guān)的靜態(tài)庫,防止漏掉
file(GLOB GGML_LIBS "${LLAMA_DIR}/build/bin/*dylib")
target_link_libraries(HelloAI
${LLAMA_DIR}/build/bin/libllama.dylib
${GGML_LIBS}
)
if (APPLE)
target_link_libraries(HelloAI "-framework Foundation -framework Metal -framework MetalKit")
endif()
3.2 main.cpp 完整代碼 & 詳細(xì)注釋
// 標(biāo)準(zhǔn)庫頭文件引入
#include <iostream> // 用于標(biāo)準(zhǔn)輸入輸出流操作
#include <vector> // 使用動態(tài)數(shù)組容器存儲token序列
#include <string> // 字符串處理
#include <cstring> // C風(fēng)格字符串處理
#include "llama.h" // llama.cpp核心庫,提供LLM推理功能
// -------------------------------------------------------------------------
// 主函數(shù) - 程序入口
// -------------------------------------------------------------------------
int main(int argc, char** argv) {
// 1. 模型路徑和推理參數(shù)配置
std::string model_path = "xxxx/models/qwen2.5-0.5b-instruct-q4_k_m.gguf"; // 模型文件絕對路徑
std::string prompt = "你是誰?"; // 初始提示詞(prompt)
int n_predict = 32; // 最大生成token數(shù)量限制
// 2. 初始化GGML后端計算資源
ggml_backend_load_all(); // 加載所有可用的后端計算設(shè)備(CPU/GPU等)
// 3. 加載LLM模型
llama_model_params model_params = llama_model_default_params(); // 獲取默認(rèn)模型參數(shù)
// model_params.n_gpu_layers = 99; // 開啟Metal GPU加速(適用于macOS)
// 從文件加載模型,返回模型指針
llama_model* model = llama_model_load_from_file(model_path.c_str(), model_params);
if (!model) { // 模型加載失敗檢查
std::cerr << "Failed to load model: " << model_path << std::endl;
return1;
}
// 獲取模型的詞匯表(用于tokenization)
const llama_vocab* vocab = llama_model_get_vocab(model);
// 4. 將提示詞(prompt)轉(zhuǎn)換為token序列
// 4.1 首先獲取token數(shù)量
constint n_prompt = -llama_tokenize(vocab, prompt.c_str(), prompt.size(), nullptr, 0, true, true);
// 4.2 分配空間并執(zhí)行tokenization
std::vector<llama_token> prompt_tokens(n_prompt); // 存儲token序列的vector
if (llama_tokenize(vocab, prompt.c_str(), prompt.size(), prompt_tokens.data(), prompt_tokens.size(), true, true) < 0)
{
std::cerr << "Failed to tokenize prompt" << std::endl;
return1;
}
// 5. 創(chuàng)建推理上下文(context)
llama_context_params ctx_params = llama_context_default_params(); // 默認(rèn)上下文參數(shù)
ctx_params.n_ctx = 2048; // 上下文窗口大小(最大token數(shù))
ctx_params.n_batch = n_prompt; // 批處理大小(提升推理效率)
ctx_params.no_perf = false; // 啟用性能統(tǒng)計
// 從模型創(chuàng)建推理上下文
llama_context* ctx = llama_init_from_model(model, ctx_params);
if (!ctx) { // 上下文創(chuàng)建失敗檢查
std::cerr << "Failed to create context" << std::endl;
return1;
}
// 6. 初始化采樣器(sampler) - 控制文本生成策略
auto sparams = llama_sampler_chain_default_params(); // 默認(rèn)采樣器參數(shù)
sparams.no_perf = false; // 啟用性能統(tǒng)計
llama_sampler* smpl = llama_sampler_chain_init(sparams); // 初始化采樣器鏈
// 添加貪婪采樣策略(每次選擇概率最高的token)
llama_sampler_chain_add(smpl, llama_sampler_init_greedy());
// 打印原始prompt(用于調(diào)試)
for (auto id : prompt_tokens) {
char buf[128];
// 將token轉(zhuǎn)換為可讀文本
int n = llama_token_to_piece(vocab, id, buf, sizeof(buf), 0, true);
if (n < 0) {
fprintf(stderr, "%s: error: failed to convert token to piece\n", __func__);
return1;
}
std::string s(buf, n);
printf("%s", s.c_str()); // 打印token對應(yīng)的文本
}
// 7. 準(zhǔn)備批處理(batch)數(shù)據(jù)
llama_batch batch = llama_batch_get_one(prompt_tokens.data(), prompt_tokens.size());
// 如果是編碼器-解碼器架構(gòu)的模型
// if (llama_model_has_encoder(model))
// {
// // 先編碼prompt
// if (llama_encode(ctx, batch))
// {
// std::cerr << "llama_encode failed" << std::endl;
// return 1;
// }
// // 獲取解碼起始token
// llama_token decoder_start_token_id = llama_model_decoder_start_token(model);
// if (decoder_start_token_id == LLAMA_TOKEN_NULL)
// {
// // 如果模型沒有指定,使用BOS(begin of sequence) token
// decoder_start_token_id = llama_vocab_bos(vocab);
// }
// // 準(zhǔn)備解碼階段的batch
// batch = llama_batch_get_one(&decoder_start_token_id, 1);
// }
// 8. 主推理循環(huán) - 生成文本
constauto t_main_start = ggml_time_us(); // 記錄開始時間(微秒)
int n_decode = 0; // 解碼token計數(shù)器
llama_token new_token_id; // 存儲新生成的token
// 循環(huán)直到生成足夠token或遇到結(jié)束標(biāo)記
for (int n_pos = 0; n_pos + batch.n_tokens < n_prompt + n_predict; )
{
// 執(zhí)行解碼推理
if (llama_decode(ctx, batch))
{
std::cerr << "llama_decode failed" << std::endl;
return1;
}
n_pos += batch.n_tokens; // 更新已處理token位置
// 選擇下一個token
{
// 使用采樣器選擇最可能的下一個token
new_token_id = llama_sampler_sample(smpl, ctx, -1);
// 檢查是否是結(jié)束標(biāo)記(End of Generation)
if (llama_vocab_is_eog(vocab, new_token_id)) {
break; // 遇到結(jié)束標(biāo)記則終止生成
}
// 將token轉(zhuǎn)換為可讀文本
char buf[128];
int n = llama_token_to_piece(vocab, new_token_id, buf, sizeof(buf), 0, true);
if (n < 0) {
std::cerr << "Failed to convert token to piece" << std::endl;
return1;
}
std::string s(buf, n);
std::cout << s; // 輸出生成的文本
// 準(zhǔn)備下一輪推理的batch(單token)
batch = llama_batch_get_one(&new_token_id, 1);
n_decode += 1; // 增加解碼計數(shù)
}
}
// 生成結(jié)束提示
std::cout << "\n\nDone!" << std::endl;
// 9. 性能統(tǒng)計
constauto t_main_end = ggml_time_us(); // 記錄結(jié)束時間
// 計算并輸出生成速度(tokens/秒)
std::cout << "decoded: " << n_decode << " tokens in "
<< (t_main_end - t_main_start) / 1000000.0f << " s, speed: "
<< n_decode / ((t_main_end - t_main_start) / 1000000.0f) << " tok/s"
<< std::endl;
// 打印采樣器和上下文的性能數(shù)據(jù)
llama_perf_sampler_print(smpl);
llama_perf_context_print(ctx);
// 10. 資源清理
llama_sampler_free(smpl); // 釋放采樣器
llama_free(ctx); // 釋放上下文
llama_model_free(model); // 釋放模型
return0; // 程序正常退出
}
3.3 編譯運行
mkdir build
cd build
cmake ..
make
./HelloAI
3.4 運行結(jié)果
可以看到正?;貜?fù)了我的提問,當(dāng)然,被截斷了,因為到達了 n_predict 數(shù)量。

4. 總結(jié)
總結(jié)一下主要步驟:
(1)指路:定好模型路徑字符串。
(2)載入:Load Backend -> Load Model -> Get Vocab。
(3)預(yù)處理:Tokenize (把字符串變數(shù)字)。
(4)配置:Create Context (申請內(nèi)存) -> Init Sampler (設(shè)定規(guī)則)。
(5)跑圈:Batch (填入Prompt) -> Decode (計算) -> Sample (選詞) -> Print -> Next Batch (填入新詞) -> Repeat。
(6)關(guān)燈:Free 所有指針。
本文結(jié)束,至此,我們大體知道了手動調(diào)用一個模型文件的總過程。下篇文章,我們在這個 main.cpp 文件上,深入看一下 llama.cpp 接口的定義,以及一些編碼過程中可能有的疑問,例如: 為什么調(diào)用模型文件是這么一個過程?Batch是什么?等。