Pytorch2.x时代,关于C++部署的讨论

1,315次阅读
没有评论

Pytorch2.x时代,关于C++部署的讨论 

Pytorch模型的高性能部署一直是大家讨论的问题,有两点比较重要:

  • 高度优化的算子
  • 可以高效率运行计算图的架构和runtime

高度优化的算子不用多说,TensorRT为什么那么快,因为engine在构建的时候,在每个平台(A10、A100、T4等)上搜索到了最优最快的kernel(实现了一些op)。高效率运行计算图也是很关键的一点,TensorRT构建好engine后,需要libnvinfer.so来驱动,其中实现了什么,在使用过程中很容易猜到:

  • 序列化和反序列化,也就是所谓的生成engine,读取engine
  • 推理engine、多stream运行计算图,管理engine所需要的一些环境,比如显存和中间变量等

为了达到极致的性能,TensorRT的整个运行时都是在C++++环境中,虽然提供了Python-API,但实际调用执行的操作都是在C++中,Python只提供包了一层的作用,算子和执行整个计算图的地方都是C++。

Pytorch2.x时代,关于C++部署的讨论c++ api vs python api

python有快速开发以及验证的优点,但是相比C++来说速度较慢而且比较费内存,一般高性能场景都是使用C++去部署,尽量避免使用python环境。

TORCH 1.x时期的C++部署

torch1.x的实际场景中,一般是搭配使用libtorch + torchscript,这俩在很多生产环境中已经验证过了。

libtorch可以使用C++ API去完成和python中使用pytorch-op实现一样的功能,比如:

#include <ATen/ATen.h>

at::Tensor a = at::ones({22}, at::kInt);
at::Tensor b = at::randn({22});
auto c = a + b.to(at::kInt);

转化为Pytorch就是:

import torch

a = torch.ones((22), dtype=torch.int32)
b = torch.randn((22))
c = a + b.to(torch.int32)

而torchscript则用于trace或者script我们的模型到C++环境中部署,速度方面变化不大,主要是通过torchscript导出的模型可以在C++环境中加载并运行,不需要依赖python了,可以减少一些python的over head:

#include <torch/script.h> // One-stop header.

#include <iostream>
#include <memory>

int main(int argc, const char* argv[]) {
  if (argc != 2) {
    std::cerr << “usage: example-app <path-to-exported-script-module>n”;
    return -1;
  }

  torch::jit::script::Module module;
  try {
    // Deserialize the ScriptModule from a file using torch::jit::load().
    module = torch::jit::load(argv[1]);
  }
  catch (const c10::Error& e) {
    std::cerr << “error loading the modeln”;
    return -1;
  }

  std::cout << “okn”;
}

关于torchscript的解读有不少,这里不赘述了,感兴趣的可以参阅:

  • https://zhuanlan.zhihu.com/p/486914187
  • https://zhuanlan.zhihu.com/p/489090393
  • https://zhuanlan.zhihu.com/p/363319763
  • https://zhuanlan.zhihu.com/p/652193676
  • https://zhuanlan.zhihu.com/p/410507557

TORCH 2.x的C++部署

torch2.0出来的时候,最主要的就是torch.compile的新API,可以直接优化模型。

torch.compile核心是dynamo,dynamo相比torch.jit.trace和torch.jit.script,是一个功能更强大的trace工具[1],trace模型从而优化模型。dynamo出现后,我也很好奇torchscript是否会被废弃?

Pytorch2.x时代,关于C++部署的讨论torchscript

目前看来torchscript还是会继续存在,只是freeze了,功能还会维护,bug还会修,但不会有新功能了。

基于torch.jit.trace的模型导出路径成为过去式了,那么新的基于pt2.0的C++导出方案是啥?

torch官方前一周发了一篇新blog,正式提到了cpp wrapper,核心就是torch.export[2] + cpp wrapper[3]:

  • PyTorch 2.1 Contains New Performance Features for AI Developers[4]

使用cpp wrapper去invoke the generated kernels and external kernels in TorchInductor,可以减少python的overhead,实际测试中,模型速度越快,python overhead占比越大,提升也就越大:

Pytorch2.x时代,关于C++部署的讨论cpp wrapper benchmark

我们都知道torch2.0可以基于triton生成高性能的kernel,例如:

@torch.compile
def opt_foo(x, y):
    a = torch.sin(x)
    b = torch.cos(y)
    return a + b

for _ in range(100):
    opt_foo(torch.randn(10).cuda(), torch.randn(10).cuda())

