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
// mini_infer/core/types.h
#pragma once

#include <cstdint>
#include <string>

namespace mini_infer {
namespace core {

/**
* @brief Status codes
*/
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);

/**
* @brief Device types
*/
enum class DeviceType {
CPU,
CUDA,
UNKNOWN
};

/**
* @brief Device information
*/
struct Device {
DeviceType type{DeviceType::CPU};
int32_t id{0};
std::string to_string() const;
};

} // namespace core
} // namespace mini_infer
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
// mini_infer/core/types.cpp
#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;
}

} // namespace core
} // namespace mini_infer

2. Backend 接口:异构计算的“合约” (backend.h)

这是本章的核心。Backend 是一个纯虚基类,它定义了一个计算后端必须遵守的“合约” (Interface)。

请注意,这个接口继承并扩展Allocator 的职责。它不仅负责 allocate/deallocate,还定义了在该设备上执行的基本操作

(注:为了保持架构的一致性,我们已将 cpu_backend.h 中的 copy_tensorsynchronize 提升到基类接口中,因为它们是所有后端都必须提供的功能。)

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
// mini_infer/backends/backend.h
#pragma once

#include "mini_infer/core/tensor.h"
#include "mini_infer/core/types.h"
#include <memory>

namespace mini_infer {
namespace backends {

/**
* @brief Backend interface abstract class
* Defines the interface that different compute backends (CPU, CUDA, etc.) need to implement
*/
class Backend {
public:
virtual ~Backend() = default;

// 身份标识
virtual core::DeviceType device_type() const = 0;
virtual const char* name() const = 0;

// 内存管理 (来自 Allocator 的职责)
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)知道 CPUBackendCUDABackend 的具体存在。

工厂模式 (Factory Pattern) 是解决之道。BackendFactory 是唯一一个“知道”所有具体后端类的“总管”。

1
2
3
4
5
6
7
8
9
10
11
12
13
// mini_infer/backends/backend.h (接上文)

/**
* @brief Backend factory
*/
class BackendFactory {
public:
static std::shared_ptr<Backend> create_backend(core::DeviceType type);
static std::shared_ptr<Backend> get_default_backend();
};

} // namespace backends
} // namespace mini_infer
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
// mini_infer/backends/backend.cpp
#include "mini_infer/backends/backend.h"
#include "mini_infer/backends/cpu_backend.h"
// #include "mini_infer/backends/cuda_backend.h" // <-- 未来在此添加

namespace mini_infer {
namespace backends {

std::shared_ptr<Backend> BackendFactory::create_backend(core::DeviceType type) {
switch (type) {
case core::DeviceType::CPU:
// 注意:这里返回一个 std::shared_ptr,管理其生命周期
return std::make_shared<CPUBackend>();
case core::DeviceType::CUDA:
// TODO: return std::make_shared<CUDABackend>();
return nullptr;
default:
return nullptr;
}
}

std::shared_ptr<Backend> BackendFactory::get_default_backend() {
// 默认返回 CPU 后端
return create_backend(core::DeviceType::CPU);
}

} // namespace backends
} // namespace mini_infer

这个设计让上层代码变得极其简洁,例如: 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
// mini_infer/backends/cpu_backend.h
#pragma once

#include "mini_infer/backends/backend.h"

namespace mini_infer {
namespace backends {

/**
* @brief CPU backend implementation
*/
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 {
// CPU 是同步设备,无需操作
}

const char* name() const override {
return "CPU";
}
};

} // namespace backends
} // namespace mini_infer

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
// mini_infer/backends/cpu_backend.cpp
#include "mini_infer/backends/cpu_backend.h"
#include "mini_infer/core/allocator.h" // 引入 CPUAllocator
#include <cstring> // 引入 std::memcpy 和 std::memset

namespace mini_infer {
namespace backends {

// --- 内存管理:转发给 CPUAllocator 单例 ---

void* CPUBackend::allocate(size_t size) {
return core::CPUAllocator::get_instance()->allocate(size);
}

void CPUBackend::deallocate(void* ptr) {
core::CPUAllocator::get_instance()->deallocate(ptr);
}

// --- 核心操作:转发给 C 标准库 ---

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()) {
// TODO: 应该返回一个 Status::ERROR_INVALID_ARGUMENT
return;
}
// Tensor::data() 返回 void*
std::memcpy(dst.data(), src.data(), src.size_in_bytes());
}

} // namespace backends
} // namespace mini_infer

总结与展望

至此,Mini-Infer 的架构又上了一个新台阶。我们不仅有了 Tensor (数据),现在还有了 Backend (执行上下文)。

我们的架构现在是:

  1. Tensor 持有数据,它知道自己的 ShapeDataType
  2. Backend (如 CPUBackend) 知道如何 allocate 内存,以及如何 memcpymemset 这块内存。
  3. Tensordata_ (即 shared_ptr<void>) 在创建时,其“自定义删除器”会调用当前 Backenddeallocate 方法。

我们已经拥有了“数据”和“场地”。