Mini-Infer (30): CUDA 后端支持 (中) — CUDADeviceContext 与异构执行环境
1. DeviceContext 抽象回顾
在 Mini-Infer 的架构中,DeviceContext 是执行环境的抽象基类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
class DeviceContext { public: virtual ~DeviceContext() = default;
virtual core::DeviceType device_type() const = 0;
virtual void synchronize() = 0; };
|
对于 CPU,CPUDeviceContext 的实现非常简单(几乎是空的)。但对于 CUDA,我们需要管理更多资源。
2. CUDADeviceContext 核心设计
CUDADeviceContext 是 GPU 推理的执行环境,管理以下资源:
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
|
class CUDADeviceContext : public DeviceContext { public: explicit CUDADeviceContext(int device_id = 0); ~CUDADeviceContext() override;
core::DeviceType device_type() const override { return core::DeviceType::CUDA; } void synchronize() override;
int device_id() const { return device_id_; } cudaStream_t stream() const { return stream_; } cudnnHandle_t cudnn_handle(); cublasHandle_t cublas_handle(); void* get_workspace(size_t size); const cudaDeviceProp& device_properties() const; size_t available_memory() const;
private: int device_id_; cudaStream_t stream_{nullptr}; cudnnHandle_t cudnn_handle_{nullptr}; cublasHandle_t cublas_handle_{nullptr}; cudaDeviceProp device_prop_; void* workspace_{nullptr}; size_t workspace_size_{0}; };
|
3. CUDA Stream 管理
A. 什么是 CUDA Stream?
CUDA Stream 是一个命令队列,GPU 按顺序执行队列中的操作。不同 Stream 之间的操作可以并行执行。
1 2 3 4
| Stream 0: [Kernel A] → [Kernel B] → [Kernel C] Stream 1: [Kernel D] → [Kernel E] ↓ 可以并行执行
|
B. Stream 创建
1 2 3 4 5
|
void CUDADeviceContext::init_stream() { CUDA_CHECK_THROW(cudaStreamCreateWithFlags(&stream_, cudaStreamNonBlocking)); }
|
为什么使用 cudaStreamNonBlocking?
- 默认 Stream(Stream 0)会与其他 Stream 同步。
cudaStreamNonBlocking 创建的 Stream 不会与默认 Stream 同步,允许更好的并行性。
C. 同步操作
1 2 3 4 5 6 7
| void CUDADeviceContext::synchronize() { cudaError_t status = cudaStreamSynchronize(stream_); if (status != cudaSuccess) { MI_LOG_ERROR("[CUDADeviceContext] Stream synchronization failed: " + std::string(cudaGetErrorString(status))); } }
|
使用场景:
- 在读取 GPU 计算结果之前。
- 在测量执行时间时。
- 在跨 Stream 依赖时。
4. cuDNN Handle 管理
A. 为什么需要 Handle?
cuDNN 是 NVIDIA 的深度学习原语库,提供高度优化的卷积、池化等操作。每个 cuDNN 操作都需要一个 cudnnHandle_t。
Handle 的作用:
- 状态管理:保存 cuDNN 的内部状态。
- Stream 绑定:指定操作在哪个 Stream 上执行。
- 资源复用:避免重复创建/销毁的开销。
B. 懒初始化 (Lazy Initialization)
1 2 3 4 5 6 7 8 9 10 11 12
| cudnnHandle_t CUDADeviceContext::cudnn_handle() { if (!cudnn_handle_) { init_cudnn(); } return cudnn_handle_; }
void CUDADeviceContext::init_cudnn() { CUDNN_CHECK_THROW(cudnnCreate(&cudnn_handle_)); CUDNN_CHECK_THROW(cudnnSetStream(cudnn_handle_, stream_)); MI_LOG_INFO("[CUDADeviceContext] cuDNN handle initialized"); }
|
为什么懒初始化?
- 节省资源:如果不使用 cuDNN 操作,就不创建 Handle。
- 加快启动:Context 创建时不需要等待 cuDNN 初始化。
- 按需加载:某些模型可能只用 cuBLAS,不用 cuDNN。
C. Stream 绑定
1
| CUDNN_CHECK_THROW(cudnnSetStream(cudnn_handle_, stream_));
|
这确保所有 cuDNN 操作都在我们的 Stream 上执行,与其他操作正确同步。
5. cuBLAS Handle 管理
A. cuBLAS 的作用
cuBLAS 是 NVIDIA 的线性代数库,提供矩阵乘法(GEMM)、向量操作等。在深度学习中,全连接层和注意力机制大量使用 GEMM。
B. 实现
1 2 3 4 5 6 7 8 9 10 11 12
| cublasHandle_t CUDADeviceContext::cublas_handle() { if (!cublas_handle_) { init_cublas(); } return cublas_handle_; }
void CUDADeviceContext::init_cublas() { CUBLAS_CHECK_THROW(cublasCreate(&cublas_handle_)); CUBLAS_CHECK_THROW(cublasSetStream(cublas_handle_, stream_)); MI_LOG_INFO("[CUDADeviceContext] cuBLAS handle initialized"); }
|
C. 与 cuDNN 的协同
cuDNN 和 cuBLAS 可以共享同一个 Stream,确保操作按正确顺序执行:
1 2 3
| Stream: [cuDNN Conv] → [cuBLAS GEMM] → [cuDNN Pooling] ↓ 顺序执行,无需显式同步
|
6. Workspace 管理 (TensorRT 风格)
A. 什么是 Workspace?
许多 cuDNN 操作需要临时内存(Workspace)来存储中间结果。例如,卷积的 im2col 变换需要额外空间。
B. 动态扩展策略
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| void* CUDADeviceContext::get_workspace(size_t size) { if (size > workspace_size_) { if (workspace_) { cudaFree(workspace_); workspace_ = nullptr; }
cudaError_t status = cudaMalloc(&workspace_, size); if (status != cudaSuccess) { MI_LOG_ERROR("[CUDADeviceContext] Failed to allocate workspace of size " + std::to_string(size) + " bytes"); workspace_size_ = 0; return nullptr; }
workspace_size_ = size; MI_LOG_INFO("[CUDADeviceContext] Allocated workspace: " + std::to_string(size / 1024.0 / 1024.0) + " MB"); }
return workspace_; }
|
设计思想:
- 按需分配:只在需要时分配。
- 只增不减:Workspace 只会增大,不会缩小。
- 复用内存:多个操作共享同一块 Workspace。
C. 为什么不预分配固定大小?
- 模型差异大:不同模型需要的 Workspace 大小差异很大。
- 节省显存:小模型不需要大 Workspace。
- 灵活性:动态形状场景下,Workspace 需求可能变化。
TensorRT 的 IExecutionContext::setDeviceMemory 也采用类似策略。
7. 设备属性查询
A. cudaDeviceProp 缓存
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| void CUDADeviceContext::query_device_properties() { CUDA_CHECK_THROW(cudaGetDeviceProperties(&device_prop_, device_id_));
std::ostringstream oss; oss << "[CUDADeviceContext] Device " << device_id_ << " properties:\n" << " Name: " << device_prop_.name << "\n" << " Compute Capability: " << device_prop_.major << "." << device_prop_.minor << "\n" << " Total Global Memory: " << (device_prop_.totalGlobalMem / 1024.0 / 1024.0) << " MB\n" << " Multiprocessors: " << device_prop_.multiProcessorCount << "\n" << " Max Threads Per Block: " << device_prop_.maxThreadsPerBlock << "\n" << " Warp Size: " << device_prop_.warpSize;
MI_LOG_INFO(oss.str()); }
|
常用属性:
| 属性 |
含义 |
用途 |
name |
GPU 名称 |
日志和调试 |
major/minor |
计算能力 |
选择 Kernel 实现 |
totalGlobalMem |
总显存 |
内存规划 |
multiProcessorCount |
SM 数量 |
并行度优化 |
maxThreadsPerBlock |
最大线程数 |
Kernel 配置 |
warpSize |
Warp 大小 |
内存对齐 |
B. available_memory() 实现
1 2 3 4 5 6 7 8 9 10 11 12
| size_t CUDADeviceContext::available_memory() const { size_t free_bytes = 0; size_t total_bytes = 0;
cudaError_t status = cudaMemGetInfo(&free_bytes, &total_bytes); if (status != cudaSuccess) { MI_LOG_WARNING("[CUDADeviceContext] Failed to query memory info"); return 0; }
return free_bytes; }
|
使用场景:
- 在分配大块内存前检查是否有足够空间。
- 内存不足时的降级策略。
- 性能分析和监控。
8. 资源生命周期管理
A. 构造函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| CUDADeviceContext::CUDADeviceContext(int device_id) : device_id_(device_id) {
CUDA_CHECK_THROW(cudaSetDevice(device_id_));
query_device_properties();
init_stream();
MI_LOG_INFO("[CUDADeviceContext] Initialized on device " + std::to_string(device_id_) + ": " + std::string(device_prop_.name)); }
|
注意:cuDNN 和 cuBLAS Handle 不在构造函数中初始化(懒初始化)。
B. 析构函数
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
| CUDADeviceContext::~CUDADeviceContext() {
if (workspace_) { cudaFree(workspace_); workspace_ = nullptr; }
if (cublas_handle_) { cublasDestroy(cublas_handle_); cublas_handle_ = nullptr; }
if (cudnn_handle_) { cudnnDestroy(cudnn_handle_); cudnn_handle_ = nullptr; }
if (stream_) { cudaStreamDestroy(stream_); stream_ = nullptr; }
MI_LOG_INFO("[CUDADeviceContext] Destroyed context for device " + std::to_string(device_id_)); }
|
RAII 原则:资源在构造时获取,在析构时释放,确保不会泄漏。
9. 错误处理宏
A. CUDNN_CHECK / CUDNN_CHECK_THROW
1 2 3 4 5 6 7 8 9
| #define CUDNN_CHECK(call) \ do { \ cudnnStatus_t status = call; \ if (status != CUDNN_STATUS_SUCCESS) { \ MI_LOG_ERROR("[cuDNN] " + std::string(cudnnGetErrorString(status)) + \ " at " + std::string(__FILE__) + ":" + std::to_string(__LINE__)); \ return core::Status::ERROR_RUNTIME; \ } \ } while(0)
|
B. CUBLAS_CHECK / CUBLAS_CHECK_THROW
1 2 3 4 5 6 7 8 9
| #define CUBLAS_CHECK(call) \ do { \ cublasStatus_t status = call; \ if (status != CUBLAS_STATUS_SUCCESS) { \ MI_LOG_ERROR("[cuBLAS] Error code " + std::to_string(status) + \ " at " + std::string(__FILE__) + ":" + std::to_string(__LINE__)); \ return core::Status::ERROR_RUNTIME; \ } \ } while(0)
|
注意:cuBLAS 没有 cublasGetErrorString 函数,只能输出错误码。
10. 总结
本篇我们实现了 CUDA 后端的核心组件——CUDADeviceContext:
- CUDA Stream 管理:异步执行的基础。
- cuDNN Handle 懒初始化:按需创建,节省资源。
- cuBLAS Handle 懒初始化:与 cuDNN 共享 Stream。
- Workspace 动态扩展:TensorRT 风格的临时内存管理。
- 设备属性查询:为优化决策提供信息。
- RAII 资源管理:确保资源正确释放。
下一篇,我们将看看如何在 Build-Time 预加载权重到 GPU,实现 TensorRT 风格的零开销推理。