定义好一个函数后,加上@torch.compile装饰器,执行几次即可得到优化后的模型,默认使用的优化器是TorchInductor,借助depyf[5],我们可以看到优化好后生成的triton代码(GPU端):

import triton
import triton.language as tl
from torch._inductor.ir import ReductionHint
from torch._inductor.ir import TileHint
from torch._inductor.triton_heuristics import AutotuneHint, pointwise
from torch._inductor.utils import instance_descriptor
from torch._inductor import triton_helpers

@pointwise(
    size_hints=[16], 
    filename=__file__,
    triton_meta={‘signature’: {0‘*fp32’1‘*fp32’2‘*fp32’3‘i32’}, ‘device’0‘device_type’‘cuda’‘constants’: {}, ‘configs’: [instance_descriptor(divisible_by_16=(012), equal_to_1=(), ids_of_folded_args=(), divisible_by_8=())]},
    inductor_meta={‘autotune_hints’: set(), ‘kernel_name’‘triton_poi_fused_add_cos_sin_0’‘mutated_arg_names’: []},
    min_elem_per_thread=0
)
@triton.jit
def triton_(in_ptr0, in_ptr1, out_ptr0, xnumel, XBLOCK : tl.constexpr):
    xnumel = 10
    xoffset = tl.program_id(0) * XBLOCK
    xindex = xoffset + tl.arange(0, XBLOCK)[:]
    xmask = xindex < xnumel
    x0 = xindex
    tmp0 = tl.load(in_ptr0 + (x0), xmask)
    tmp2 = tl.load(in_ptr1 + (x0), xmask)
    tmp1 = tl.sin(tmp0)
    tmp3 = tl.cos(tmp2)
    tmp4 = tmp1 + tmp3
    tl.store(out_ptr0 + (x0), tmp4, xmask)

这个triton代码可以直接调用,但是依赖python环境,如果想要切换到C++端,则修改下config:

import torch._inductor.config as config
config.cpp_wrapper = True

后重新执行几次,可以得到生成的cpp调用代码:

#include <ATen/ATen.h>
#include <ATen/core/dispatch/Dispatcher.h>
#include <ATen/native/BinaryOps.h>
#include <torch/csrc/inductor/aoti_torch/tensor_converter.h>
#include <torch/csrc/inductor/inductor_ops.h>
#define reinterpret_tensor torch::inductor::_reinterpret_tensor
#define alloc_from_pool torch::inductor::_alloc_from_pool
#include <c10/util/generic_math.h>

[[maybe_unused]] static int64_t align(int64_t nbytes) {
  return (nbytes + 64 – 1) & -64;
}
#include <filesystem>

#include <c10/cuda/CUDAGuard.h>
#include <c10/cuda/CUDAStream.h>

#define CUDA_DRIVER_CHECK(EXPR)                    
do {                                               
    CUresult code = EXPR;                          
    const char *msg;                               
    cuGetErrorString(code, &msg);                  
    if (code != CUDA_SUCCESS) {                    
        throw std::runtime_error(                  
            std::string(“CUDA driver error: “) +   
            std::string(msg));                     
    }                                              
} while (0);

namespace {

struct Grid {
    Grid(uint32_t x, uint32_t y, uint32_t z)
      : grid_x(x), grid_y(y), grid_z(z) {}
    uint32_t grid_x;
    uint32_t grid_y;
    uint32_t grid_z;

    bool is_non_zero() {
        return grid_x > 0 && grid_y > 0 && grid_z > 0;
    }
};

}  // anonymous namespace

static inline CUfunction loadKernel(
        std::string filePath,
        const std::string &funcName,
        uint32_t sharedMemBytes,
        const std::optional<std::string> &cubinDir = std::nullopt)
 
{
    if (cubinDir) {
        std::filesystem::path p1{*cubinDir};
        std::filesystem::path p2{filePath};
        filePath = (p1 / p2.filename()).string();
    }

    CUmodule mod;
    CUfunction func;
    CUDA_DRIVER_CHECK(cuModuleLoad(&mod, filePath.c_str()));
    CUDA_DRIVER_CHECK(cuModuleGetFunction(&func, mod, funcName.c_str()));
    if (sharedMemBytes > 0) {
        CUDA_DRIVER_CHECK(cuFuncSetAttribute(
            func,
            CU_FUNC_ATTRIBUTE_MAX_DYNAMIC_SHARED_SIZE_BYTES,
            sharedMemBytes
        ))
    }
    return func;
}

static inline void launchKernel(
        CUfunction func,
        uint32_t gridX,
        uint32_t gridY,
        uint32_t gridZ,
        uint32_t numWarps,
        uint32_t sharedMemBytes,
        void* args[],
        cudaStream_t stream)
 
