手把手教你如何高效地在 MMCV 中贡献算子

简介: 不知道大家在使用 MMCV 的过程中有没有遇到这种情况:MMCV 没有提供自己需要的 CPU/CUDA 算子,于是希望提一个 PR(Pull Request),将这个算子加入 MMCV,但是又不知从何处下手。本文以最简单的 TensorAdd 算子为例,向大家展示为 MMCV 贡献算子的全过程,希望能够帮助大家更好地理解 MMCV 算子的


前言



不知道大家在使用 MMCV 的过程中有没有遇到这种情况:MMCV 没有提供自己需要的 CPU/CUDA 算子,于是希望提一个 PR(Pull Request),将这个算子加入 MMCV,但是又不知从何处下手。本文以最简单的 TensorAdd 算子为例,向大家展示为 MMCV 贡献算子的全过程,希望能够帮助大家更好地理解 MMCV 算子的目录结构,以便更高效地贡献算子。

注意:如果您不太了解提 PR 的流程,可以阅读https://mmcv.readthedocs.io/zh_CN/latest/community/pr.html

1.更新文档



在 docs/en/understand_mmcv/ops.md 以及 docs/zh_cn/understand_mmcv/ops.md 里添加 TensorAdd,该文档用于维护 MMCV 已支持的算子。

2.算子实现



在这篇文章里我们提供了 CPU 算子和 CUDA 算子的实现方法,您可以选择只提供 CPU 算子或者只提供 CUDA 算子或者两种算子都提供。


2.1 提供算子 C++ 接口


在 mmcv/ops/csrc/pytorch/ 目录添加 tensor_add.cpp 文件。

// Copyright(c) OpenMMLab.All rights reserved.
 2#include "pytorch_cpp_helper.hpp"
 3#include "pytorch_device_registry.hpp"
 4
 5void tensor_add_impl(const Tensor input1, const Tensor input2, Tensor output) {
 6  DISPATCH_DEVICE_IMPL(tensor_add_impl, input1, input2, output);
 7}
 8
 9void tensor_add(const Tensor input1, const Tensor input2, Tensor output) {
10  tensor_add_impl(input1, input2, output);
11}

tensor_add 是算子在 C++ 层的接口,而 tensor_add_impl 中的 DISPATCH_DEVICE_IMPL 宏会根据 Tensor 参数的设备类型自动选择 CPU 或 CUDA 的算子实现。DISPATCH_DEVICE_IMPL 宏依赖于REGISTER_DEVICE_IMPL 宏,我们会在下面的 CPU 算子实现和 CUDA 算子实现提到。DISPATCH_DEVICE_IMPL 和 REGISTER_DEVICE_IMPL 都用于算子分发,更多细节见 兼容性文档PR-1463 以及 Dispatcher 机制解析


2.2 CPU 算子实现


在 mmcv/ops/csrc/pytorch/cpu/ 目录添加 tensor_add_cpu.cpp 文件。

// Copyright(c) OpenMMLab.All rights reserved.
 2#include "pytorch_cpp_helper.hpp"
 3#include "pytorch_device_registry.hpp"
 4
 5template <typename T>
 6void tensor_add_cpu_kernel(int N, const T* input1, const T* input2, T* output) {
 7  for (int i = 0; i < N; i++) {
 8    output[i] = input1[i] + input2[i];
 9  }
10}
11
12void TensorAddCPUKernelLaucher(const Tensor input1, const Tensor input2,
13                         Tensor output) {
14  int N = input1.size(0);
15  AT_DISPATCH_FLOATING_TYPES_AND_HALF(
16      input1.scalar_type(), "tensor_add_cpu_kernel", [&] {
17        tensor_add_cpu_kernel<scalar_t>(N, input1.data_ptr<scalar_t>(),
18                                        input2.data_ptr<scalar_t>(),
19                                        output.data_ptr<scalar_t>());
20      });
21}
22
23void tensor_add_cpu(const Tensor input1, const Tensor input2, Tensor output) {
24  TensorAddCPUKernelLaucher(input1, input2, output);
25}
26void tensor_add_impl(const Tensor input1, const Tensor input2, Tensor output);
27
28REGISTER_DEVICE_IMPL(tensor_add_impl, CPU, tensor_add_cpu);

