基于Pytorch,如何用CUDA自己写算子?(一)

1、简介

大家都知道,深度学习通过梯度反传去优化网络的参数。Pytorch作为具有代表性的深度学习库,对前向(Forward)和梯度反传(Backward)都封装的非常好。在日常的编程中,只需要调用Pytorch封装好的算子,就可以完成网络的设计和训练。

但是,在一些任务中,有些算法没法用Pytroch封装好的算子去实现,需要自己实现。这个时候,则需要自己去写CUDA代码,自己去写Forward和Backward的代码。这个博客就是基于简单的案例去讲解,如何基于Pytorch,用CUDA自己写算子。

在学习这个博客之前,需要具备一定的CUDA编程的基础。

2、简单案例

2.1 案例介绍

输入:一个张量x;

算子:

目的:基于Pytorch,自己实现该算子,完成forward和backward

这个案例虽然非常简单,非常的好理解,非常合适用于说明写CUDA和封装的过程。

假设我们的输入张量,那么经过该算子,得到的结果应该是

对于该算子,偏导为,则反传到x的梯度应该是

接下来,讲解如何实现代码,并对代码的意思进行简单的讲解。

2.2 案例代码
2.2.1 整体介绍

整个工程结构有以下几个文件:

1、test.py

作用:主函数,用于做测试

2、function_cuda_kernel.cu

作用:编写运行在GPU上的cuda代码,forward和backward的实现就是在这个文件里面

3、function_cuda.cpp

作用:将CUDA函数暴露给Python,python不能直接调用.cu中的C++函数,需要使用pybind11建立绑定关系

4、setup.py

作用:编译.cpp 和 .cu 文件,生成Python可以导入的扩展模块,生成 .so文件,供python调用

5、defined_functions.py

作用:把编译好的 CUDA 前向和反向函数接入 PyTorch 的自动求导系统。使用torch.autograd.Function 组织forward/backward

接下来,讲解每个函数的实现

2.2.2 test.py 文件
复制代码
import torch
from defined_functions import AllFunctions

def main():
    x = torch.tensor( [1.0, 2.0, -3.0, 4.0]).cuda()
    x.requires_grad_(True)

    function = AllFunctions()
    y = function.squareApply(x)
    
    print("前向的结果:", y)

    loss = y.sum()
    # 自动触发自定义 CUDA backward
    loss.backward()

    print("\n反向梯度 x.grad:")
    print(x.grad)

if __name__ == "__main__":
    main()

这个main函数代码比较简单,就是定义了一个张量x,然后调用封装好了的前向函数。同时,自动启动梯度反传的计算,输出x的梯度。

在上面代码中,y = function.squareApply(x) 就是调用forward函数;loss.backward() 则会自己逐步的做梯度反传,并自动执行我们写好的backward函数。

2.2.3 defined_functions.py 文件
复制代码
import torch
import square_cuda_ext

class AllFunctions:
    def squareApply(self, inputTensor: torch.Tensor):
        square = SquareFuction()
        return square.apply(inputTensor)


class SquareFuction(torch.autograd.Function):
    @staticmethod
    def forward(ctx, inputTensor: torch.Tensor):
        if not  inputTensor.is_cuda:
            raise RuntimeError("input_tensor must be a CUDA tensor")

        input_contiguous = inputTensor.contiguous()
        ctx.save_for_backward(input_contiguous)
        
        output = square_cuda_ext.square_forward(input_contiguous)

        return output

    @staticmethod
    def backward(ctx, grad_output):
        (inputTensor,) = ctx.saved_tensors
        grad_output_contiguous = grad_output.contiguous()

        # 调用自己编写的 CUDA backward
        grad_input = square_cuda_ext.square_backward(
            grad_output_contiguous,
            inputTensor,
        )
        return grad_input

在这段代码里面,自己定义了一个类SquareFuction。这个类里面写了一个forward,一个backward,在这两个函数里面,分别调用了用cuda写的forward和backward计算过程。

SquareFuction类的基类是torch.autograd.Function,意思就是这个类是自定义前向和后向是怎么计算的,是pytorch提供的基类。

@staticmethod 表示函数是静态函数,只定义计算规则,不创建普通实例。通过ctx是保存数据。

ctx 是上下文信息,用于保存前向阶段的信息,并在反向阶段取回,用于计算梯度。是Pytorch的torch.autograd.Function专门设计的参数,用于连接前向传播和反向传播。

在forward和backward函数中,分别调用了 square_cuda_ext.square_forward(input_contiguous) 和grad_input = square_cuda_ext.square_backward(grad_output_contiguous,inputTensor) ,也就是我们自己封装的前向和后向函数。

2.2.4 function_cuda.cpp 文件
复制代码
#include <torch/extension.h>

torch::Tensor square_cuda_forward(torch::Tensor input);
torch::Tensor square_cuda_backward(torch::Tensor grad_output, torch::Tensor input);

PYBIND11_MODULE(TORCH_EXTENSION_NAME, m)
{
    m.def("square_forward", &square_cuda_forward);
    m.def("square_backward", &square_cuda_backward);
}