{
    CUDA_DRIVER_CHECK(cuLaunchKernel(
        func, gridX, gridY, gridZ, 32*numWarps, 11, sharedMemBytes, stream, args, nullptr
    ));
}

static CUfunction triton_poi_fused_add_cos_sin_0 = nullptr;

std::vector<at::Tensor> inductor_entry_cpp(const std::vector<at::Tensor>& inputs) {

    py::gil_scoped_release release;
    auto arg0_1 = std::move(inputs[0]);
    auto arg1_1 = std::move(inputs[1]);

    at::cuda::CUDAGuard device_guard(0);
    auto buf0 = at::empty_strided({10L, }, {1L, }, at::TensorOptions(c10::Device(at::kCUDA, 0)).dtype(at::kFloat));
    // Source Nodes: [a, add, b], Original ATen: [aten.add, aten.cos, aten.sin]
    if (triton_poi_fused_add_cos_sin_0 == nullptr) {
        triton_poi_fused_add_cos_sin_0 = loadKernel(“/tmp/torchinductor_oldpan/rg/crgz7xmq52z337gwizafhl5xeujixy6bjenwk4nrtrulwqolpnzf.cubin”“triton__0d1d2d3”0);
    }
    CUdeviceptr var_0 = reinterpret_cast<CUdeviceptr>(arg0_1.data_ptr());
    CUdeviceptr var_1 = reinterpret_cast<CUdeviceptr>(arg1_1.data_ptr());
    CUdeviceptr var_2 = reinterpret_cast<CUdeviceptr>(buf0.data_ptr());
    auto var_3 = 10;
    void* kernel_args_var_0[] = {&var_0, &var_1, &var_2, &var_3};
    cudaStream_t stream0 = at::cuda::getCurrentCUDAStream(0);
    Grid triton_poi_fused_add_cos_sin_0_grid_0 = Grid(1L1L1L);
    launchKernel(triton_poi_fused_add_cos_sin_0, triton_poi_fused_add_cos_sin_0_grid_0.grid_x, triton_poi_fused_add_cos_sin_0_grid_0.grid_y, triton_poi_fused_add_cos_sin_0_grid_0.grid_z, 10, kernel_args_var_0, stream0);
    arg0_1.reset();
    arg1_1.reset();
    return {buf0};
}

其中调用的cubin就是上述生成triton代码编译出来的/tmp/torchinductor_oldpan/rg/xxx.cubin,这样的话就可以直接拿这个cpp代码去no-python环境跑起来了。

不过实际中我们更多用的是整个模型,例如resnet50,并且带有权重参数,当然这种也是支持的。torch官方也提供了aot工具可以导出整个模型为so:

import torch
from torch._export import aot_compile, dynamic_dim

torch.manual_seed(1337)

class Net(torch.nn.Module):
    def __init__(self):
        super().__init__()
        self.fc = torch.nn.Linear(6410)

    def forward(self, x, y):
        return self.fc(torch.sin(x) + torch.cos(y))

data = {}

for device in [“cpu”“cuda”]:
    model = Net().to(device=device)
    x = torch.randn((3264), device=device)
    y = torch.randn((3264), device=device)
    with torch.no_grad():
        ref_output = model(x, y)

    torch._dynamo.reset()
    with torch.no_grad():
        constraints = [
            dynamic_dim(x, 0) >= 1,
            dynamic_dim(x, 0) <= 1024,
            dynamic_dim(x, 0) == dynamic_dim(y, 0),
        ]
        model_so_path = aot_compile(model, (x, y), constraints=constraints)

    data.update({
        f”model_so_path_{device}: model_so_path,
        f”inputs_{device}: [x, y],
        f”outputs_{device}: [ref_output],
    })

# Use this to communicate tensors to the cpp code
class Serializer(torch.nn.Module):
    def __init__(self, data):
        super().__init__()
        for key in data:
            setattr(self, key, data[key])

torch.jit.script(Serializer(data)).save(“data.pt”)

通过这个aot_compile可以直接导出带有模型入口的so,因为是aot,需要提前指定输入的一些维度信息,对于支持dynamic来说是必要的。

导出的so可以通过以下C++方式读取:

