1. 项目概述
在大模型SDK开发中,日志系统、数据结构设计和设计模式应用是三个最基础也最关键的环节。最近我在开发一个C++大模型推理SDK时,对这三个方面做了系统性的封装和优化。本文将分享spdlog日志库的二次封装经验、通用数据结构的定义方法,以及策略模式在模型推理中的实际应用。
这个SDK需要处理高并发请求、多种模型格式和复杂的计算流程。日志系统必须满足高性能、线程安全和灵活配置的需求;数据结构要兼顾内存效率和接口统一;而策略模式则用于解耦模型加载和推理流程。下面我会从实际代码出发,详细讲解每个环节的实现细节。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. spdlog日志系统封装
2.1 为什么选择spdlog
在C++生态中,spdlog是目前最成熟的日志库之一。相比log4cxx等方案,它有以下几个优势:
- 头文件only,集成简单
- 性能极高(官方benchmark显示比竞争方案快2-3倍)
- 内置异步日志和多种sink(文件、控制台、syslog等)
- 支持丰富的格式化选项
在我们的性能测试中,单线程每秒可以记录超过1,000,000条日志,完全满足大模型推理的需求。
2.2 基础封装实现
首先创建一个Logger单例类,管理所有日志实例:
cpp复制class Logger {
public:
static Logger& instance() {
static Logger instance;
return instance;
}
void init(const std::string& configPath);
std::shared_ptr<spdlog::logger> getLogger(const std::string& name);
private:
std::unordered_map<std::string, std::shared_ptr<spdlog::logger>> loggers_;
std::mutex mutex_;
};
初始化时从配置文件加载日志配置:
cpp复制void Logger::init(const std::string& configPath) {
// 解析JSON配置
auto config = loadConfig(configPath);
// 创建sinks
std::vector<spdlog::sink_ptr> sinks;
if (config.file.enabled) {
auto file_sink = std::make_shared<spdlog::sinks::rotating_file_sink_mt>(
config.file.path, config.file.max_size, config.file.max_files);
sinks.push_back(file_sink);
}
if (config.console.enabled) {
auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
sinks.push_back(console_sink);
}
// 创建异步logger
auto logger = std::make_shared<spdlog::async_logger>(
"main", begin(sinks), end(sinks),
spdlog::thread_pool(),
spdlog::async_overflow_policy::block);
// 设置日志级别和格式
logger->set_level(config.level);
logger->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%^%l%$] [%n] [%t] %v");
loggers_["main"] = logger;
}
2.3 高级特性封装
2.3.1 结构化日志
大模型推理需要记录大量结构化数据,我们扩展了日志接口:
cpp复制#define LOG_STRUCTURED(level, ...) \
SPDLOG_LOGGER_CALL(logger_, level, __VA_ARGS__)
// 使用示例
LOG_STRUCTURED(spdlog::level::info,
R"({"event": "model_loaded", "model": "{}", "elapsed": {}})",
model_name, load_time);
2.3.2 性能优化技巧
- 避免频繁的日志级别检查:
cpp复制// 不好的写法 - 每次调用都会检查级别
logger->debug("Some debug info: {}", expensive_to_string(obj));
// 好的写法 - 先检查级别
if (logger->should_log(spdlog::level::debug)) {
logger->debug("Some debug info: {}", expensive_to_string(obj));
}
- 使用异步日志时的注意事项:
cpp复制// 初始化时设置合理的队列大小和线程数
spdlog::init_thread_pool(8192, 1); // 8K items, 1 thread
// 溢出策略选择
auto logger = std::make_shared<spdlog::async_logger>(
"async_logger", sinks.begin(), sinks.end(),
spdlog::thread_pool(),
spdlog::async_overflow_policy::block); // 阻塞优于丢弃
提示:在生产环境中,建议将日志级别设置为info以上,避免debug日志影响性能。
3. 通用数据结构设计
3.1 基础数据结构定义
大模型SDK需要处理多种数据类型,我们设计了一个通用的Tensor结构:
cpp复制class Tensor {
public:
enum class DataType {
FLOAT32,
INT32,
INT64,
BOOL
};
Tensor(DataType type, const std::vector<int64_t>& shape);
// 内存管理
void* data() const;
size_t bytes() const;
// 形状操作
const std::vector<int64_t>& shape() const;
size_t numel() const;
// 类型转换
template <typename T>
T* data_as() const {
if (type_ != type_to_enum<T>::value) {
throw std::runtime_error("Type mismatch");
}
return static_cast<T*>(data_.get());
}
private:
DataType type_;
std::vector<int64_t> shape_;
std::shared_ptr<void> data_;
};
3.2 内存管理优化
大模型处理中内存分配非常频繁,我们实现了内存池:
cpp复制class MemoryPool {
public:
static MemoryPool& instance() {
static MemoryPool pool;
return pool;
}
void* allocate(size_t size) {
std::lock_guard<std::mutex> lock(mutex_);
auto it = pools_.lower_bound(size);
if (it != pools_.end() && it->first == size && !it->second.empty()) {
auto ptr = it->second.top();
it->second.pop();
return ptr;
}
return ::malloc(size);
}
void deallocate(void* ptr, size_t size) {
std::lock_guard<std::mutex> lock(mutex_);
pools_[size].push(ptr);
}
private:
std::map<size_t, std::stack<void*>> pools_;
std::mutex mutex_;
};
使用时Tensor的构造函数修改为:
cpp复制Tensor::Tensor(DataType type, const std::vector<int64_t>& shape)
: type_(type), shape_(shape) {
size_t size = numel() * element_size();
data_.reset(MemoryPool::instance().allocate(size),
[this](void* ptr) {
MemoryPool::instance().deallocate(ptr, numel() * element_size());
});
}
3.3 序列化支持
为了支持模型输入输出的网络传输,我们实现了ProtoBuf序列化:
cpp复制class TensorProto {
public:
static tensorflow::TensorProto to_proto(const Tensor& tensor);
static Tensor from_proto(const tensorflow::TensorProto& proto);
};
// 使用示例
auto proto = TensorProto::to_proto(tensor);
std::string serialized = proto.SerializeAsString();
// 反序列化
tensorflow::TensorProto proto;
proto.ParseFromString(serialized);
auto tensor = TensorProto::from_proto(proto);
4. 策略模式在模型推理中的应用
4.1 策略模式基础实现
大模型SDK需要支持多种推理引擎(ONNX Runtime、TensorRT、LibTorch等),我们使用策略模式解耦:
cpp复制class InferenceStrategy {
public:
virtual ~InferenceStrategy() = default;
virtual Tensor forward(const Tensor& input) = 0;
};
class OnnxRuntimeStrategy : public InferenceStrategy {
public:
OnnxRuntimeStrategy(const std::string& model_path);
Tensor forward(const Tensor& input) override;
private:
Ort::Session session_;
};
class TensorRTStrategy : public InferenceStrategy {
// 类似实现
};
4.2 策略工厂
为了简化策略创建,实现一个工厂类:
cpp复制class StrategyFactory {
public:
using Creator = std::function<std::unique_ptr<InferenceStrategy>(const std::string&)>;
static StrategyFactory& instance() {
static StrategyFactory factory;
return factory;
}
void register_creator(const std::string& name, Creator creator) {
creators_[name] = creator;
}
std::unique_ptr<InferenceStrategy> create(
const std::string& name, const std::string& model_path) {
auto it = creators_.find(name);
if (it == creators_.end()) {
throw std::runtime_error("Unknown strategy: " + name);
}
return it->second(model_path);
}
private:
std::unordered_map<std::string, Creator> creators_;
};
// 注册策略
StrategyFactory::instance().register_creator("onnx",
[](const std::string& path) {
return std::make_unique<OnnxRuntimeStrategy>(path);
});
4.3 动态策略切换
在运行时根据模型类型自动选择最优策略:
cpp复制class InferenceContext {
public:
void load_model(const std::string& path) {
std::string ext = get_file_extension(path);
if (ext == "onnx") {
strategy_ = StrategyFactory::instance().create("onnx", path);
} else if (ext == "plan") {
strategy_ = StrategyFactory::instance().create("tensorrt", path);
} else {
throw std::runtime_error("Unsupported model format");
}
}
Tensor forward(const Tensor& input) {
return strategy_->forward(input);
}
private:
std::unique_ptr<InferenceStrategy> strategy_;
};
5. 性能优化与问题排查
5.1 常见性能问题
-
日志性能瓶颈:
- 现象:推理延迟增加,CPU使用率高
- 排查:临时关闭日志观察性能变化
- 解决:减少不必要的日志,使用异步日志
-
内存碎片化:
- 现象:长时间运行后内存占用持续增长
- 排查:使用valgrind或tcmalloc分析
- 解决:使用内存池统一管理
5.2 线程安全注意事项
- 日志线程安全:
cpp复制// 错误的跨线程logger使用
static std::shared_ptr<spdlog::logger> logger;
// 正确的做法
std::shared_ptr<spdlog::logger> get_thread_local_logger() {
thread_local static auto logger = create_logger();
return logger;
}
- 策略模式中的状态管理:
cpp复制class InferenceStrategy {
public:
virtual Tensor forward(const Tensor& input) = 0;
// 不是线程安全的!
void set_option(const std::string& key, const std::string& value);
// 线程安全版本
virtual void update_options(
const std::unordered_map<std::string, std::string>& options) = 0;
};
5.3 调试技巧
- 日志追踪请求流:
cpp复制class RequestTracer {
public:
RequestTracer(const std::string& request_id)
: request_id_(request_id) {
SPDLOG_INFO("[{}] Request started", request_id_);
}
~RequestTracer() {
SPDLOG_INFO("[{}] Request completed in {}ms",
request_id_, timer_.elapsed_ms());
}
private:
std::string request_id_;
Timer timer_;
};
- 策略性能分析:
cpp复制class ProfilingStrategy : public InferenceStrategy {
public:
ProfilingStrategy(std::unique_ptr<InferenceStrategy> inner)
: inner_(std::move(inner)) {}
Tensor forward(const Tensor& input) override {
Timer timer;
auto result = inner_->forward(input);
SPDLOG_INFO("Inference took {}ms", timer.elapsed_ms());
return result;
}
private:
std::unique_ptr<InferenceStrategy> inner_;
};
在实际项目中,这套架构成功支撑了日均百万级的推理请求。日志系统帮助快速定位了90%以上的线上问题,策略模式使得新增推理引擎支持的时间从原来的2周缩短到2天,而通用Tensor结构则统一了前后端的数据交互接口。