这个代码比较简单,首先是声明了两个cuda 的函数,这两个函数在function_cuda_kernel.cu文件中实现的。然后,利用PYBIND11_MODULE,把 C++ 函数暴露给Python。

在python中调用的时候,就只用调用square_forward和square_backward函数即可,实际执行的是.cu中的square_cuda_forward 和 square_cuda_backward函数。

2.2.5 function_cuda_kernel.cu 文件

这个文件是实现算子的核心文件,代码如下:

复制代码
#include <torch/extension.h>

__global__ void square_forward_kernel(float* input, float* output, int64_t num_elements)
{
    int64_t index = static_cast<int64_t> (blockIdx.x * blockDim.x + threadIdx.x);
    if (index < num_elements)
    {
        float x = input[index];
        output[index] = x * x;
    }
}

__global__ void square_backward_kernel(float* grad_output, float* input, float* grad_input, int64_t num_elements)
{
    int64_t index = static_cast<int64_t> (blockIdx.x * blockDim.x + threadIdx.x);    
    if (index < num_elements)
    {
        float x = input[index];
        float grad_out = grad_output[index];
        grad_input[index] = grad_out * 2.0f * x;
    }
}

torch::Tensor square_cuda_forward(torch::Tensor input)
{
    int64_t num_elements = input.numel();
    int threads_per_block = 256;
    int blocks = static_cast<int> ( (num_elements + threads_per_block - 1) / threads_per_block);
    torch::Tensor output = torch::empty_like(input);
    square_forward_kernel<<<blocks, threads_per_block>>>(input.data_ptr<float>(), output.data_ptr<float>(), num_elements);
    return output;
}

torch::Tensor square_cuda_backward(torch::Tensor grad_output, torch::Tensor input)
{
    torch::Tensor grad_input = torch::empty_like(input);
    int64_t num_elements = input.numel();
    int threads_per_block = 256;
    int blocks = static_cast<int> ( (num_elements + threads_per_block - 1) / threads_per_block);
    square_backward_kernel<<<blocks, threads_per_block>>>(grad_output.data_ptr<float>(), input.data_ptr<float>(), grad_input.data_ptr<float>(), num_elements);
    return grad_input;
}

这个就是用cuda实现了简单的平方计算,以及平方的梯度计算,没啥好多说的。

2.2.6 setup.py文件

这个文件的代码如下:

复制代码
GCC_PATH = "/usr/bin/gcc-11"
GXX_PATH = "/usr/bin/g++-11"

from setuptools import setup
from torch.utils.cpp_extension import (
    BuildExtension,
    CUDAExtension,
)


setup(
    name="function_project",

    ext_modules=[
        CUDAExtension(
            name="square_cuda_ext",

            sources=[
                "function_cuda.cpp",
                "function_cuda_kernel.cu",
            ],

            extra_compile_args={
                "cxx": [
                    "-O3",
                ],
                "nvcc": [
                    "-O3",
                ],
            },
        )
    ],

    cmdclass={
        "build_ext": BuildExtension
    },
)

核心的是 setup() 函数,在里面定义了工程名叫什么(name = "function_project"),这个是定义了最后编译出来的.so文件的名字。

最核心的是ext_modules,这个里面的name决定了最后Python扩展模块的名字,也就是用的时候,是import square_cuda_ext、sources表示的是原文件。extra_compile_args 这个里面是一些编译的设置。

2.3 运行

先输入命令,编译我们写的算子:

python setup.py build_ext --inplace

然后,再直接

python test.py

一切顺利的话,就可以得到如下结果:

前向的结果: tensor( 1., 4., 9., 16., device='cuda:0', grad_fn=<SquareFuctionBackward>)

反向梯度 x.grad:

tensor( 2., 4., -6., 8., device='cuda:0')

相关推荐
大飞记Python7 分钟前
AI大模型Token计费全解析:输入/输出、缓存命中、阶梯计价一文看懂
人工智能·缓存
Dave1205469 分钟前
Day17:Agent Memory高级设计——短期记忆、长期记忆与向量语义记忆
人工智能·学习
40岁资深老架构师尼恩10 分钟前
工业级 AI问数 AST语法 + 知识图谱语义+ 质量规则 三层安全校验 架构设计与实现
人工智能·安全·知识图谱
言乐612 分钟前
Python游戏水平测试辅助系统2
开发语言·windows·python·游戏·django
武子康16 分钟前
旧对话为什么没有恢复代码:Pi Session Tree 与上下文投影
人工智能
武子康17 分钟前
DeepSeek 把 Agent Core 也插件化了:Harness 的真正赌注
人工智能·llm·agent
wu_jing_sheng026 分钟前
哨兵卫星数据下载工具:基于CDSE的PySide6桌面应用开发实践
人工智能
小唔w27 分钟前
2026年AI培训行业观察:主流学习渠道一览
人工智能·学习
美狐美颜sdk30 分钟前
直播APP开发技术架构解析:从音视频流到第三方美颜SDK接入的完整方案
大数据·人工智能·音视频·美颜sdk·美颜api
CIO_Alliance38 分钟前
AI认知系列(3)| 数据、算法、算力、场景四要素协同
人工智能·算法·ipaas·系统集成·企业cio联盟·企业级ai化转型