void test_aoti(const std::string& device) {
  torch::NoGradGuard no_grad;

  std::string data_path =
      (std::filesystem::path(STRINGIZE(CMAKE_CURRENT_BINARY_DIR)) / “data.pt”)
           .string();
  torch::jit::script::Module data_loader = torch::jit::load(data_path);
  std::string path_attr = “model_so_path_” + device;
  std::string inputs_attr = “inputs_” + device;
  std::string outputs_attr = “outputs_” + device;
  const auto& model_so_path = data_loader.attr(path_attr.c_str()).toStringRef();
  const auto& input_tensors =
      data_loader.attr(inputs_attr.c_str()).toTensorList().vec();
  const auto& ref_output_tensors =
      data_loader.attr(outputs_attr.c_str()).toTensorList().vec();

  std::unique_ptr<torch::inductor::AOTIModelContainerRunner> runner;
  if (device == “cuda”) {
    runner = std::make_unique<torch::inductor::AOTIModelContainerRunnerCuda>(
        model_so_path.c_str());
  } else if (device == “cpu”) {
    runner = std::make_unique<torch::inductor::AOTIModelContainerRunnerCpu>(
        model_so_path.c_str());
  } else {
    std::cout << “unsupported device: “ << device << std::endl;
  }
  auto actual_output_tensors = runner->run(input_tensors);
  assert(actual_output_tensors.size() == ref_output_tensors.size());
}

核心就是AOTIModelContainerRunnerCpu

torch针对inductor设计了AOTIModelContainerRunnerCpu类去加载和运行生成的so,so中会包含一些计算图的执行步骤。

具体例子在pytorch/test/cpp/aot_inductor中,有两个例子,一个是torch::CustomClassHolder包一层AOTIModelContainerRunnerCuda跑,也就是和torchscript的结合,另一个是单独的AOTIModelContainerRunnerCuda去跑,搭配API直接C++调用。

对于一些常见的op,比如全连接self.fc = torch.nn.Linear(64, 10),可以直接调用外部算子不需要triton去codegen,上述例子中直接调用的是torch.ops.aten.addmm,更多细节可以看pytorch/torch/_inductor/select_algorithm.py

整体来说,这种导出方式也比较符合常识,常见的op可以直接调用已经高度优化的版本,未见过的一些算子可以使用triton去生成,fuse等图融合操作可以通过fx pass去做,导出c++也可以通过aot的方式导出,还有一些提升性能的runtime细节设计,整体潜力还是蛮大的。

还有很多细节没有来得及看,比如模型中的某些可以并行的op是如何多stream运行的,dynamic的情况是怎么处理的,中间变量如何存放的,显存是如何管理的,都需要花时间去看看。

如果有疑问欢迎交流。

参考

  • https://pytorch.org/tutorials/prototype/inductor_cpp_wrapper_tutorial.html
  • https://www.youtube.com/watch?v=eN5fqBNrjOo&list=PL_lsbAsL_o2BivkGLiDfHY9VqWlaNoZ2O&index=33
  • https://pytorch.org/blog/new-features-for-ai/
  • https://github.com/pytorch/pytorch/pull/111124
  • https://github.com/pytorch/pytorch/pull/88167
  • https://discuss.pytorch.org/t/pytorch-2-and-the-c-interface/168034/4
  • https://github.com/pytorch/TensorRT/discussions/1743
  • https://github.com/pytorch/TensorRT/discussions/1557
  • https://github.com/pytorch/TensorRT/issues/1404
  • https://github.com/pytorch/TensorRT/discussions/1372
  • https://discuss.pytorch.org/t/pytorch-2-and-the-c-interface/168034/2
  • https://discuss.pytorch.org/t/torch-compiles-deployment-story-to-non-python-host-processes/180943
  • https://dev-discuss.pytorch.org/t/using-nsight-systems-to-profile-gpu-workload/59/10

参考资料

[1]

dynamo相比torch.jit.trace和torch.jit.script,是一个功能更强大的trace工具: https://pytorch.org/tutorials/intermediate/torch_compile_tutorial.html?highlight=torch%20compile

[2]

torch.export: https://pytorch.org/docs/main/export.html

[3]

cpp wrapper: https://pytorch.org/tutorials/prototype/inductor_cpp_wrapper_tutorial.html

[4]

PyTorch 2.1 Contains New Performance Features for AI Developers: https://pytorch.org/blog/new-features-for-ai/

[5]

depyf: https://github.com/thuml/depyf

往期回顾


关注oldpan博客”,持续酝酿深度质量文我是老潘,我们下期见~
          打上星标✨不再错过老潘的及时推文     如果觉得有收获,来个点赞加好看

 

Read More 

正文完
可以使用微信扫码关注公众号(ID:xzluomor)
post-qrcode
 0
评论(没有评论)

文心AIGC

2023 年 12 月
 123