我们看到最后一行 REGISTER_DEVICE_IMPL 宏将 tensor_add_impl 和 tensor_add_cpu 绑定在一起。tensor_add_cpu 调用 TensorAddCPUKernelLaucher,TensorAddCPUKernelLaucher 启动 tensor_add_cpu_kernel 完成算子的计算。


2.3 CUDA 算子实现


CUDA 算子的调用过程和 CPU 算子类似,但是在代码安排上略有不同,比如很多 CUDA Kernel 被放在 mmcv/ops/csrc/common/cuda/ 目录,因为该目录负责管理后端无关可共享的代码(cuda kernel,mlu kernel等)。


2.3.1 算子绑定


在 mmcv/ops/csrc/pytorch/cuda/cudabind.cpp 里添加 TensorAdd 的 CUDA 算子绑定。

// Copyright (c) OpenMMLab. All rights reserved.
 2...
 3void TensorAddCUDAKernelLauncher(const Tensor input1, const Tensor input2,
 4                                 const Tensor output);
 5
 6void tensor_add_cuda(const Tensor input1, const Tensor input2, Tensor output) {
 7  TensorAddCUDAKernelLauncher(input1, input2, output);
 8}
 9
10void tensor_add_impl(const Tensor input1, const Tensor input2, Tensor output);
11
12REGISTER_DEVICE_IMPL(tensor_add_impl, CUDA, tensor_add_cuda);

我们看到最后一行 REGISTER_DEVICE_IMPL 宏将 tensor_add_impl 和 tensor_add_cuda 绑定在一起。tensor_add_cuda 调用 TensorAddCUDAKernelLaucher。

2.3.2 KernelLaucher


在 mmcv/ops/csrc/pytorch/cuda/ 目录添加 tensor_add_cuda.cu 文件。

// Copyright (c) OpenMMLab. All rights reserved.
 2#include <torch/types.h>
 3
 4#include "pytorch_cuda_helper.hpp"
 5#include "tensor_add_cuda_kernel.cuh"
 6
 7void TensorAddCUDAKernelLauncher(const Tensor input1, const Tensor input2,
 8                                 const Tensor output) {
 9  int N = input1.size(0);
10  at::cuda::CUDAGuard device_guard(input1.device());
11  cudaStream_t stream = at::cuda::getCurrentCUDAStream();
12
13  AT_DISPATCH_FLOATING_TYPES_AND_HALF(
14      input1.scalar_type(), "tensor_add_cuda_kernel", ([&] {
15        dim3 blocks(GET_BLOCKS(N));
16        dim3 threads(THREADS_PER_BLOCK);
17        tensor_add_cuda_kernel<<<blocks, threads, 0, stream>>>(
18            N, input1.data_ptr<scalar_t>(), input2.data_ptr<scalar_t>(),
19            output.data_ptr<scalar_t>());
20      }));
21
22  AT_CUDA_CHECK(cudaGetLastError());
23}

TensorAddCUDAKernelLauncher 会启动 tensor_add_cuda_kernel 完成算子的具体操作。其中使用 AT_DISPATCH_FLOATING_TYPES_AND_HALF 宏启动 CUDA Kernel ,该宏内部包装了一个 switch 语句来完成针对张量类型的分派,更多这类宏可见:https://github.com/pytorch/pytorch/blob/HEAD/aten/src/ATen/Dispatch.h#L262


2.3.3 Kernel


在 mmcv/ops/csrc/common/cuda/ 下添加 tensor_add_cuda_kernel.cuh。

// Copyright (c) OpenMMLab. All rights reserved.
 2#ifndef TENSOR_ADD_CUDA_KERNEL_CUH
 3#define TENSOR_ADD_CUDA_KERNEL_CUH
 4
 5#ifdef MMCV_USE_PARROTS
 6#include "parrots_cuda_helper.hpp"
 7#else
 8#include "pytorch_cuda_helper.hpp"
 9#endif
10
11template <typename T>
12__global__ void tensor_add_cuda_kernel(const int N, const T* input1,
13                                       const T* input2, T* output) {
14  CUDA_1D_KERNEL_LOOP(i, N) { output[i] = input1[i] + input2[i]; }
15}
16#endif  // TENSOR_ADD_CUDA_KERNEL_CUH

在这里实现了算子的具体操作。其中 CUDA_1D_KERNEL_LOOP 是 MMCV 提供的一个简写 Kernel Loop 的宏


