使用 C++ 运行 LLM¶
本指南解释了如何使用 ExecuTorch 的 C++ runner 库来运行已导出为 .pte
格式的 LLM 模型。Runner 库提供了一个用于 LLM 文本生成的高级 API,负责处理分词、推理和 token 生成。
先决条件¶
在开始之前,请确保您已
使用
export_llm
API 导出了.pte
格式的模型,如 开箱即用导出流行的 LLM 或 导出自定义 LLM 中所述。请参阅 模型元数据 部分,了解需要序列化到
.pte
的重要元数据。
与您的模型兼容的分词器文件
对于 HuggingFace 分词器,这是一个 JSON 文件
tokenizer.json
对于 SentencePiece 分词器,这是一个
tokenizer.model
文件,通常与权重文件放在一起
安装了 CMake 和 C++ 编译器
CMake 版本 3.29 或更高版本
g++ 或 clang 编译器
模型元数据¶
元数据包含在导出步骤中应包含的几个重要配置参数,这些参数将由 runner 库使用。
enable_dynamic_shape
:模型是否支持动态输入形状max_seq_len
:模型可以处理的最大序列长度max_context_len
:KV 缓存的最大上下文长度use_kv_cache
:模型是否使用 KV 缓存以实现高效生成get_bos_id
:序列开始 (Beginning-of-sequence) token IDget_eos_ids
:序列结束 (End-of-sequence) token IDs
导出时添加元数据¶
为确保您的模型具有必要的元数据,您可以在导出时使用导出配置中的 metadata
参数进行指定。
# export_llm
python -m extension.llm.export.export_llm \
--config path/to/config.yaml \
+base.metadata='{"get_bos_id":128000, "get_eos_ids":[128009, 128001], "get_max_context_len":4096}'
构建 Runner 库¶
ExecuTorch LLM runner 库可以使用 CMake 构建。要将其集成到您的项目中,请
将 ExecuTorch 添加到您的 CMake 项目的依赖项中
启用所需的组件(extension_module, extension_tensor 等)
将您的应用程序链接到
extension_llm_runner
库
以下是 CMake 配置的一个简化示例
# Enable required components
set_overridable_option(EXECUTORCH_BUILD_EXTENSION_MODULE ON)
set_overridable_option(EXECUTORCH_BUILD_EXTENSION_TENSOR ON)
set_overridable_option(EXECUTORCH_BUILD_EXTENSION_LLM_RUNNER ON)
# Add ExecuTorch as a dependency
add_subdirectory(executorch)
# Link against the LLM runner library
target_link_libraries(your_app PRIVATE extension_llm_runner)
构建 Llama Runner¶
ExecuTorch 在 examples/models/llama
目录中提供了一个完整的 Llama 模型 C++ runner 示例。此 runner 演示了如何使用 LLM runner 库来运行导出为 .pte
格式的 Llama 模型。
请注意,此 runner 库不仅限于 Llama 模型,还可以用于任何已导出为 .pte
的纯文本、仅解码器的 LLM 模型。
基本用法示例¶
以下是使用 runner 的一个简化示例
#include <executorch/extension/llm/runner/text_llm_runner.h>
using namespace executorch::extension::llm;
int main() {
// Load tokenizer and create runner
auto tokenizer = load_tokenizer("path/to/tokenizer.json", nullptr, std::nullopt, 0, 0);
auto runner = create_text_llm_runner("path/to/model.pte", std::move(tokenizer));
// Load the model
runner->load();
// Configure generation
GenerationConfig config;
config.max_new_tokens = 100;
config.temperature = 0.8f;
// Generate text with streaming output
runner->generate("Hello, world!", config,
[](const std::string& token) { std::cout << token << std::flush; },
nullptr);
return 0;
}
Runner API 架构¶
ExecuTorch LLM runner 库采用模块化架构设计,该架构将文本生成流水线中不同组件之间的关注点分离。
IRunner 接口¶
IRunner
接口(irunner.h
)定义了 LLM 文本生成的核心功能。该接口作为与 LLM 模型交互的主要抽象。
class IRunner {
public:
virtual ~IRunner() = default;
virtual bool is_loaded() const = 0;
virtual runtime::Error load() = 0;
virtual runtime::Error generate(...) = 0;
virtual runtime::Error generate_from_pos(...) = 0;
virtual void stop() = 0;
};
让我们详细检查每个方法。
bool is_loaded() const
检查模型和所有必需的资源是否已加载到内存中并准备好进行推理。在尝试生成文本之前,此方法对于验证 runner 的状态很有用。
runtime::Error load()
加载模型并为其做好推理准备。这包括:
从
.pte
文件加载模型权重初始化任何必要的缓冲区或缓存
准备执行环境
在任何生成尝试之前都应调用此方法。它返回一个 Error
对象,指示成功或失败。
runtime::Error generate(
const std::string& prompt,
const GenerationConfig& config,
std::function<void(const std::string&)> token_callback,
std::function<void(const Stats&)> stats_callback)
用于文本生成的主要方法。它接受:
prompt
:用于生成输入的文本config
:控制生成过程的配置参数token_callback
:一个回调函数,它接收每个生成的 token 作为字符串stats_callback
:一个回调函数,它在生成完成后接收性能统计数据
token 回调函数在每个 token 生成时被调用,允许流式输出。stats 回调函数在生成完成后提供详细的性能指标。
runtime::Error generate_from_pos(
const std::string& prompt,
int64_t start_pos,
const GenerationConfig& config,
std::function<void(const std::string&)> token_callback,
std::function<void(const Stats&)> stats_callback)
generate()
的高级版本,允许从 KV 缓存的特定位置开始生成。这对于从先前的状态继续生成很有用。
void stop()
立即停止生成循环。这通常是从另一个线程调用以中断长时间运行的生成。
GenerationConfig 结构¶
GenerationConfig
结构控制生成过程的各个方面。
struct GenerationConfig {
bool echo = true; // Whether to echo the input prompt in the output
int32_t max_new_tokens = -1; // Maximum number of new tokens to generate
bool warming = false; // Whether this is a warmup run
int32_t seq_len = -1; // Maximum number of total tokens
float temperature = 0.8f; // Temperature for sampling
int32_t num_bos = 0; // Number of BOS tokens to add
int32_t num_eos = 0; // Number of EOS tokens to add
// Helper method to resolve the actual max_new_tokens based on constraints
int32_t resolve_max_new_tokens(int32_t max_context_len, int32_t num_prompt_tokens) const;
};
resolve_max_new_tokens
方法处理基于以下内容确定可以生成多少 token 的逻辑:
模型的最大上下文长度
prompt 中的 token 数量
用户指定的 maximum sequence length 和 maximum new tokens
Tokenizer 支持¶
runner 库通过统一的接口支持多种分词器格式。
std::unique_ptr<tokenizers::Tokenizer> tokenizer = load_tokenizer(
tokenizer_path, // Path to tokenizer file
nullptr, // Optional special tokens
std::nullopt, // Optional regex pattern (for TikToken)
0, // BOS token index
0 // EOS token index
);
支持的分词器格式包括:
HuggingFace Tokenizers:JSON 格式分词器
SentencePiece:
.model
格式分词器TikToken:BPE 分词器
Llama2c:Llama2.c 格式的 BPE 分词器
对于自定义分词器,您可以在 pytorch-labs/tokenizers 存储库中找到实现。
其他 API¶
模型预热¶
为了获得更准确的计时和最佳性能,您应该在实际推理之前执行预热运行。
runner->warmup("Hello world", 10); // Generate 10 tokens as warmup
预热期间:
创建一个具有以下属性的特殊
GenerationConfig
:echo = false
:prompt 不包含在输出中warming = true
:表示这是预热运行max_new_tokens
:设置为要生成的 token 数量
模型将运行整个生成流水线。
加载模型(如果尚未加载)
对 prompt 进行分词
预填充 KV 缓存
生成指定的 token 数量
预热期间的特殊行为:
token 不会显示到控制台。
runner 会记录“正在进行预热运行…”和“预热运行完成!”的消息。
预热后:
Stats
对象会被重置,以清除性能指标。模型将保持加载状态,并准备好进行实际推理。
预热对于准确的基准测试尤为重要,因为第一次推理通常包含一次性初始化成本,这会扭曲性能测量。
内存使用情况监控¶
您可以使用 Stats
对象监控内存使用情况。
std::cout << "RSS after loading: " << get_rss_bytes() / 1024.0 / 1024.0 << " MiB" << std::endl;