45678910
11121314151617
18192021222324
25262728293031
文心AIGC
文心AIGC
人工智能ChatGPT,AIGC指利用人工智能技术来生成内容,其中包括文字、语音、代码、图像、视频、机器人动作等等。被认为是继PGC、UGC之后的新型内容创作方式。AIGC作为元宇宙的新方向,近几年迭代速度呈现指数级爆发,谷歌、Meta、百度等平台型巨头持续布局
文章搜索
热门文章
潞晨尤洋:日常办公没必要上私有模型,这三类企业才需要 | MEET2026

潞晨尤洋:日常办公没必要上私有模型,这三类企业才需要 | MEET2026

潞晨尤洋:日常办公没必要上私有模型,这三类企业才需要 | MEET2026 Jay 2025-12-22 09...
面向「空天具身智能」,北航团队提出星座规划新基准丨NeurIPS’25

面向「空天具身智能」,北航团队提出星座规划新基准丨NeurIPS’25

面向「空天具身智能」,北航团队提出星座规划新基准丨NeurIPS’25 鹭羽 2025-12-13 22:37...
钉钉又发新版本!把 AI 搬进每一次对话和会议

钉钉又发新版本!把 AI 搬进每一次对话和会议

钉钉又发新版本!把 AI 搬进每一次对话和会议 梦晨 2025-12-11 15:33:51 来源:量子位 A...
商汤Seko2.0重磅发布,合作短剧登顶抖音AI短剧榜No.1

商汤Seko2.0重磅发布,合作短剧登顶抖音AI短剧榜No.1

商汤Seko2.0重磅发布,合作短剧登顶抖音AI短剧榜No.1 十三 2025-12-15 14:13:14 ...
跳过“逐字生成”!蚂蚁集团赵俊博:扩散模型让我们能直接修改Token | MEET2026

跳过“逐字生成”!蚂蚁集团赵俊博:扩散模型让我们能直接修改Token | MEET2026

跳过“逐字生成”!蚂蚁集团赵俊博:扩散模型让我们能直接修改Token | MEET2026 一水 2025-1...
最新评论
ufabet ufabet มีเกมให้เลือกเล่นมากมาย: เกมเดิมพันหลากหลาย ครบทุกค่ายดัง
tornado crypto mixer tornado crypto mixer Discover the power of privacy with TornadoCash! Learn how this decentralized mixer ensures your transactions remain confidential.
ดูบอลสด ดูบอลสด Very well presented. Every quote was awesome and thanks for sharing the content. Keep sharing and keep motivating others.
ดูบอลสด ดูบอลสด Pretty! This has been a really wonderful post. Many thanks for providing these details.
ดูบอลสด ดูบอลสด Pretty! This has been a really wonderful post. Many thanks for providing these details.
ดูบอลสด ดูบอลสด Hi there to all, for the reason that I am genuinely keen of reading this website’s post to be updated on a regular basis. It carries pleasant stuff.
Obrazy Sztuka Nowoczesna Obrazy Sztuka Nowoczesna Thank you for this wonderful contribution to the topic. Your ability to explain complex ideas simply is admirable.
ufabet ufabet Hi there to all, for the reason that I am genuinely keen of reading this website’s post to be updated on a regular basis. It carries pleasant stuff.
ufabet ufabet You’re so awesome! I don’t believe I have read a single thing like that before. So great to find someone with some original thoughts on this topic. Really.. thank you for starting this up. This website is something that is needed on the internet, someone with a little originality!
ufabet ufabet Very well presented. Every quote was awesome and thanks for sharing the content. Keep sharing and keep motivating others.
热评文章
预见未来:96位前沿先锋超万字核心观点总结,抢抓未来产业新高地

预见未来:96位前沿先锋超万字核心观点总结,抢抓未来产业新高地

预见未来:96位前沿先锋超万字核心观点总结,抢抓未来产业新高地 henry 2025-12-11 10:27:...
Meta公开抄阿里Qwen作业,还闭源了…

Meta公开抄阿里Qwen作业,还闭源了…

Meta公开抄阿里Qwen作业,还闭源了… Jay 2025-12-11 11:48:25 来源:量子位 Ja...
MEET2026挤爆了,AI圈今年最该听的20+场演讲&对谈都在这

MEET2026挤爆了,AI圈今年最该听的20+场演讲&对谈都在这

MEET2026挤爆了,AI圈今年最该听的20+场演讲&对谈都在这 西风 2025-12-11 15:...
钉钉又发新版本!把 AI 搬进每一次对话和会议

钉钉又发新版本!把 AI 搬进每一次对话和会议

钉钉又发新版本!把 AI 搬进每一次对话和会议 梦晨 2025-12-11 15:33:51 来源:量子位 A...