从零开始构建PhotonInfer推理引擎2
本文详细解析PhotonInfer项目PR2的核心设计理念和技术实现,聚焦模型权重读取、mmap内存映射、tokenizer机制和张量抽象,带你深入理解现代推理引擎的数据加载和计算技术。
📋 文章导览
项目背景 :PR2的核心定位
设计哲学 :内存映射与零拷贝理念
核心组件 :权重加载、tokenizer、张量抽象、文件格式解析
关键代码 :mmap实现与数据结构详解
性能优化 :为什么mmap能大幅提升性能?
教学价值 :PR2教会了我们什么?
🎯 项目背景:PR2的核心使命 从PR1到PR2:基础设施到数据加载 PR1建立了现代C++20基础设施 ,PR2则实现了高效的数据加载系统 :
1 2 3 4 5 6 ✅ 内存映射权重加载(mmap + RAII) ✅ 多格式模型支持(FP32/INT8量化) ✅ TikTokenizer实现(Base64 + 贪心匹配) ✅ 张量抽象层(Eigen集成) ✅ 完整的文件格式解析
PR2的技术挑战 深度学习推理引擎面临的核心问题:
大模型加载 :7B参数模型需要14GB内存,如何高效加载?
内存效率 :避免数据拷贝,减少内存占用
格式兼容 :支持多种模型格式和量化方案
token处理 :如何将文本转换为模型可理解的token序列?
🧠 设计哲学:内存映射与零拷贝 1. mmap:文件到内存的桥梁 传统文件读取的问题:
1 2 3 4 5 std::ifstream file ("model.bin" , std::ios::binary) ;std::vector<char > buffer (1024 * 1024 * 1024 ) ; file.read (buffer.data (), buffer.size ());
PhotonInfer的mmap理念:
1 2 3 4 void * data = mmap (nullptr , file_size, PROT_READ, MAP_PRIVATE, fd, 0 );
2. RAII包装的内存管理 1 2 3 4 5 6 7 8 9 10 11 class RawModelData {public : virtual ~RawModelData () { if (data_ != nullptr && data_ != MAP_FAILED) { munmap (data_, file_size_); } if (fd_ != -1 ) { close (fd_); } } };
3. 类型安全的权重访问 1 2 3 4 5 6 7 8 template <typename WeightType>class RawModelDataTyped : public RawModelData {public : const WeightType* weight (usize offset) const { return static_cast <const WeightType*>(weight_data_) + offset; } };
🏗️ 核心架构:PR2的五大组件 组件1:模型加载器 (ModelLoader) mmap文件映射的完整流程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 Result<void> ModelLoader::mmap_file( const std::filesystem::path& path, i32& fd, void*& data, usize& file_size) { // 1. 打开文件 fd = open(path.c_str(), O_RDONLY); if (fd == -1) { return Err<void>(ErrorCode::FileNotFound, "Failed to open model file: " + path.string()); } // 2. 获取文件大小 struct stat sb; if (fstat(fd, &sb) == -1) { close(fd); return Err<void>(ErrorCode::IOError, "Failed to get file size"); } file_size = static_cast<usize>(sb.st_size); // 3. 内存映射文件 data = mmap(nullptr, file_size, PROT_READ, MAP_PRIVATE, fd, 0); if (data == MAP_FAILED || data == nullptr) { close(fd); return Err<void>(ErrorCode::IOError, "Failed to mmap model file"); } return Ok(); }
mmap关键参数解析 :
PROT_READ:只读映射
MAP_PRIVATE:私有映射,修改不会写回文件
fd:文件描述符
offset=0:从文件开头映射
模型文件格式解析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Result<ModelLoader::LoadedModel> ModelLoader::load (...) { ModelConfig config; std::fread (&config, sizeof (ModelConfig), 1 , file); i32 group_size = 1 ; if (is_quantized) { std::fread (&group_size, sizeof (i32), 1 , file); } RawModelData* raw_data = is_quantized ? new RawModelDataInt8 () : new RawModelDataFp32 (); mmap_file (model_path, fd, data, file_size); raw_data->weight_data_ = data + weight_offset (is_quantized); }
组件2:原始模型数据 (RawModelData) 模板方法模式的类型安全访问 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class RawModelData {public : virtual const void * weight (usize offset) const = 0 ; protected : void * weight_data_ = nullptr ; }; class RawModelDataFp32 : public RawModelData {public : const void * weight (usize offset) const override { return static_cast <const f32*>(weight_data_) + offset; } }; class RawModelDataInt8 : public RawModelData {public : const void * weight (usize offset) const override { return static_cast <const i8*>(weight_data_) + offset; } };
工厂模式的类型创建 1 2 3 4 5 6 7 8 9 10 11 12 template <typename T>struct RawModelDataFactory ;template <>struct RawModelDataFactory <f32> { using type = RawModelDataFp32; }; template <>struct RawModelDataFactory <i8> { using type = RawModelDataInt8; };
组件3:TikTokenizer实现 Base64解码的编译期优化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 constexpr auto TikTokenizer::build_base64_table () noexcept -> std::array<i32, 256> { std::array<i32, 256> table{}; constexpr std::string_view base64_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" ; for (usize i = 0 ; i < base64_chars.size (); ++i) { table[static_cast <u8>(base64_chars[i])] = static_cast <i32>(i); } return table; } auto TikTokenizer::base64_decode (std::string_view encoded) -> std::string { static constexpr auto lookup_table = build_base64_table (); }
贪心tokenization算法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 auto TikTokenizer::encode(std::string_view text) const -> std::vector<i32> { std::vector<i32> tokens; usize i = 0; while (i < text.length()) { // 贪心策略:优先匹配最长token const usize max_len = std::min(text.length() - i, usize{16}); bool found = false; for (usize len = max_len; len > 0; --len) { const std::string_view substr = text.substr(i, len); const std::string key{substr}; if (auto it = vocab_.find(key); it != vocab_.end()) { tokens.push_back(it->second); i += len; found = true; break; } } if (!found) { // 回退策略:字节级编码 const std::string byte_str{text[i]}; if (auto it = vocab_.find(byte_str); it != vocab_.end()) { tokens.push_back(it->second); } ++i; } } return tokens; }
组件4:张量抽象层 (Tensor) Eigen集成的零拷贝视图 1 2 3 4 5 6 7 8 9 10 11 class Tensor {public : static Result<Tensor> create (std::vector<int32_t > dims, DataType dtype) ; template <typename T> auto matrix_map () -> Eigen::Map<Eigen::Matrix<T, Eigen::Dynamic, Eigen::Dynamic>> { } };
组件5:张量抽象层 (Tensor) Eigen零拷贝集成的设计理念 张量是深度学习的核心数据结构,PR2实现了基于Eigen的张量抽象:
核心设计原则 :
存储vs计算分离 :Tensor管理内存,Eigen::Map提供计算视图
零拷贝访问 :Eigen::Map直接操作底层Buffer数据
类型安全 :编译期类型检查,运行时类型转换
设备抽象 :统一的CPU/CUDA接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Tensor {public : static Result<Tensor> create (std::vector<int32_t > dims, DataType dtype) ; template <typename T> Eigen::Map<Eigen::Matrix<T, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor>> matrix_map (); Buffer buffer_; std::vector<int32_t > dims_; usize size_; DataType dtype_; };
内存布局与步长计算 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 std::vector<usize> Tensor::strides() const { if (dims_.empty()) { return {}; } std::vector<usize> strides(dims_.size()); strides.back() = 1; // 行优先(row-major)步长计算 for (isize i = static_cast<isize>(dims_.size()) - 2; i >= 0; --i) { strides[i] = strides[i + 1] * dims_[i + 1]; } return strides; }
Eigen零拷贝矩阵视图 1 2 3 4 5 6 7 8 9 10 11 12 13 template <typename T>Eigen::Map<Eigen::Matrix<T, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor>> Tensor::matrix_map () { if (ndim () != 2 ) { return Eigen::Map<Eigen::Matrix<T, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor>>( nullptr , 0 , 0 ); } return Eigen::Map<Eigen::Matrix<T, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor>>( ptr <T>(), dims_[0 ], dims_[1 ]); }
使用模式 :
1 2 3 4 5 6 7 8 9 10 11 auto tensor_result = Tensor::create ({2 , 3 }, DataType::Float32);Tensor tensor = std::move (tensor_result.value ()); auto mat = tensor.matrix_map <float >();mat = mat * 2.0f + 1.0f ; auto vec = tensor.vector_map <float >();vec = vec.array ().sqrt ();
张量创建工厂方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 template <typename T>Result<Tensor> Tensor::from_vector (const std::vector<T>& data, DeviceType device) { DataType dtype = cpp_type_to_data_type_v<T>; std::vector<int32_t > dims = {static_cast <int32_t >(data.size ())}; auto result = create (dims, dtype, device); if (!result) return result; Tensor tensor = std::move (result.value ()); if (device == DeviceType::CPU) { std::memcpy (tensor.ptr <T>(), data.data (), data.size () * sizeof (T)); } return Ok (std::move (tensor)); }
Move-only语义与RAII 1 2 3 4 5 6 7 8 9 10 class Tensor {public : Tensor (const Tensor&) = delete ; Tensor& operator =(const Tensor&) = delete ; Tensor (Tensor&&) noexcept = default ; Tensor& operator =(Tensor&&) noexcept = default ; };
📊 性能特性分析 mmap的性能优势 1. 内存效率对比 1 2 3 4 5 传统加载: 磁盘 -> 内核缓冲区 -> 用户缓冲区 -> GPU (多次拷贝) mmap加载: 磁盘 -> 虚拟内存 -> 进程地址空间 (零拷贝,按需加载)
2. 启动时间优化 1 2 3 4 5 auto start = std::chrono::high_resolution_clock::now ();auto result = ModelLoader::load ("llama-7b.bin" );auto end = std::chrono::high_resolution_clock::now ();
3. 内存占用优化
传统方式 :需要额外缓冲区存储整个文件
mmap方式 :只占用虚拟内存地址空间,物理内存按需分配
量化模型支持 1 2 3 4 5 6 7 auto loaded = ModelLoader::load_typed <i8>("model_quant.bin" );auto weights = loaded.raw_data->weight (offset); auto loaded_fp32 = ModelLoader::load_typed <f32>("model.bin" );auto weights_fp32 = loaded_fp32. raw_data->weight (offset);
🎬 实战示例:完整的模型加载流程 示例1:加载Llama模型 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #include "photon/model/model_loader.hpp" #include "photon/model/tokenizer.hpp" void load_and_use_model () { auto tokenizer_result = TikTokenizer::load ("tokenizer.model" ); if (!tokenizer_result) { std::cerr << "Failed to load tokenizer" << std::endl; return ; } TikTokenizer tokenizer = std::move (tokenizer_result.value ()); auto model_result = ModelLoader::load ("llama-7b.bin" , false ); if (!model_result) { std::cerr << "Failed to load model" << std::endl; return ; } auto & loaded = model_result.value (); std::string prompt = "Hello, how are you?" ; auto tokens = tokenizer.encode (prompt); tokens.insert (tokens.begin (), tokenizer.bos_id ()); tokens.push_back (tokenizer.eos_id ()); std::cout << "Encoded " << prompt.size () << " chars into " << tokens.size () << " tokens" << std::endl; }
示例2:量化模型处理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 auto quant_result = ModelLoader::load_typed <i8>("llama-7b-quant.bin" );if (!quant_result) { return ; } auto & loaded = quant_result.value ();const i8* attention_weights = static_cast <const i8*>( loaded.raw_data->weight (attention_offset) ); float scale = 1.0f / loaded.group_size;for (size_t i = 0 ; i < weight_count; ++i) { float dequantized = attention_weights[i] * scale; }
示例3:张量操作与Eigen集成 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 #include "photon/core/tensor.hpp" void tensor_operations () { auto tensor_result = Tensor::create ({2 , 3 }, DataType::Float32); if (!tensor_result) { std::cerr << "Failed to create tensor" << std::endl; return ; } Tensor tensor = std::move (tensor_result.value ()); auto mat = tensor.matrix_map <float >(); mat << 1.0f , 2.0f , 3.0f , 4.0f , 5.0f , 6.0f ; mat = mat * 2.0f + 1.0f ; auto vec = tensor.vector_map <float >(); vec = vec.array ().sqrt (); std::cout << "Shape: [" << tensor.dim (0 ) << ", " << tensor.dim (1 ) << "]" << std::endl; std::cout << "Size: " << tensor.size () << " elements" << std::endl; std::cout << "Data type: " << static_cast <int >(tensor.dtype ()) << std::endl; std::vector<float > data = {1.0f , 2.0f , 3.0f , 4.0f , 5.0f }; auto vec_tensor_result = Tensor::from_vector (data); if (vec_tensor_result) { Tensor vec_tensor = std::move (vec_tensor_result.value ()); std::cout << "Created 1D tensor with " << vec_tensor.size () << " elements" << std::endl; } }
📊 文件格式详解 模型文件结构 1 2 3 4 5 ┌─────────────────┬─────────────────┬─────────────────┐ │ ModelConfig │ Group Size │ Weight Data │ │ (7 * int32) │ (int32) │ (float/int8) │ │ │ [可选] │ │ └─────────────────┴─────────────────┴─────────────────┘
ModelConfig结构 1 2 3 4 5 6 7 8 9 struct ModelConfig { int32_t dim = 0 ; int32_t hidden_dim = 0 ; int32_t layer_num = 0 ; int32_t head_num = 0 ; int32_t kv_head_num = 0 ; int32_t vocab_size = 0 ; int32_t seq_len = 0 ; };
Tokenizer文件格式 1 2 3 4 5 # tokenizer.model 文件格式 base64_encoded_token token_id aGVsbG8= 12345 # "hello" -> 12345 d29ybGQ= 12346 # "world" -> 12346 ...
Base64编码原理 1 2 3 4 5 "hello" -> base64_encode ("hello" ) -> "aGVsbG8=" base64_decode ("aGVsbG8=" ) -> "hello"
🎓 教学价值:PR2教会了我们什么? 1. 系统级编程思维
内存映射 :理解虚拟内存和按需加载
RAII资源管理 :确保系统资源正确释放
零拷贝设计 :性能优化的核心思想
2. 深度学习系统设计
文件格式设计 :如何设计高效的二进制格式
量化支持 :INT8量化的数据流和计算
tokenizer机制 :文本到token的转换算法
张量抽象 :内存管理和计算视图的分离设计
3. 现代C++特性应用
模板元编程 :编译期类型计算
constexpr函数 :编译期常量计算
std::filesystem :跨平台文件操作
4. 性能优化思维
避免内存拷贝 :mmap和零拷贝视图
按需加载 :page fault机制的应用
缓存友好 :数据结构设计的影响
存储vs计算分离 :Tensor内存管理,Eigen计算加速
🚀 PR2的意义:数据加载的艺术 PR2完成了推理引擎最核心的数据加载基础设施 :
高效权重加载 :mmap + RAII实现零拷贝大模型加载
多格式支持 :FP32/INT8量化模型的统一接口
tokenizer集成 :完整的文本预处理流水线
张量抽象 :Eigen零拷贝集成的多维数组管理
类型安全 :编译期检查的模板元编程
这些设计为后续的Transformer推理实现 奠定了坚实基础。
📚 延伸阅读
结语 :PR2展示了如何用系统级编程技巧构建高性能的深度学习基础设施。通过mmap、RAII和类型安全的结合,我们不仅获得了高效的数据加载能力,更重要的是学会了一种性能导向的系统设计思维 。
这正是PhotonInfer项目的魅力所在:用严谨的工程实践,支撑AI推理的无限可能 。
关注我的B站账号,获取更多深度学习系统优化的教学内容!
#PhotonInfer #C++20 #mmap #深度学习 #推理引擎 #从零构建 #TikTokenizer