3. 提供 Python 接口



在完成 C++/CUDA 的算子后,我们需要在 mmcv/ops/csrc/pytorch/pybind.cpp 里实现 C++ 接口和 Python 接口的绑定,从而提供一个 Python 可以调用的接口。

// Copyright (c) OpenMMLab. All rights reserved.
2...
3void tensor_add(const Tensor input1, const Tensor input2, Tensor output);
4
5PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {  
6  ...
7  m.def("tensor_add", &tensor_add, "tensor_add", py::arg("input1"),
8        py::arg("input2"), py::arg("output"));
9}

4. Python 调用算子



在 mmcv/ops/ 下添加 tensor_add.py。

# Copyright (c) OpenMMLab. All rights reserved.
 2import torch
 3from torch.autograd import Function
 4from torch.autograd.function import once_differentiable
 5
 6from ..utils import ext_loader
 7
 8ext_module = ext_loader.load_ext('_ext', ['tensor_add'])
 9
10
11class TensorAdd(Function):
12
13    @staticmethod
14    def forward(ctx, input1: torch.Tensor,
15                input2: torch.Tensor) -> torch.Tensor:
16        """Add two tensor.
17
18        Args:
19            input1 (torch.Tensor): shape (N).
20            input2 (torch.Tensor): shape (N).
21
22        Returns:
23            torch.Tensor: shape (N), tensor of input1 + input2.
24        """
25        assert input1.is_contiguous() and input2.is_contiguous()
26        assert input1.dim() == 1 and input2.dim() == 1
27        assert input1.size(0) == input2.size(0)
28        output = torch.zeros(
29            input1.size(0), dtype=input1.dtype, device=input1.device)
30        ext_module.tensor_add(input1, input2, output)
31        return output
32
33    @staticmethod
34    @once_differentiable
35    def backward(ctx, grad_output):
36        return grad_output, grad_output
37
38
39tensor_add = TensorAdd.apply

mmcv/ops/__init__.py 文件添加对外的接口。

# Copyright (c) OpenMMLab. All rights reserved.
2...
3from .tensor_add import tensor_add
4
5__all__ = [
6    ..., 
7    'tensor_add'
8]


5. 编译 MMCV



参考 从源码编译 MMCV 的步骤重新编译 MMCV


从源码编译 MMCV:https://mmcv.readthedocs.io/zh_CN/latest/get_started/build.html

PyTorch 源码解读之 cpp_extension:

https://zhuanlan.zhihu.com/p/348555597


6. 添加单元测试



import numpy as np
 2import pytest
 3import torch
 4
 5from mmcv.ops import tensor_add
 6
 7
 8@pytest.mark.parametrize('device', [
 9    'cpu',
10    pytest.param(
11        'cuda',
12        marks=pytest.mark.skipif(
13            not torch.cuda.is_available(), reason='requires CUDA support'))
14])
15@pytest.mark.parametrize('dtype', [torch.float, torch.half])
16def test_tensor_add(device, dtype):
17    n = 1024 * 1024
18    input1 = torch.rand(n).type(dtype).to(device).requires_grad_()
19    input2 = torch.rand(n).type(dtype).to(device).requires_grad_()
20    expected_output = (input1 + input2).cpu()
21    output = tensor_add(input1, input2)
22    output.backward(torch.ones_like(output))
23    assert np.allclose(
24        output.detach().cpu(), expected_output.detach(), atol=1e-4)
25    assert np.allclose(input1.grad.cpu().detach(), 1, atol=1e-4)

在终端通过 pytest tests/test_ops/test_tensor_add.py 测试 tensor_add 是否正确。

7. 需要注意的点



1.文件需要添加 copyright。

2.DISPATCH_DEVICE_IMPL 接口参数至少要有一个是 Tensor,否则无法根据设备类型做算子分发。

3.目录安排、代码风格和命名规范等需符合 MMCV 算子的规范,例如 tensor_add_cpu_kernel 不能写成 tensor_add_kernel、tensor_add_cpukernel 等其他形式。

4.如果算子需要反向传播,请编写反向传播算子并根据 自定义 C++ 和 CUDA 扩展 组织 Python 代码。


8. 函数调用流程图



