Mini-Infer (29): CUDA 后端支持 (上) — CUDAAllocator 与显存管理
1. 问题背景:CPU 与 GPU 内存管理的差异
在之前的实现中,Mini-Infer 只支持 CPU 推理。现在我们要添加 CUDA 后端支持,首先需要解决的就是 GPU 显存管理。
CPU 和 GPU 内存管理有几个关键差异:
| 特性 |
CPU 内存 |
GPU 显存 |
| 分配函数 |
malloc / aligned_alloc |
cudaMalloc |
| 释放函数 |
free |
cudaFree |
| 分配开销 |
较低 |
较高(需要驱动调用) |
| 默认对齐 |
通常 16 字节 |
256 字节 |
| 访问方式 |
直接访问 |
需要 Kernel 或 cudaMemcpy |
| 错误处理 |
返回 nullptr |
返回 cudaError_t |
为了统一管理,我们需要一个抽象的 Allocator 接口,以及针对 CUDA 的具体实现。
2. Allocator 抽象接口回顾
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
|
class Allocator { public: virtual ~Allocator() = default;
virtual void* allocate(size_t size, size_t alignment) = 0;
virtual void deallocate(void* ptr) = 0;
virtual DeviceType device_type() const = 0; };
|
这个接口足够简单,但足以支持不同的内存分配策略。
3. CUDAAllocator 实现
A. 类定义
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
|
#ifdef MINI_INFER_USE_CUDA
#include <cuda_runtime.h>
class CUDAAllocator : public core::Allocator { public:
explicit CUDAAllocator(int device_id = 0); ~CUDAAllocator() override = default;
CUDAAllocator(const CUDAAllocator&) = delete; CUDAAllocator& operator=(const CUDAAllocator&) = delete;
void* allocate(size_t size, size_t alignment) override; void deallocate(void* ptr) override;
core::DeviceType device_type() const override { return core::DeviceType::CUDA; }
int device_id() const { return device_id_; }
private: int device_id_; };
#endif
|
B. allocate 实现
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
|
CUDAAllocator::CUDAAllocator(int device_id) : device_id_(device_id) { cudaSetDevice(device_id_); }
void* CUDAAllocator::allocate(size_t size, size_t alignment) { if (size == 0) { return nullptr; }
cudaSetDevice(device_id_);
void* ptr = nullptr; cudaError_t status = cudaMalloc(&ptr, size);
if (status != cudaSuccess) { MI_LOG_ERROR("[CUDAAllocator] cudaMalloc failed: " + std::string(cudaGetErrorString(status)) + " (requested " + std::to_string(size) + " bytes)"); return nullptr; }
return ptr; }
|
注意:alignment 参数在当前实现中被忽略,因为 cudaMalloc 默认返回 256 字节对齐的内存。
C. deallocate 实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| void CUDAAllocator::deallocate(void* ptr) { if (ptr == nullptr) { return; }
cudaSetDevice(device_id_);
cudaError_t status = cudaFree(ptr); if (status != cudaSuccess) { MI_LOG_ERROR("[CUDAAllocator] cudaFree failed: " + std::string(cudaGetErrorString(status))); } }
|
D. device_id 管理
在多 GPU 系统中,每个 CUDAAllocator 实例绑定到一个特定的 GPU:
1 2 3 4 5
| auto allocator0 = std::make_shared<CUDAAllocator>(0);
auto allocator1 = std::make_shared<CUDAAllocator>(1);
|
每次分配/释放前都调用 cudaSetDevice,确保操作在正确的 GPU 上执行。
4. 错误处理宏设计
CUDA API 调用可能失败,我们需要统一的错误处理机制。
A. CUDA_CHECK (返回 Status)
1 2 3 4 5 6 7 8 9 10 11
|
#define CUDA_CHECK(call) \ do { \ cudaError_t status = call; \ if (status != cudaSuccess) { \ MI_LOG_ERROR("[CUDA] " + std::string(cudaGetErrorString(status)) + \ " at " + std::string(__FILE__) + ":" + std::to_string(__LINE__)); \ return core::Status::ERROR_RUNTIME; \ } \ } while(0)
|
使用场景:在返回 core::Status 的函数中。
1 2 3 4 5
| core::Status some_function() { CUDA_CHECK(cudaMalloc(&ptr, size)); CUDA_CHECK(cudaMemcpy(dst, src, size, cudaMemcpyHostToDevice)); return core::Status::SUCCESS; }
|
B. CUDA_CHECK_THROW (抛出异常)
1 2 3 4 5 6 7 8 9 10
| #define CUDA_CHECK_THROW(call) \ do { \ cudaError_t status = call; \ if (status != cudaSuccess) { \ std::string error_msg = "[CUDA] " + std::string(cudaGetErrorString(status)) + \ " at " + std::string(__FILE__) + ":" + std::to_string(__LINE__); \ MI_LOG_ERROR(error_msg); \ throw std::runtime_error(error_msg); \ } \ } while(0)
|
使用场景:在构造函数或无法返回 Status 的函数中。
1 2 3 4
| CUDADeviceContext::CUDADeviceContext(int device_id) { CUDA_CHECK_THROW(cudaSetDevice(device_id)); CUDA_CHECK_THROW(cudaStreamCreate(&stream_)); }
|
C. 返回 Status vs 抛出异常的选择
| 场景 |
推荐方式 |
原因 |
| 构造函数 |
抛出异常 |
构造函数无法返回值 |
| 普通成员函数 |
返回 Status |
调用者可以优雅处理 |
| 析构函数 |
静默处理 |
析构函数不应抛出异常 |
| 性能关键路径 |
返回 Status |
异常有性能开销 |
5. 与 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
| class CPUAllocator : public Allocator { public: static CPUAllocator* instance() { static CPUAllocator allocator; return &allocator; }
void* allocate(size_t size, size_t alignment) override { #ifdef _WIN32 return _aligned_malloc(size, alignment); #else void* ptr = nullptr; posix_memalign(&ptr, alignment, size); return ptr; #endif }
void deallocate(void* ptr) override { #ifdef _WIN32 _aligned_free(ptr); #else free(ptr); #endif } };
|
对比:
| 特性 |
CPUAllocator |
CUDAAllocator |
| 模式 |
单例 |
每设备一个实例 |
| 对齐 |
显式指定 |
固定 256 字节 |
| 错误处理 |
返回 nullptr |
返回 nullptr + 日志 |
| 跨平台 |
需要条件编译 |
CUDA API 统一 |
6. 与 Storage 的集成
在 Storage::reset 中,根据设备类型选择分配器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| void Storage::reset(size_t capacity_bytes, DeviceType device, size_t alignment) { auto allocator_type = device == DeviceType::CPU ? AllocatorFactory::AllocatorType::CPU : AllocatorFactory::AllocatorType::CUDA; auto allocator = AllocatorFactory::get_allocator(allocator_type);
void* ptr = allocator->allocate(aligned_bytes, alignment);
buffer_.reset(ptr, [allocator](void* p) { allocator->deallocate(p); });
#ifdef MINI_INFER_USE_CUDA if (device == DeviceType::CUDA) { cudaMemset(ptr, 0, aligned_bytes); } else #endif { std::memset(ptr, 0, aligned_bytes); } }
|
7. 未来优化方向
当前的 CUDAAllocator 是最简单的实现,每次分配都调用 cudaMalloc。在生产环境中,这可能成为性能瓶颈。
A. 内存池 (Memory Pooling)
预分配一大块显存,然后从中分配小块:
1 2 3 4 5 6 7 8 9 10
| class PooledCUDAAllocator : public Allocator { void* pool_; size_t pool_size_; FreeList free_list_;
void* allocate(size_t size, size_t alignment) override { } };
|
B. 缓存分配器 (Caching Allocator)
类似 PyTorch 的 CUDACachingAllocator:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class CachingCUDAAllocator : public Allocator { std::map<size_t, std::vector<void*>> cached_blocks_;
void* allocate(size_t size, size_t alignment) override { }
void deallocate(void* ptr) override { cached_blocks_[size].push_back(ptr); } };
|
C. 内存统计和追踪
1 2 3 4 5 6 7 8 9 10 11
| class TrackedCUDAAllocator : public Allocator { std::atomic<size_t> allocated_bytes_{0}; std::atomic<size_t> peak_bytes_{0};
void* allocate(size_t size, size_t alignment) override { void* ptr = cudaMalloc(...); allocated_bytes_ += size; peak_bytes_ = std::max(peak_bytes_.load(), allocated_bytes_.load()); return ptr; } };
|
8. 总结
本篇我们实现了 CUDA 后端的第一个组件——CUDAAllocator:
- 简单封装:
cudaMalloc / cudaFree 的 RAII 封装。
- 多设备支持:每个分配器绑定到特定的 GPU。
- 错误处理宏:
CUDA_CHECK 和 CUDA_CHECK_THROW 统一错误处理。
- 与 Storage 集成:通过
AllocatorFactory 自动选择分配器。
下一篇,我们将实现 CUDADeviceContext,管理 CUDA Stream、cuDNN Handle 和 cuBLAS Handle,为 GPU 推理提供完整的执行环境。