尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

BGE-Large-Zh+C++高性能语义匹配引擎开发

BGE-Large-Zh+C++高性能语义匹配引擎开发 BGE-Large-ZhC高性能语义匹配引擎开发1. 引言语义匹配是自然语言处理中的核心任务它能让计算机理解文本之间的相似性和关联性。BGE-Large-Zh作为当前最强大的中文语义向量模型在各类评测中都表现出色。但要在实际生产环境中部署特别是对性能有要求的场景Python往往不够快。今天我们就来聊聊怎么用C打造一个高性能的语义匹配引擎既能享受BGE模型的强大能力又能获得C的极致性能。我会手把手带你从环境搭建到多线程优化让你也能开发出工业级的语义匹配系统。2. 环境准备与模型转换2.1 系统要求首先确保你的开发环境满足以下要求操作系统: Ubuntu 20.04 或 CentOS 8编译器: GCC 9.0 或 Clang 10.0内存: 至少16GB RAM模型加载需要GPU: 可选但推荐NVIDIA GPUCUDA 11.02.2 安装必要依赖# 更新系统包 sudo apt update sudo apt upgrade -y # 安装基础开发工具 sudo apt install -y build-essential cmake git wget # 安装ONNX Runtime依赖 sudo apt install -y libprotobuf-dev protobuf-compiler # 如果有GPU安装CUDA工具包 # sudo apt install -y nvidia-cuda-toolkit2.3 模型转换与准备BGE-Large-Zh原始模型是PyTorch格式我们需要先转换为ONNX格式# convert_model.py import torch from transformers import AutoModel, AutoTokenizer model_name BAAI/bge-large-zh tokenizer AutoTokenizer.from_pretrained(model_name) model AutoModel.from_pretrained(model_name) # 示例输入 dummy_input tokenizer(这是一个测试, return_tensorspt) # 导出为ONNX格式 torch.onnx.export( model, tuple(dummy_input.values()), bge-large-zh.onnx, input_names[input_ids, attention_mask, token_type_ids], output_names[last_hidden_state, pooler_output], dynamic_axes{ input_ids: {0: batch_size, 1: sequence_length}, attention_mask: {0: batch_size, 1: sequence_length}, token_type_ids: {0: batch_size, 1: sequence_length} }, opset_version13 )运行转换脚本后你会得到bge-large-zh.onnx文件这就是我们C程序要使用的模型。3. C基础推理实现3.1 项目结构搭建先创建项目目录结构semantic_engine/ ├── CMakeLists.txt ├── include/ │ ├── ModelWrapper.h │ └── TextProcessor.h ├── src/ │ ├── main.cpp │ ├── ModelWrapper.cpp │ └── TextProcessor.cpp └── third_party/ └── onnxruntime/3.2 ONNX Runtime集成首先在CMakeLists.txt中配置项目cmake_minimum_required(VERSION 3.16) project(SemanticEngine) set(CMAKE_CXX_STANDARD 17) # 下载ONNX Runtime include(FetchContent) FetchContent_Declare( onnxruntime URL https://github.com/microsoft/onnxruntime/releases/download/v1.15.1/onnxruntime-linux-x64-1.15.1.tgz ) # 添加可执行文件 add_executable(semantic_engine src/main.cpp src/ModelWrapper.cpp src/TextProcessor.cpp ) # 包含目录 target_include_directories(semantic_engine PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include ${onnxruntime_SOURCE_DIR}/include ) # 链接库 target_link_libraries(semantic_engine PRIVATE ${onnxruntime_SOURCE_DIR}/lib/libonnxruntime.so )3.3 模型封装类创建模型包装器来管理ONNX Runtime会话// include/ModelWrapper.h #pragma once #include onnxruntime_cxx_api.h #include vector #include string class ModelWrapper { public: ModelWrapper(const std::string model_path); ~ModelWrapper(); std::vectorfloat inference(const std::vectorint64_t input_ids, const std::vectorint64_t attention_mask, const std::vectorint64_t token_type_ids); private: Ort::Env env_; Ort::Session session_; Ort::AllocatorWithDefaultOptions allocator_; std::vectorconst char* input_names_; std::vectorconst char* output_names_; };实现模型推理逻辑// src/ModelWrapper.cpp #include ModelWrapper.h #include iostream ModelWrapper::ModelWrapper(const std::string model_path) : env_(ORT_LOGGING_LEVEL_WARNING, BGE_Inference), session_(env_, model_path.c_str(), Ort::SessionOptions{}) { // 获取输入输出名称 size_t num_input_nodes session_.GetInputCount(); for (size_t i 0; i num_input_nodes; i) { auto name session_.GetInputNameAllocated(i, allocator_); input_names_.push_back(name.get()); name.release(); } size_t num_output_nodes session_.GetOutputCount(); for (size_t i 0; i num_output_nodes; i) { auto name session_.GetOutputNameAllocated(i, allocator_); output_names_.push_back(name.get()); name.release(); } } std::vectorfloat ModelWrapper::inference( const std::vectorint64_t input_ids, const std::vectorint64_t attention_mask, const std::vectorint64_t token_type_ids) { // 准备输入tensor std::vectorint64_t input_shape {1, static_castint64_t(input_ids.size())}; Ort::MemoryInfo memory_info Ort::MemoryInfo::CreateCpu( OrtAllocatorType::OrtArenaAllocator, OrtMemType::OrtMemTypeDefault); std::vectorOrt::Value input_tensors; input_tensors.push_back(Ort::Value::CreateTensorint64_t( memory_info, const_castint64_t*(input_ids.data()), input_ids.size(), input_shape.data(), input_shape.size())); input_tensors.push_back(Ort::Value::CreateTensorint64_t( memory_info, const_castint64_t*(attention_mask.data()), attention_mask.size(), input_shape.data(), input_shape.size())); input_tensors.push_back(Ort::Value::CreateTensorint64_t( memory_info, const_castint64_t*(token_type_ids.data()), token_type_ids.size(), input_shape.data(), input_shape.size())); // 运行推理 auto output_tensors session_.Run( Ort::RunOptions{nullptr}, input_names_.data(), input_tensors.data(), input_tensors.size(), output_names_.data(), output_names_.size()); // 提取输出向量 float* float_array output_tensors[0].GetTensorMutableDatafloat(); size_t tensor_size output_tensors[0].GetTensorTypeAndShapeInfo().GetElementCount(); return std::vectorfloat(float_array, float_array tensor_size); }4. 文本处理与向量化4.1 文本预处理创建文本处理器来处理中文分词和编码// include/TextProcessor.h #pragma once #include vector #include string #include unordered_map class TextProcessor { public: TextProcessor(); struct TokenizedResult { std::vectorint64_t input_ids; std::vectorint64_t attention_mask; std::vectorint64_t token_type_ids; }; TokenizedResult tokenize(const std::string text); private: std::unordered_mapstd::string, int vocab_; void load_vocab(const std::string vocab_path vocab.txt); };4.2 实现分词逻辑// src/TextProcessor.cpp #include TextProcessor.h #include fstream #include sstream #include algorithm TextProcessor::TextProcessor() { load_vocab(); } void TextProcessor::load_vocab(const std::string vocab_path) { // 这里简化实现实际应该加载BGE模型的词汇表 vocab_ { {[CLS], 101}, {[SEP], 102}, {[PAD], 0}, {这, 1000}, {是, 1001}, {一个, 1002}, {测试, 1003} // ... 实际应该有完整的词汇表 }; } TextProcessor::TokenizedResult TextProcessor::tokenize(const std::string text) { TokenizedResult result; // 简单分词逻辑实际应该用更复杂的中文分词 std::vectorstd::string tokens; std::stringstream ss(text); std::string token; while (ss token) { tokens.push_back(token); } // 添加特殊标记 tokens.insert(tokens.begin(), [CLS]); tokens.push_back([SEP]); // 转换为ID for (const auto token : tokens) { if (vocab_.find(token) ! vocab_.end()) { result.input_ids.push_back(vocab_[token]); } else { result.input_ids.push_back(vocab_[[UNK]]); // 未知词 } } // 填充attention mask和token type ids result.attention_mask std::vectorint64_t(result.input_ids.size(), 1); result.token_type_ids std::vectorint64_t(result.input_ids.size(), 0); return result; }5. 多线程推理优化5.1 线程池实现为了充分利用多核CPU我们需要实现线程池// include/ThreadPool.h #pragma once #include vector #include queue #include thread #include mutex #include condition_variable #include future #include functional class ThreadPool { public: ThreadPool(size_t threads); ~ThreadPool(); templateclass F, class... Args auto enqueue(F f, Args... args) - std::futuretypename std::result_ofF(Args...)::type; private: std::vectorstd::thread workers; std::queuestd::functionvoid() tasks; std::mutex queue_mutex; std::condition_variable condition; bool stop; };5.2 线程池实现// src/ThreadPool.cpp #include ThreadPool.h ThreadPool::ThreadPool(size_t threads) : stop(false) { for (size_t i 0; i threads; i) { workers.emplace_back([this] { for (;;) { std::functionvoid() task; { std::unique_lockstd::mutex lock(this-queue_mutex); this-condition.wait(lock, [this] { return this-stop || !this-tasks.empty(); }); if (this-stop this-tasks.empty()) return; task std::move(this-tasks.front()); this-tasks.pop(); } task(); } }); } } templateclass F, class... Args auto ThreadPool::enqueue(F f, Args... args) - std::futuretypename std::result_ofF(Args...)::type { using return_type typename std::result_ofF(Args...)::type; auto task std::make_sharedstd::packaged_taskreturn_type()( std::bind(std::forwardF(f), std::forwardArgs(args)...) ); std::futurereturn_type res task-get_future(); { std::unique_lockstd::mutex lock(queue_mutex); if (stop) throw std::runtime_error(enqueue on stopped ThreadPool); tasks.emplace([task](){ (*task)(); }); } condition.notify_one(); return res; } ThreadPool::~ThreadPool() { { std::unique_lockstd::mutex lock(queue_mutex); stop true; } condition.notify_all(); for (std::thread worker : workers) worker.join(); }5.3 批量推理优化利用线程池进行批量推理// 在ModelWrapper中添加批量推理方法 std::vectorstd::vectorfloat batch_inference( const std::vectorstd::vectorint64_t batch_input_ids, const std::vectorstd::vectorint64_t batch_attention_mask, const std::vectorstd::vectorint64_t batch_token_type_ids) { ThreadPool pool(std::thread::hardware_concurrency()); std::vectorstd::futurestd::vectorfloat results; for (size_t i 0; i batch_input_ids.size(); i) { results.emplace_back( pool.enqueue([this, batch_input_ids, batch_attention_mask, batch_token_type_ids, i] { return this-inference(batch_input_ids[i], batch_attention_mask[i], batch_token_type_ids[i]); }) ); } std::vectorstd::vectorfloat embeddings; for (auto result : results) { embeddings.push_back(result.get()); } return embeddings; }6. 内存管理与优化6.1 对象池技术为了避免频繁的内存分配使用对象池来管理常用对象// include/ObjectPool.h #pragma once #include queue #include mutex templatetypename T class ObjectPool { public: ObjectPool(size_t initial_size 10) { for (size_t i 0; i initial_size; i) { pool_.push(new T()); } } ~ObjectPool() { while (!pool_.empty()) { delete pool_.front(); pool_.pop(); } } T* acquire() { std::lock_guardstd::mutex lock(mutex_); if (pool_.empty()) { return new T(); } T* obj pool_.front(); pool_.pop(); return obj; } void release(T* obj) { std::lock_guardstd::mutex lock(mutex_); pool_.push(obj); } private: std::queueT* pool_; std::mutex mutex_; };6.2 智能内存管理使用智能指针管理模型资源// 在ModelWrapper中使用智能指针管理会话 class ModelWrapper { private: std::unique_ptrOrt::Session session_; // ... 其他成员 }; // 使用对象池管理输入输出tensor ObjectPoolstd::vectorint64_t input_pool; ObjectPoolstd::vectorfloat output_pool;7. 完整示例与性能测试7.1 主程序实现// src/main.cpp #include ModelWrapper.h #include TextProcessor.h #include ThreadPool.h #include iostream #include chrono int main() { try { // 初始化模型和处理器 ModelWrapper model(bge-large-zh.onnx); TextProcessor processor; // 测试文本 std::vectorstd::string texts { 今天天气真好, 人工智能技术发展迅速, 语义匹配很有用, C性能优化很重要 }; // 批量处理 auto start std::chrono::high_resolution_clock::now(); std::vectorTextProcessor::TokenizedResult tokenized_results; for (const auto text : texts) { tokenized_results.push_back(processor.tokenize(text)); } // 准备批量输入 std::vectorstd::vectorint64_t batch_input_ids; std::vectorstd::vectorint64_t batch_attention_mask; std::vectorstd::vectorint64_t batch_token_type_ids; for (const auto result : tokenized_results) { batch_input_ids.push_back(result.input_ids); batch_attention_mask.push_back(result.attention_mask); batch_token_type_ids.push_back(result.token_type_ids); } // 批量推理 auto embeddings model.batch_inference(batch_input_ids, batch_attention_mask, batch_token_type_ids); auto end std::chrono::high_resolution_clock::now(); auto duration std::chrono::duration_caststd::chrono::milliseconds(end - start); std::cout 批量处理 texts.size() 个文本耗时: duration.count() ms std::endl; // 输出前几个向量的维度 if (!embeddings.empty()) { std::cout 向量维度: embeddings[0].size() std::endl; std::cout 前5个元素: ; for (int i 0; i 5; i) { std::cout embeddings[0][i] ; } std::cout std::endl; } } catch (const std::exception e) { std::cerr 错误: e.what() std::endl; return 1; } return 0; }7.2 编译与运行创建编译脚本#!/bin/bash # build.sh mkdir -p build cd build cmake .. make -j$(nproc) # 运行程序 ./semantic_engine7.3 性能优化建议根据测试结果你可以进一步优化批处理大小调整找到最适合你硬件的最佳批处理大小量化优化使用FP16或INT8量化减少内存使用算子融合使用ONNX Runtime的图优化功能内存复用重用输入输出缓冲区减少分配开销8. 实际应用建议在实际项目中部署时考虑以下几点部署架构使用gRPC或REST API提供推理服务添加负载均衡和健康检查实现模型热更新机制性能监控添加Prometheus指标收集监控内存使用和推理延迟设置自动扩缩容策略错误处理添加重试机制实现降级策略完善的日志记录开发过程中最大的挑战可能是内存管理和多线程同步建议多用Valgrind和AddressSanitizer检查内存问题多用线程分析工具优化并发性能。整体用下来C调用BGE模型确实比Python快不少特别是在批量处理的时候。内存管理需要多花点心思但性能提升是值得的。如果你需要处理大量文本相似度计算这种方案还是很合适的。获取更多AI镜像想探索更多AI镜像和应用场景访问 CSDN星图镜像广场提供丰富的预置镜像覆盖大模型推理、图像生成、视频生成、模型微调等多个领域支持一键部署。
返回列表