横实线区别 Python 层和 C++ 层,两者之间通过 pybind11 绑定。竖实线区别 C++ 算子和 CUDA 算子,算子的分发由 DISPATCH_DEVICE_IMPL 决定。

640.png


9. 总结



希望本篇文章让您更为深入地了解了如何在 MMCV 中添加自定义算子


文章来源:【OpenMMLab

 2022-02-09 18:07

目录
相关文章
|
2月前
|
机器学习/深度学习 监控 PyTorch
深度学习工程实践:PyTorch Lightning与Ignite框架的技术特性对比分析
在深度学习框架的选择上,PyTorch Lightning和Ignite代表了两种不同的技术路线。本文将从技术实现的角度,深入分析这两个框架在实际应用中的差异,为开发者提供客观的技术参考。
53 7
|
5月前
|
机器学习/深度学习 PyTorch TensorFlow
深度学习框架之争:全面解析TensorFlow与PyTorch在功能、易用性和适用场景上的比较,帮助你选择最适合项目的框架
【8月更文挑战第31天】在深度学习领域,选择合适的框架至关重要。本文通过开发图像识别系统的案例,对比了TensorFlow和PyTorch两大主流框架。TensorFlow由Google开发,功能强大,支持多种设备,适合大型项目和工业部署;PyTorch则由Facebook推出,强调灵活性和速度,尤其适用于研究和快速原型开发。通过具体示例代码展示各自特点,并分析其适用场景,帮助读者根据项目需求和个人偏好做出明智选择。
116 0
|
5月前
|
机器学习/深度学习 自然语言处理 TensorFlow
迁移学习入门指南超棒!教你如何利用预训练模型加速 TensorFlow 项目开发,开启高效之旅!
【8月更文挑战第31天】在深度学习领域,迁移学习是种在资源有限的情况下快速开发高性能模型的技术。本指南介绍如何在TensorFlow中利用预训练模型实现迁移学习,包括选择、加载预训练模型、自定义顶层、冻结预训练层及训练模型等内容,以帮助提升模型性能和训练速度。
98 0
|
8月前
|
机器学习/深度学习 数据采集 数据可视化
探秘scikit-learn:机器学习库的核心功能详解
【4月更文挑战第17天】探索scikit-learn,Python机器学习库,涵盖数据预处理(如标准化、归一化)、模型选择(分类、回归、聚类等)、模型训练、评估与优化(交叉验证、网格搜索)、流水线和集成学习,以及可视化和解释性工具。这个库简化了复杂项目,助力用户高效构建和理解机器学习模型,适合各水平学习者提升技能。
|
8月前
|
机器学习/深度学习 TensorFlow 算法框架/工具
【深度学习】Tensorflow、MindSpore框架介绍及张量算子操作实战(超详细 附源码)
【深度学习】Tensorflow、MindSpore框架介绍及张量算子操作实战(超详细 附源码)
236 0
|
机器学习/深度学习 并行计算 API
【TVM 学习资料】TensorIR 快速入门
【TVM 学习资料】TensorIR 快速入门
212 0
|
算法 PyTorch 算法框架/工具
送你5个MindSpore算子使用经验
送你5个MindSpore算子使用经验
193 0
送你5个MindSpore算子使用经验
|
PyTorch 算法框架/工具
pytorch使用方法积累
1. net.parameters()查看网络参数 2. torch.optim.lr_scheduler.MultiStepLR 2.1 学习率的参数配置
96 0
|
TensorFlow 算法框架/工具
TF学习——Tensorflow框架之基础概念、设计思路、常用方法之详细攻略
TF学习——Tensorflow框架之基础概念、设计思路、常用方法之详细攻略
|
算法框架/工具 TensorFlow 算法
带你读《TensorFlow机器学习实战指南(原书第2版)》之二:TensorFlow进阶
本书由资深数据科学家撰写,从实战角度系统讲解TensorFlow基本概念及各种应用实践。真实的应用场景和数据,丰富的代码实例,详尽的操作步骤,带领读者由浅入深系统掌握TensorFlow机器学习算法及其实现。本书第1章和第2章介绍了关于TensorFlow使用的基础知识,后续章节则针对一些典型算法和典型应用场景进行了实现,并配有较详细的程序说明,可读性非常强。读者如果能对其中代码进行复现,则必定会对TensorFlow的使用了如指掌。