Mini-Infer 架构深潜 (2): 抽象 Backend 层 - 解耦异构计算
引言:从“数据”到“执行上下文”
在上一篇文章 中,我们为 Mini-Infer 奠定了数据基石:一个健壮、内存安全的 Tensor 类。我们通过 Allocator 接口解耦了内存的“来源”。
然而,一个现代推理框架不仅要处理“来自哪里”的内存(CPU vs. CUDA),还必须处理“在哪里执行 ”以及“如何操作 ”这些内存。
在 CPU 上,memcpy (内存拷贝) 是一个简单的 std::memcpy。但在 GPU 上,它是一个必须通过 CUDA API 调用的 cudaMemcpy,一个涉及总线通信的复杂异步操作。
本篇,我们将构建 Mini-Infer 的后端抽象层 (Backend) 。这是一个至关重要的层,它将“计算”与“硬件”彻底分离,使我们的框架能够驾驭 CPU、GPU 等不同的异构计算设备。
1. 基础类型:定义框架的“通用语言” (types.h)
在构建抽象层之前,我们需要一套通用的“词汇”来描述状态和设备。types.h 文件为此而生。
Status: 一个强类型的 enum class,用于统一所有 API 的返回码。这比使用布尔值或整数宏健壮得多。
DeviceType: 标识硬件类型 (CPU, CUDA)。
Device: 一个结构体,不仅标识了 DeviceType,还包含了 id。这对于支持多 GPU 系统(例如,CUDA:0, CUDA:1)是必不可少的。
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 37 38 39 40 41 42 43 #pragma once #include <cstdint> #include <string> namespace mini_infer {namespace core {enum class Status { SUCCESS = 0 , ERROR_INVALID_ARGUMENT, ERROR_OUT_OF_MEMORY, ERROR_NOT_IMPLEMENTED, ERROR_RUNTIME, ERROR_BACKEND, ERROR_UNKNOWN }; const char * status_to_string (Status status) ;enum class DeviceType { CPU, CUDA, UNKNOWN }; struct Device { DeviceType type{DeviceType::CPU}; int32_t id{0 }; std::string to_string () const ; }; } }
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 37 #include "mini_infer/core/types.h" namespace mini_infer {namespace core {const char * status_to_string (Status status) { switch (status) { case Status::SUCCESS: return "Success" ; case Status::ERROR_INVALID_ARGUMENT:return "Invalid argument" ; case Status::ERROR_OUT_OF_MEMORY: return "Out of memory" ; case Status::ERROR_NOT_IMPLEMENTED: return "Not implemented" ; case Status::ERROR_RUNTIME: return "Runtime error" ; case Status::ERROR_BACKEND: return "Backend error" ; case Status::ERROR_UNKNOWN: default : return "Unknown error" ; } } std::string Device::to_string () const { std::string result; switch (type) { case DeviceType::CPU: result = "CPU" ; break ; case DeviceType::CUDA: result = "CUDA:" + std::to_string (id); break ; default : result = "Unknown" ; break ; } return result; } } }
2. Backend 接口:异构计算的“合约” (backend.h)
这是本章的核心。Backend 是一个纯虚基类,它定义了一个计算后端 必须遵守的“合约” (Interface)。
请注意,这个接口继承并扩展 了 Allocator 的职责。它不仅负责 allocate/deallocate,还定义了在该设备上执行的基本操作 。
(注:为了保持架构的一致性,我们已将 cpu_backend.h 中的 copy_tensor 和 synchronize 提升到基类接口中,因为它们是所有后端都必须提供的功能。)
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 #pragma once #include "mini_infer/core/tensor.h" #include "mini_infer/core/types.h" #include <memory> namespace mini_infer {namespace backends {class Backend {public : virtual ~Backend () = default ; virtual core::DeviceType device_type () const = 0 ; virtual const char * name () const = 0 ; virtual void * allocate (size_t size) = 0 ; virtual void deallocate (void * ptr) = 0 ; virtual void memcpy (void * dst, const void * src, size_t size) = 0 ; virtual void memset (void * ptr, int value, size_t size) = 0 ; virtual void copy_tensor (core::Tensor& dst, const core::Tensor& src) = 0 ; virtual void synchronize () = 0 ; };
这个设计的关键点:
memcpy / memset: 为什么需要这些?因为 std::memcpy 无法操作 GPU 内存。这个接口强制 CUDABackend 必须提供一个使用 cudaMemcpy 的实现。
synchronize(): 这是异步计算(如 CUDA)的生命线。在 CPU 上它什么也不做,但在 GPU 上,它将是 cudaStreamSynchronize(),用于等待所有异步 CUDA kernel 执行完毕。
copy_tensor(): 这是一个更高级的、知道 Tensor 结构的拷贝。它封装了 memcpy 的调用。
3. BackendFactory:解耦的“入口” (backend.cpp)
我们如何创建 Backend?我们不希望框架的上层代码(如 Net)知道 CPUBackend 或 CUDABackend 的具体存在。
工厂模式 (Factory Pattern) 是解决之道。BackendFactory 是唯一一个“知道”所有具体后端类的“总管”。
1 2 3 4 5 6 7 8 9 10 11 12 13 class BackendFactory {public : static std::shared_ptr<Backend> create_backend (core::DeviceType type) ; static std::shared_ptr<Backend> get_default_backend () ; }; } }
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 #include "mini_infer/backends/backend.h" #include "mini_infer/backends/cpu_backend.h" namespace mini_infer {namespace backends {std::shared_ptr<Backend> BackendFactory::create_backend (core::DeviceType type) { switch (type) { case core::DeviceType::CPU: return std::make_shared <CPUBackend>(); case core::DeviceType::CUDA: return nullptr ; default : return nullptr ; } } std::shared_ptr<Backend> BackendFactory::get_default_backend () { return create_backend (core::DeviceType::CPU); } } }
这个设计让上层代码变得极其简洁,例如: auto backend = BackendFactory::get_default_backend(); Net 类只需要持有这个 std::shared_ptr<Backend>,而无需关心它到底是 CPU 还是 GPU。
4. CPUBackend:第一个具体实现 (cpu_backend.h & .cpp)
最后,我们实现 Backend 接口的第一个具体子类:CPUBackend。
(注:CPUBackend 的职责是“执行”,它本身不需要是单例。它通过调用 CPUAllocator 的单例来“获取”内存。这是一个清晰的职责分离。)
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 37 38 #pragma once #include "mini_infer/backends/backend.h" namespace mini_infer {namespace backends {class CPUBackend : public Backend {public : CPUBackend () = default ; ~CPUBackend () override = default ; core::DeviceType device_type () const override { return core::DeviceType::CPU; } void * allocate (size_t size) override ; void deallocate (void * ptr) override ; void memcpy (void * dst, const void * src, size_t size) override ; void memset (void * ptr, int value, size_t size) override ; void copy_tensor (core::Tensor& dst, const core::Tensor& src) override ; void synchronize () override { } const char * name () const override { return "CPU" ; } }; } }
CPUBackend 的实现非常直接:它的大部分工作都是对 CPUAllocator 和 C 标准库 (cstring) 的转发调用 。
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 37 38 39 40 41 #include "mini_infer/backends/cpu_backend.h" #include "mini_infer/core/allocator.h" #include <cstring> namespace mini_infer {namespace backends {void * CPUBackend::allocate (size_t size) { return core::CPUAllocator::get_instance ()->allocate (size); } void CPUBackend::deallocate (void * ptr) { core::CPUAllocator::get_instance ()->deallocate (ptr); } void CPUBackend::memcpy (void * dst, const void * src, size_t size) { std::memcpy (dst, src, size); } void CPUBackend::memset (void * ptr, int value, size_t size) { std::memset (ptr, value, size); } void CPUBackend::copy_tensor (core::Tensor& dst, const core::Tensor& src) { if (dst.size_in_bytes () != src.size_in_bytes ()) { return ; } std::memcpy (dst.data (), src.data (), src.size_in_bytes ()); } } }
总结与展望
至此,Mini-Infer 的架构又上了一个新台阶。我们不仅有了 Tensor (数据),现在还有了 Backend (执行上下文)。
我们的架构现在是:
Tensor 持有数据,它知道自己的 Shape 和 DataType。
Backend (如 CPUBackend) 知道如何 allocate 内存,以及如何 memcpy 和 memset 这块内存。
Tensor 的 data_ (即 shared_ptr<void>) 在创建时,其“自定义删除器”会调用当前 Backend 的 deallocate 方法。
我们已经拥有了“数据”和“场地”。