sparse.conv export onnx(讲解) - centerpoint

1 @函数装饰器

装饰器允许你在不修改原始代码的情况下,给函数增加额外的功能。装饰器本质上是一个函数,它接受一个函数作为参数并返回一个新的函数。    

demo    



# @装饰器,python中顾名思义。和装饰器设计概念一样,这里使用@其实是语法糖
def mydecorator(f):  # 装饰器函数,接受一个函数作为参数, 即被装饰的函数会被作为参数传递给装饰器函数
    def wrapper(*args,**kwargs):  # 接受任意数量和类型的参数,执行被装饰的函数,返回了被装饰的函数,再把参数传递给被装饰的函数,最终执行
        print("装饰器开始")
        f(*args,**kwargs)   # 最终执行被装饰的函数
        print("装饰器结束")
    return wrapper
 
@mydecorator
def hello(name):
    print("hello",name)
 
hello("world")  
# 执行情况: 
# 贴装饰器后,执行hello(“world”)时,hello(name)作为参数传入装饰器函数,装饰器函数执行,返回装饰器实际增强函数,
# 然后传入参数 ‘world’ 即wrapper("world"),wrapper函数执行,执行wrapper函数体内的内容,执行了被装饰的函数
 
# # 装饰器可以叠加使用
# @mydecorator
# @mydecorator   # 从上到下依次装饰,然后从内到外依次执行
# def hello(name):
#     print("hello",name)
 

print('----------------------------------------------------')

# 装饰器可以带参数
def mydecorator2(arg):  # arg-装饰器参数 "hello2"
    def decorator(f):  # 接收要被装饰函数的函数
        def wrapper(*args,**kwargs):  # 如果被装饰的函数带参数则需要这样写,接收被装饰函数的参数
            print("装饰器开始2 arg ",arg) 
            f(*args,**kwargs)   # 执行被装饰的函数
            print("装饰器结束2")  
        return wrapper
    return decorator
 
@mydecorator2("hello2")
def hello2(name):
    print("hello2",name)
 
hello2("world2")

 

装饰器开始
hello world
装饰器结束
----------------------------------------------------
装饰器开始2 arg  hello2
hello2 world2
装饰器结束2

demo2 带参数的原函数

如果原函数是带参数的话,那么装饰器需要怎么写呢? 我们需要把参数写到log_decorator里面的那个函数实际上wrapper,下面我们可以通过使用*args**kwargs来接收和传递参数下面是一个示例,演示如何创建一个适用于带参数的函数的装饰器:

import time

def log_decorator(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        func(*args, **kwargs)
        end_time = time.time()
        execution_time = end_time - start_time
        print(f"Function {func.__name__} executed in {execution_time} seconds")
    return wrapper

在上述示例中,log_decorator是一个装饰器函数,它接收任意类型和数量的参数,并使用*args**kwargs来接收和传递参数。在wrapper函数内部,使用func(*args, **kwargs)来调用原始函数,并将参数传递给它。 现在,我们可以使用@语法来应用这个装饰器,无论带有参数还是不带参数的函数都可以。

@log_decorator
def say_hello():
    print("Hello, world!")

@log_decorator
def greet(name):
    print(f"Hello, {name}!")

say_hello()  # 执行带装饰器的say_hello函数
greet("Alittle")  # 执行带装饰器的greet函数

@log_decorator分别为say_hellogreet函数应用了装饰器。无论是不带参数的say_hello函数还是带参数的greet函数,装饰器都能正常工作。

demo3  带参数的装饰器

 

当需要给装饰器传递参数时,可以使用装饰器工厂函数来创建带参数的装饰器。装饰器工厂函数实际上是一个闭包函数,它接收参数并返回一个真正的装饰器函数。比如我们需要为不同的业务逻辑添加不同的日志等级,就需要在装饰器中添加参数了。

import time

def log_decorator_with_params(log_level):
    def log_decorator(func):
        def wrapper(*args, **kwargs):
            start_time = time.time()
            result = func(*args, **kwargs)
            end_time = time.time()
            execution_time = end_time - start_time
            print(f"{log_level}: Function {func.__name__} executed in {execution_time} seconds")
            return result
        return wrapper
    return log_decorator

如上所示,这种写法可以称为闭包的闭包,log_decorator_with_params是一个装饰器工厂函数,它接收一个参数log_level,用于指定日志的前缀。它返回一个真正的装饰器函数log_decorator,该装饰器函数在函数执行前后打印带有指定前缀的日志。

现在,我们可以使用@语法来应用带参数的装饰器。

@log_decorator_with_params(log_level="INFO")
def say_hello():
    print("Hello, world!")

@log_decorator_with_params(log_level="DEBUG")
def greet(name):
    print(f"Hello, {name}!")

say_hello()  # 执行带装饰器的say_hello函数
greet("Alice")  # 执行带装饰器的greet函数

上述示例中,@log_decorator_with_params("INFO")@log_decorator_with_params("DEBUG")分别为say_hellogreet函数应用了带参数的装饰器。运行代码时,会分别打印日志等级。 在定义装饰器函数wrapper时,使用了*args**kwargs作为参数,这样能够适配任意类型和数量的参数,并将其传递给原始函数。这样可以确保带参数的函数装饰器适用于不同的函数签名。通过使用装饰器工厂函数,我们可以轻松创建带参数的装饰器,提供更大的灵活性,让装饰器可以根据不同的场景和需求来定制其行为。

demo4

def register_node(fn):

    fnnames   = fn.split(".")
    fn_module = eval(".".join(fnnames[:-1]))
    fn_name   = fnnames[-1]
    oldfn = getattr(fn_module, fn_name)
    
    def make_hook(bind_fn):

        ilayer = 0
        def internal_forward(self, *args):
            global enable_trace

            if not enable_trace:
                return oldfn(self, *args)

            global avoid_reuse_container
            nonlocal ilayer

            # Use the enable_trace flag to avoid internal trace calls
            enable_trace = False
            y = oldfn(self, *args)
            bind_fn(self, ilayer, y, *args) 
            enable_trace = True

            avoid_reuse_container.extend(list(args) + [y]) 
            ilayer += 1
            return y

        setattr(fn_module, fn_name, internal_forward)
    return make_hook

@register_node("spconv.conv.SparseConvolution.forward")
def symbolic_sparse_convolution(self, ilayer, y, x):
    register_tensor(y)
    print(f"   --> SparseConvolution{ilayer}[{'subm' if self.subm else 'conv'}] -> Input {get_tensor_id(x)}, Output {get_tensor_id(y)}")

    if self.transposed:
        output_size = spconv.ops.get_deconv_output_size(
            x.features.size(), self.kernel_size, self.stride, self.padding, self.dilation, self.output_padding
        )
    else:
        output_size = spconv.ops.get_conv_output_size(
            x.features.size(), self.kernel_size, self.stride, self.padding, self.dilation
        )
    
    if self.subm:
        output_size[0] = x.features.size(0)
    
    output_size[1] = self.out_channels
    inputs = [
        get_tensor_id(x), 
        append_initializer(self.weight.data.permute(4, 0, 1, 2, 3), f"spconv{ilayer}.weight"),
    ]

    if self.bias is not None:
        inputs.append(append_initializer(self.bias.data, f"spconv{ilayer}.bias"))

    output_bound = 200000
    if hasattr(self, "output_bound"):
        output_bound = self.output_bound

    nodes.append(
        helper.make_node(
            "SparseConvolution", inputs, [get_tensor_id(y)], f"conv{ilayer}", 
            ndim = self.ndim,
            input_spatial_shape = x.spatial_shape,
            output_spatial_shape = y.spatial_shape,
            in_channels = self.in_channels,
            out_channels = self.out_channels,
            kernel_size = self.kernel_size,
            output_bound = output_bound,
            stride = self.stride,
            dilation = self.dilation,
            padding = self.padding,
            transposed = self.transposed,
            inverse = self.inverse,
            output_padding = self.output_padding,
            groups = self.groups,
            subm = self.subm,
            rulebook = self.indice_key,
            activation = getattr(self, "act_type", "None"),
            input_shape  = x.features.shape,
            output_shape = y.features.shape
        )
    )

下面我们详细分析一下在执行 spconv.conv.SparseConvolution.forward 方法时,代码是如何工作的,主要是涉及到装饰器 register_node 和函数 symbolic_sparse_convolution 的使用

我们先来过一下装饰器 register_node 的工作原理

  • 1. 初始化阶段
    • 当 export_tool.py 脚本运行时,首先会去执行 register_node(“spconv.conv.SparseConvolution.forward”) 装饰器
    • 这个参数接收一个字符串参数,代表要修改的函数的名称
  • 2. register_node 功能
    • 在 register_node 内部,它首先会根据传入的字符串找到相应的函数对象
    • 然后,它会返回一个名为 make_hook 的函数
  • 3. 创建 mask_hook
    • make_hook 是一个内部函数,它的目的是接收一个函数(这里是 symbolic_sparse_convolution)并“钩住”(hook)原始的 SparseConvolution.forward 方法

接着我们再过一下整个执行过程

  • 1. 应用装饰器
    • 当 @register_node(“spconv.conv.SparseConvolution.forward”) 应用于 symbolic_sparse_convolution 函数时,实际上是一个语法糖,其作用等同于 symbolic_sparse_convolution = register_node(“spconv.conv.SparseConvolution.forward”)(symbolic_sparse_convolution)
    • make_hook 接收 symbolic_sparse_convolution 作为参数,然后在 SparseConvolution.forward 上调用 internal_forward 函数
  • 2. 修改 SparseConvolution.forward
    • internal_forward 函数替换了原始的 SparseConvolution.forward 方法,这意味着每次调用 SparseConvolution.forward 时,实际上是调用 internal_forward
  • 3. 执行 internal forward
    • 当 SparseConvolution.forward 被调用时,internal_forward 被执行
    • 在 inernal_forward 内部,首先会调用原始的 SparseConvolution.forward 方法(通过 oldfn(self *args)),然后会调用 symbolic_sparse_convolution 函数
  • 4. 执行 symbolic_sparse_convolution
    • symbolic_sparse_convolution 函数在每次 SparseConvolution.forward 被调用后执行,用于执行追踪逻辑,如创建 onnx 图中的节点

总的来说,当 spconv.conv.SparseConvolution.forward 方法被调用时,由于装饰器 register_node 的作用,实际上是先执行了 internal_forward 函数。这个函数首先执行原始的 SparseConvolution.forward 方法,然后执行 symbolic_sparse_convolution 函数来进行追踪和收集信息。这种机制允许在不更改原始函数代码的前提下,增加额外的功能,这在许多情况下非常有用,特别是在需要追踪或记录函数行为的场景中。

2. 稀疏卷积概述

 spconv 的原理,它通过 hash table 将输入输出中不为 0 的点的位置坐标保存下来,并通过 rulebook 记录输入输出和 filter 之间的对应关系,把没有意义的点全部 skip 掉,只保留真正想做计算的点,从而将一个稀疏的卷积计算变成密集的矩阵乘法计算,实现加速

构建 hash table

第一步是来构建 hash tables

构建 Rulebook


第二步是建立 Rulebook,这是稀疏卷积的关键部分。Rulebook 的目的类似于 im2col,它将卷积从数学形式转换为有效的可编程形式。但与 im2col 不同的是,Rulebook 收集了卷积中所有涉及的原子操作,然后将它们关联到相应的 kernel 元素上。

3. SCN导出


关于稀疏卷积的导出有以下几点说明:

在实现上,traveller59 的 SparseConv 的实现比较完善

https://github.com/traveller59/spconv

我们通常以 CenterPoint 的 SCN 导出为案例来讲解 spconv

https://github.com/tianweiy/CenterPoint/blob/master/det3d/models/backbones/scn.py

SPConv问题的解决方案思维导图如下所示:

图3-1 SPConv问题的解决方案


3.1 实现trace


Trace0

由于 SparseConv 的特殊性,输入输出采用特殊的 tensor 表示。因此标准的 onnx 导出已然无法处理这种复杂的情况

此时可以利用 python 最核心的特性,直接替换特定函数的实现,以实现挂钩到自己函数中

这种做法没有局限性,比 register_forward_hook 要求更低,它可以替换任意函数,而 register_forward_hook 不行

示例代码如下:

import torch
import torch.nn as nn

class Model(nn.Module):
    def __init__(self):
        super().__init__()

        self.conv1 = nn.Conv2d(3, 3, 1, 1)
        self.relu1 = nn.ReLU()
        self.conv2 = nn.Conv2d(3, 1, 1, 1)
    
    def forward(self, x):
        x = self.conv1(x)
        x = self.relu1(x)
        x = self.conv2(x)
        return x
    
def hook_forward(oldfn):
    def myforward(self, x):
        y = oldfn(self, x)
        print(f"{type(self)} -> Input {id(x)}, Output {id(y)}")
        return y
    return myforward

nn.Conv2d.forward = hook_forward(nn.Conv2d.forward)
nn.ReLU.forward   = hook_forward(nn.ReLU.forward)

model = Model()
x = torch.zeros(1, 3, 3, 3)
y = model(x)

运行效果输入如下:

图3-2 export0输出
在上面的示例代码中,我们通过替换特定函数的实现,实现了钩子函数来挂钩到自定义函数中。具体来说,我们定义了一个名为 hook_forward 的函数,用于替换原有的前向传播函数实现。该函数接受一个旧的前向传播函数作为输入,并返回一个新的前向传播函数。

通过这种方式,我们可以在模型的前向传播过程中插入自定义的操作,例如打印张量的地址、收集统计信息等。它能够保持原有的特性,并嫁接到forward中,实现自定义行为

Trace1

前面我们为了储存旧的 function,采用了闭包特性。这里我们进一步简化 hook 过程,采用字符串解析,加上装饰器。

为了避免 pytorch 对 tensor 进行复用,导致存在 id 相同的 tensor。我们使用了 clone,但是 clone 并不能总是保证唯一,所有这里其实留了一个问题!

示例代码如下:
 

import torch
import torch.nn as nn

class Model(nn.Module):
    def __init__(self):
        super().__init__()

        self.conv1 = nn.Conv2d(3, 3, 1, 1)
        self.relu1 = nn.ReLU()
        self.conv2 = nn.Conv2d(3, 1, 1, 1)
    
    def forward(self, x):
        x = self.conv1(x)
        x = self.relu1(x)
        x = self.conv2(x)
        return x
    
def hook_forward(fn):
    
    fnnames   = fn.split(".")
    fn_module = eval(".".join(fnnames[:-1]))
    fn_name   = fnnames[-1]
    oldfn     = getattr(fn_module, fn_name)

    def make_hook(bind_fn):
        def myforward(self, x):
            y = oldfn(self, x).clone()
            bind_fn(self, x, y)
            return y

        setattr(fn_module, fn_name, myforward)
    return make_hook

@hook_forward("torch.nn.Conv2d.forward")
def symbolic_conv2d(self, x, y):
    print(f"{type(self)} -> Input {id(x)}, Output {id(y)}")

@hook_forward("torch.nn.ReLU.forward")
def symbolic_relu(self, x, y):
    print(f"{type(self)} -> Input {id(x)}, Output {id(y)}")
    
model = Model()
x = torch.zeros(1, 3, 3, 3)
y = model(x)

图3-3 export1输出
在上述示例代码中,我们定义了一个 hook_forward 的装饰器函数,用于替换指定函数的前向传播实现。hook_forward 函数接受一个字符串参数 fn,该字符串表示需要替换的函数的路径。通过字符串解析和 eval 函数,获取到需要替换的旧函数对象 oldfn

hook_forward 函数内部定义了一个嵌套函数 make_hook,用于创建实际的钩子函数。make_hook 函数接受一个绑定函数 bind_fn 作为参数,用于在钩子函数中执行额外的操作。其中,bind_fn 函数的形式为 bind_fn(self, x, y),用于处理输入和输出张量。使用 setattr 函数将新的前向传播函数 myforward 替换旧函数 oldfn,以实现钩子函数的绑定。

我们再来回顾下之前 AutoCV 课程中讲到的关于装饰器的相关知识

Python 装饰器本质上是一个函数,它接受一个函数对应作为参数,并返回一个修改后的函数对象。装饰器通常使用 @decorator 的语法糖来使用,它可以将装饰器应用于函数或类的定义之前,从而实现对其功能的增强或修改。

装饰器的语法结构如下:

@表达式
def 被修饰的函数
 


其中,表达式需要返回一个函数对象,这个函数对象就是用来修饰函数的。

@hook_forward("torch.nn.Conv2d.forward") 等价于如下代码:

hook_forward("torch.nn.Conv2d.forward")(symbolic_conv2d)

hook_forward("torch.nn.Conv2d.forward") 返回了一个内部函数 make_hook,然后将 symbolic_conv2d 作为参数传递给 make_hook 函数。

由于装饰器本质上是一个函数,它在定义被修饰函数之后立即执行。因此,当定义装饰器 @hook_forward("torch.nn.Conv2d.forward") 时,装饰器函数 hook_forward 会被调用,并且 setattr 语句会在函数内部执行。此时,make_hook(symbolic_conv2d) 将创建一个新的前向传播函数,并将其替换为 nn.Conv2d 的 forward 函数,从而实现了将钩子函数绑定到前向传播函数上的目的。

Trace2

在 Trace1 中我们提到需要避免 pytorch 对 tensor 进行复用,下面我们就来解决它

首先思考下 pytorch 在何时会复用 tensor?

答案是在没有任何引用的 tensor 会被回收并复用

那么解决方案就是引用它,不释放,然后把 id 重新编号为 tensor 数

示例代码如下:

import torch
import torch.nn as nn

class Model(nn.Module):
    def __init__(self):
        super().__init__()

        self.conv1 = nn.Conv2d(3, 3, 1, 1)
        self.relu1 = nn.ReLU()
        self.conv2 = nn.Conv2d(3, 1, 1, 1)
    
    def forward(self, x):
        x = self.conv1(x)
        x = self.relu1(x)
        x = self.conv2(x)
        return x
    
def hook_forward(fn):

    fnnames   = fn.split(".") 
    fn_module = eval(".".join(fnnames[:-1]))
    fn_name   = fnnames[-1]
    oldfn = getattr(fn_module, fn_name)

    def make_hook(bind_fn):
        def myforward(self, x):
            global all_tensors
            y = oldfn(self, x)
            bind_fn(self, x, y)
            all_tensors.extend([x, y])  # 避免torch对tensor进行复用
            return y
        
        setattr(fn_module, fn_name, myforward)
    return make_hook

@hook_forward("torch.nn.Conv2d.forward")
def symbolic_conv2d(self, x, y):
    print(f"{type(self)} -> Input {get_obj_idd(x)}, Output {get_obj_idd(y)}")

@hook_forward("torch.nn.ReLU.forward")
def symbolic_relu(self, x, y):
    print(f"{type(self)} -> Input {get_obj_idd(x)}, Output {get_obj_idd(y)}")

def get_obj_idd(obj):
    global objmap

    idd = id(obj)
    if idd not in objmap:
        objmap[idd] = len(objmap)
    return objmap[idd]

# 避免torch对内存复用导致id相同
all_tensors = []

# 为每个新的tensor编号
objmap = {}

model = Model()
x = torch.zeros(1, 3, 3, 3)
y = model(x)

图3-4 export2输出
上述示例代码通过引用 Tensor 并重新编号其内存地址的方式,避免了 PyTorch 对 Tensor 进行复用。我们定义了一个 get_obj_idd 函数,用于为每个 Tensor 对象分配一个唯一的编号,避免 PyTorch 对内存的复用导致 id 相同。其中空列表 all_tensors 用于存储所有的 Tensor 对象,空字典 objmap 用于映射 Tensor 对象的内存地址到其编号。

到此为止 Trace 的核心功能得到实现,剩下的是把 trace 的 graph 交给 onnx

3.2 导出onnx


由于 trace 是我们自己实现的,因此 onnx 的创建工作也需要我们自己来动手

关于 onnx 的操作可参考 https://shouxieai.com/solution/trt/basic-1.4-onnx-editor

在开始之前我们还是需要了解下 onnx 文件的组成,方便后续操作,onnx 文件组成如下图所示:

图3-5 onnx文件组成
model:表示整个 onnx 模型,包括图结构和解析器版本、opset 版本、导出程序类型
opset 版本即 operator 版本号即 pytorch 的 op(操作算子) 版本
model.graph:表示图结构,通常是 Netron 中看到的结构
model.graph.node:表示图结构中所有节点如 conv、bn、relu 等
model.graph.initializer:权重数据大都存储在这里
model.graph.input:模型的输入,它指定了输入的名称、数据类型和形状
model.graph.output:模型的输出,它指定了输出的名称、数据类型和形状
因此我们要创建一个 onnx 需要定义模型的输入和输出即 model.graph.input 和 model.graph.output,其余的就是一些节点的定义,对应的权重使用 model.graph.initializer 进行初始化即可

下面是利用 onnx.helper 创建一个节点的示例:

helper.make_node(
    name="Conv_0",  # 节点名字,注意与op_type的区分
    op_type="Conv", # 节点的算子类型,比如'Conv'、'Relu'、'Add'
    inputs=["image", "conv.weight", "conv.bias"],   # 各个输入的名字,节点的输入包含:输入和算子的权重
    outputs=["3"],  # 输出的个数
    pads=[1, 1, 1, 1],  # 其他字符串为该节点的属性,比如'Conv'的stride、pad等
    group=1,
    dilations=[1,1],
    kernel_shape=[3,3],
    strides=[1,1]
)

自己实现的 trace 的 graph 导出为 onnx 的示例代码如下:

import torch
import torch.nn as nn
import onnx
import onnx.helper as helper
import numpy as np

# reference
# https://shouxieai.com/solution/trt/basic-1.4-onnx-editor

class Model(nn.Module):
    def __init__(self):
        super().__init__()

        self.conv1 = nn.Conv2d(3, 3, 1, 1)
        self.relu1 = nn.ReLU()
        self.conv2 = nn.Conv2d(3, 1, 1, 1)
        self.conv_right = nn.Conv2d(3, 3, 1, 1)
    
    def forward(self, x):
        r = self.conv_right(x)
        x = self.conv1(x)
        x = self.relu1(x)
        x = self.conv2(x + r)
        return x

def hook_forward(fn):
    
    fnnames   = fn.split(".")
    fn_module = eval(".".join(fnnames[:-1]))
    fn_name   = fnnames[-1]
    oldfn = getattr(fn_module, fn_name)

    def make_hook(bind_fn):
        
        ilayer = 0
        def myforward(self, x):
            global all_tensors
            nonlocal ilayer
            y = oldfn(self, x)

            bind_fn(self, ilayer, x, y)
            all_tensors.extend([x, y])
            ilayer += 1
            return y
    
        setattr(fn_module, fn_name, myforward)
    return make_hook

@hook_forward("torch.nn.Conv2d.forward")
def symbolic_conv2d(self, ilayer, x, y):
    print(f"{type(self)} -> Input {get_obj_idd(x)}, Output {get_obj_idd(y)}")

    inputs = [
        get_obj_idd(x),
        append_initializer(self.weight.data, f"conv{ilayer}.weight"),
        append_initializer(self.bias.data, f"conv{ilayer}.bias")
    ]

    nodes.append(
        helper.make_node(
            "Conv", inputs, [get_obj_idd(y)], f"conv{ilayer}",
            kernel_shape=self.kernel_size, group=self.groups, 
            pads=[0, 0] + list(self.padding), dilations=self.dilation, strides=self.stride
        )
    )

@hook_forward("torch.nn.ReLU.forward")
def symbolic_relu(self, ilayer, x, y):
    print(f"{type(self)} -> Input {get_obj_idd(x)}, Output {get_obj_idd(y)}")

    nodes.append(
        helper.make_node(
            "Relu", [get_obj_idd(x)], [get_obj_idd(y)], f"relu{ilayer}"
        )
    )

@hook_forward("torch.Tensor.__add__")
def symbolic_add(a, ilayer, b, y):
    print(f"Add -> Input {get_obj_idd(a)} + {get_obj_idd(b)}, Output {get_obj_idd(y)}")

    nodes.append(
        helper.make_node(
            "Add", [get_obj_idd(a), get_obj_idd(b)], [get_obj_idd(y)], f"add{ilayer}"
        )
    )

def append_initializer(value, name):
    initializers.append(
        helper.make_tensor(
            name=name,
            data_type=helper.TensorProto.DataType.FLOAT,
            dims=list(value.shape),
            vals=value.data.numpy().astype(np.float32).tobytes(),
            raw=True
        )
    )
    return name

def get_obj_idd(obj):
    global objmap
    
    idd = id(obj)
    if idd not in objmap:
        objmap[idd] = str(len(objmap))
    return objmap[idd]

all_tensors = []
objmap = {}
nodes = []
initializers = []

torch.manual_seed(31)
x = torch.full((1, 3, 3, 3), 0.55)
model = Model().eval()
y = model(x)

inputs = [
    helper.make_value_info(
        name="0",
        type_proto=helper.make_tensor_type_proto(
            elem_type=helper.TensorProto.DataType.FLOAT,
            shape=["batch", x.size(1), x.size(2), x.size(3)]
        )
    )
]

outputs = [
    helper.make_value_info(
        name="5",
        type_proto=helper.make_tensor_type_proto(
            elem_type=helper.TensorProto.DataType.FLOAT,
            shape=["batch", y.size(1), y.size(2), y.size(3)]
        )
    )
]

graph = helper.make_graph(
    name="mymodel",
    inputs=inputs,
    outputs=outputs,
    nodes=nodes,
    initializer=initializers
)

# 如果名字不是ai.onnx,netron解析就不是太一样了
opset = [
    helper.make_operatorsetid("ai.onnx", 11)
]

# producer主要是保持与pytorch一致
model = helper.make_model(graph, opset_imports=opset, producer_name="pytorch", producer_version="1.12")
onnx.save_model(model, "./custom.onnx")

print(y)

在上述示例代码中,通过 helper.make_node 函数创建节点,并将其添加到 nodes 列表中。append_initializer 函数用于将权重参数添加到 initializers 列表中,以便导出到 onnx 模型中。然后使用 helper.make_graph 创建计算图,传入输入、输出、节点和初始化器信息。接下来利用 helper.make_model 函数创建模型,最后使用 onnx.save_model 将模型保存为 onnx 格式的文件。

运行效果如下:

图3-6 export3输出

导出的 onnx 模型如下:

 

图3-7 导出的onnx

可以看到导出的 onnx 模型符合我们的预期,由于是导出的 onnx,因此我们可以利用 onnxruntime 对模型进行推理验证,验证的示例代码如下:

import onnxruntime
import numpy as np

session = onnxruntime.InferenceSession("custom.onnx", providers=["CPUExecutionProvider"])

x = np.full((1, 3, 3, 3), 0.55, dtype=np.float32)
y = session.run(["5"], {"0": x})[0]

print(y)

图3-8 onnx模型输出
可以看到 onnxruntime 推理验证的结果与之前的一样,可知整个过程应该没什么问题

3.3 CenterPoint SCN导出


主要代码在 https://github.com/tianweiy/CenterPoint/blob/master/det3d/models/backbones/scn.py 由于他的 forward 函数不规范,我们需要改造他,使得输入输出都是单纯的 sparseConvTensor

原始的 forward 函数如下:

def forward(self, voxel_features, coors, batch_size, input_shape):
    
    # input: # [41, 1600, 1408]
    sparse_shape = np.array(input_shape[::-1]) + [1, 0, 0]
    
    coors = coors.int()
    ret = spconv.SparseConvTensor(voxel_features, coors, sparse_shape, batch_size)
    
    x = self.conv_input(ret)
    
    x_conv1 = self.conv1(x)
    x_conv2 = self.conv2(x_conv1)
    x_conv3 = self.conv3(x_conv2)
    x_conv4 = self.conv4(x_conv3)
    
    ret = self.extra_conv(x_conv4)
    
    ret = ret.dense()
    
    N, C, D, H, W = ret.shape
    ret = ret.view(N, C * D, H, W)
    
    multi_scale_voxel_features = {
        'conv1': x_conv1,
        'conv2': x_conv2,
        'conv3': x_conv3,
        'conv4': x_conv4
    }
    
    return ret, multi_scale_voxel_features

修改后的 forward 函数如下:

def forward(self, x : spconv.SparseConvTensor):
    
    # input: # [41, 1600, 1408]
    # sparse_shape = np.array(input_shape[::-1]) + [1, 0, 0]
    
    # coors = coors.int()
    # ret = spconv.SparseConvTensor(voxel_features, coors, sparse_shape, batch_size)
    
    x = self.conv_input(x)
    
    x_conv1 = self.conv1(x)
    x_conv2 = self.conv2(x_conv1)
    x_conv3 = self.conv3(x_conv2)
    x_conv4 = self.conv4(x_conv3)
    
    ret = self.extra_conv(x_conv4)
    
    # 后续的dense只是一个scatter操作,比较简单。目前先分离他们
    
    return ret

通过加载点云特征和坐标数据,可以写一个 SCN 的简单推理案例。好进一步展开,示例代码如下:

from det3d.models.backbones.scn import SpMiddleResNetFHD
from spconv.pytorch import SparseConvTensor
import torch
import pickle

with open("test_spconv.pkl", "rb") as f:
    (voxels, coors, spatial_shape) = pickle.load(f)

print(voxels.shape, voxels.dtype, coors.shape, coors.dtype, spatial_shape)
model = SpMiddleResNetFHD(voxels.shape[1]).cuda().eval().half()

voxels = torch.from_numpy(voxels).cuda().half()
coors  = torch.from_numpy(coors).cuda()

x = SparseConvTensor(voxels, coors, spatial_shape, 1)

with torch.no_grad():
    y = model(x)

    print(y.features.shape, y.indices.shape, y.spatial_shape)

注意这里用了 half,是因为 spconv 支持 fp16 推理,性能比较好。coors 就是 indices,包括 [batch,x,y,z] 四个维度,它是 int 类型的

我们现在可以将之前的 trace 对接到 SCN 上了,有以下几点值得我们注意:

在 myforward 中我们增加了 enable_trace 标记保护,避免在调用 oldfn 时,里面再次触发 hook

def hook_forward(fn):
    ...
    
    def make_hook(bind_fn):
        
        ilayer = 0
        def myforward(self, x):
            global all_tensors, enable_trace
            nonlocal ilayer
            
            # 标记保护
            if not enable_trace: return
            
            enbale_trace = Face
            y = oldfn(self, x)
            
            bind_fn(self, ilayer, x, y)
            enable_trace = True
            all_tensors.extend([x, y])
            ilayer += 1
            return y
        
        setattr(fn_module, fn_name, myforward)
    return make_hook

hook 住 sparseConvolution 的 forward 函数,并添加对应的 node,和之前的 Conv、ReLU 一样。我们这里把 SparseConvTensor 作为一个输入值即可,不用区分 features 和 indices,因为不用走 tensorRT

@hook_forward("spconv.pytorch.conv.SparseConvolution.forward")
def symbolic_conv2d(self, ilayer, x, y):
    print(f"{type(self)} -> Input {get_obj_idd(x)}, Output {get_obj_idd(y)}")
    
    inputs = [
        get_obj_idd(x),
        append_initializer(self.weight.data, f"conv{ilayer}.weight")
    ]
    
    if self.bias is not None:
        inputs.append(append_initializer(self.bias.data, f"conv{ilayer}.bias"))
        
    nodes.append(
    	helper.make_node(
        	"SparseConvolution", inputs, [get_obj_idd(y)], f"spconv{ilayer}",
            kernel_shape=self.kernel_size, group=self.groups, pads=[0, 0] + list(self.padding),
            dilations=self.dilation, strides=self.stride
        )
    )

在 ReLU 节点中需要避免 inplace 操作,因为 inplace 会使得 input、output 的 id 一样

for name, m in model.named_modules():
    if isinstance(m, nn.ReLU):
        m.inplace = False

这里面存在很多的 bn、relu 可以进行融合,这是 spconv 提供的一些支持,其中 bn 可以与 spconv 的 weight、bias 进行 fusion,activation(relu) 则可以与 spconv 的实现进行融合,spconv 可以通过增加 act_type 标记告诉它 activation 是什么。融合后没有 bn 和 relu,更加简单

def fuse_conv_bn(self, conv_out, conv, bn):
    
    # conv ->
    # y = x * w + b

    # bn
    # t = (x - mean) / std
    # t = x * 1/var + (-mean / var)
    # y = t * gamma + beta
    # y = (x * w + b) * 1/var * gamma + (-mean/var) * gamma + beta
    # y = x * w * 1/var * gamma + (-mean/var) * gamma + beta + conv.b * 1/var * gamma
    
    # conv -> bn
    # y1 = x * conv.w + conv.b
    # y2 = (y1 - mean) / var
    # y3 = y2 * gamma + beta
    # output = ((x * conv.w + conv.b) - mean) / var * gamma + beta
    # output = (x * conv.w + conv.b - mean) / var * gamma + beta
    #        = x * conv.w / var * gamma + conv.b / var * gamma - mean / var * gamma + beta
    # weight = x * conv.w / var * gamma
    # bias   = conv.b / var * gamma - mean / var * gamma + beta

    std = torch.sqrt(bn.running_var.data) + bn.eps
    conv_out.weight.data[:] = conv.weight.data / std.view(1, -1, 1, 1) * bn.weight.data.view(1, -1, 1, 1)
    conv_out.bias.data[:]   = conv.bias.data / std * bn.weight.data + bn.bias.data + (-bn.running_mean.data / std) * bn.weight.data

SCN 导出 onnx 的示例代码如下:

from det3d.models.backbones.scn import SpMiddleResNetFHD
from spconv.pytorch import SparseConvTensor
import sponnx
import torch
import pickle
import torch.nn as nn
import onnx
import onnx.helper as helper
import numpy as np
import spconv

torch.manual_seed(31)
with open("test_spconv.pkl", "rb") as f:
    (voxels, coors, spatial_shape) = pickle.load(f)

print(spatial_shape)
model = SpMiddleResNetFHD(voxels.shape[1]).cuda().eval().half()


def hook_forward(fn):

    fnnames   = fn.split(".")
    fn_module = eval(".".join(fnnames[:-1]))
    fn_name   = fnnames[-1]
    oldfn = getattr(fn_module, fn_name)
    
    def make_hook(bind_fn):

        ilayer = 0
        def myforward(self, x):
            global all_tensors, enable_trace
            nonlocal ilayer

            if not enable_trace: return

            enable_trace = False
            y = oldfn(self, x)

            bind_fn(self, ilayer, x, y)
            enable_trace = True
            all_tensors.extend([x, y])   # 避免torch对tensor进行复用
            ilayer += 1
            return y

        setattr(fn_module, fn_name, myforward)
    return make_hook

@hook_forward("spconv.pytorch.conv.SparseConvolution.forward")
def symbolic_conv2d(self, ilayer, x, y):
    print(f"{type(self)} -> Input {get_obj_idd(x)}, Output {get_obj_idd(y)}")

    inputs = [
        get_obj_idd(x),
        append_initializer(self.weight.data, f"conv{ilayer}.weight")
    ]

    if self.bias is not None:
        inputs.append(append_initializer(self.bias.data, f"conv{ilayer}.bias"))

    nodes.append(
        helper.make_node(
            "SparseConvolution", inputs, [get_obj_idd(y)], f"spconv{ilayer}", 
            kernel_shape=self.kernel_size, group=self.groups, pads=[0, 0] + list(self.padding), dilations=self.dilation, strides=self.stride
        )
    )

@hook_forward("torch.nn.ReLU.forward")
def symbolic_relu(self, ilayer, x, y):
    print(f"{type(self)} -> Input {get_obj_idd(x)}, Output {get_obj_idd(y)}")

    nodes.append(
        helper.make_node(
            "Relu", [get_obj_idd(x)], [get_obj_idd(y)], f"relu{ilayer}"
        )
    )

@hook_forward("torch.Tensor.__add__")
def symbolic_add(self, ilayer, x, y):
    print(f"Add -> Input {get_obj_idd(self)} + {get_obj_idd(x)}, Output {get_obj_idd(y)}")

    nodes.append(
        helper.make_node(
            "Add", [get_obj_idd(self), get_obj_idd(x)], [get_obj_idd(y)], f"add{ilayer}"
        )
    )

@hook_forward("torch.nn.BatchNorm1d.forward")
def node_batchnorm1d(self, ilayer, x, y):
    print(f"{type(self)} -> Input {get_obj_idd(x)}, Output {get_obj_idd(y)}")

    nodes.append(
        helper.make_node(
            "BatchNormalization", 
            [  
                get_obj_idd(x), 
                append_initializer(self.weight, f"bn{ilayer}.weight"),
                append_initializer(self.bias, f"bn{ilayer}.bias"),
                append_initializer(self.running_mean, f"bn{ilayer}.running_mean"),
                append_initializer(self.running_var, f"bn{ilayer}.running_var"),
            ],
            [get_obj_idd(y)],
            epsilon=self.eps,
            momentum=self.momentum,
            name=f"batch_norm{ilayer}"
        )
    )

def append_initializer(value, name):
    initializers.append(
        helper.make_tensor(
            name=name,
            data_type=helper.TensorProto.DataType.FLOAT,
            dims=list(value.shape),
            vals=value.data.cpu().numpy().astype(np.float32).tobytes(),
            raw=True
        )
    )
    return name


def get_obj_idd(obj):
    global objmap

    if isinstance(obj, SparseConvTensor):
        obj = obj.features

    idd = id(obj)
    if idd not in objmap:
        objmap[idd] = str(len(objmap))
    return objmap[idd]

enable_trace = True
all_tensors = []
objmap = {}
nodes = []
initializers = []

voxels = torch.from_numpy(voxels).cuda().half()
coors  = torch.from_numpy(coors).cuda()

x = SparseConvTensor(voxels, coors, spatial_shape, 1)

for name, m in model.named_modules():
    if isinstance(m, nn.ReLU):
        m.inplace = False

with torch.no_grad():
    y = model(x)

inputs = [
    helper.make_value_info(
        name="0",
        type_proto=helper.make_tensor_type_proto(
            elem_type=helper.TensorProto.DataType.FLOAT,
            shape=["n", x.features.size(1)]
        )
    )
]

outputs = [
    helper.make_value_info(
        name=nodes[-1].output[0],
        type_proto=helper.make_tensor_type_proto(
            elem_type=helper.TensorProto.DataType.FLOAT,
            shape=["n", y.features.size(1)]
        )
    )
]

graph = helper.make_graph(
    name="mymodel",
    inputs=inputs,
    outputs=outputs,
    nodes=nodes,
    initializer=initializers
)

# 如果名字不是ai.onnx,netron解析就不是太一样了
opset = [
    helper.make_operatorsetid("ai.onnx", 11)
]

# producer主要是保持和pytorch一致
model = helper.make_model(graph, opset_imports=opset, producer_name="pytorch", producer_version="1.9")
onnx.save_model(model, "scn.onnx")

除了上面提到的几点外,其他的和之前说的没有什么差别。

值得注意的是,导出的 onnx 并不能被 tensorRT 使用,因为它的输入和输出是复合类型,而 tensorRT 不支持这种复合类型,所以只能靠自己构建和推理执行图

导出的 onnx 模型如下图所示:

图3-9 SCN的onnx导出


3.4 执行图的构建


前面我们解决了模型的 onnx 导出问题,现在来探讨模型推理问题,由于 tensorRT 这条路走不通,因此只能自行解析 onnx 单独进行推理。

onnx 是静态的计算图,我们需要构建一个执行图(如图3-10所示),执行图相比静态图最大的区别就是,每个节点都是具有真实值的,每个节点都可以执行具体计算,它是计算图起作用模式下的样子。

图3-10 执行图示例

我们学习 tensorRT 构建执行图的 API,我们可以做如下设计来表示执行图

a = engine.add_input()	# 返回tensor
b = engine.add_input()	# 返回tensor
conv1 = engine.add_conv(a)	# 返回conv layer
conv2 = engine.add_conv(b)	# 返回conv alyer
add = engine.add_add(conv1.output, conv2.output) # 返回 add layer
engine.mark_output(add.output)	# 标记最终需要保留的输出

当我们构建好执行图后

为 a、b 赋值
向 e 索要最新值,即 e.update()
e 的最新值需要通过 e.parent.compute() 得到
add 的 compute 实现为 c 和 d 必须最新值才能计算,因此 c.update(),d.update()
由于 c.update() 需要 c.parent.compute(),因此 Conv1 需要执行 compute,此时 a 已经是最新值,计算后结果给到 c
由于 d.update() 需要 d.parent.compute(),因此 Conv2 需要执行 compute,此时 b 已经是最新值,计算后结果给到 d
然后执行 add 的加法操作,e=c+d
此时,拿到的 e 已经是最新值。作为执行图结果返回

下面是 tensorRT 下的执行图构建方式,我们可以拿来参考

// 构建一个模型
/*
    Network definition:

    image
      |
    linear (fully connected) input = 3, output = 2, bias = True
      |
    sigmoid
      |
    prob
*/

// -------------------------2. 输入,模型结构和输出的基本信息-----------------------
const int num_input = 3;    // in_channel
const int num_output = 2;   // out_channel
float layer1_weight_values[] = {1.0, 2.0, 0.5, 0.1, 0.2, 0.5};  // 前3个给w1的rgb,后3个给w2的rgb
float layer1_bias_values[]   = {0.3, 0.8}

// 输入指定数据的名称、数据类型和完整维度,将输入层添加到网络
nvinfer1::ITensor* input = network->addInput("image", nvinfer1::DataType::kFLOAT, nvinfer1::Dims4(1, num_input, 1, 1));
nvinfer1::Weights layer1_weight = make_weights(layer1_weight_values, 6);
nvinfer1::Weights layer1_bias   = make_weights(layer1_bias_values, 2);
// 添加全连接层
auto layer1 = network->addFullyConnected(*input, num_output, layer1_weight, layer1_bias);   // 注意对input进行了解引用
// 添加激活层
auto prob = network->addActivation(*layer1->getOuput(0), nvinfer1::ActivationType::kSIGMOID);

// 将我们需要的prob标注为输出
network->markOutput(*prob->getOuput(0));

spconv 推理的演示代码如下

import numpy as np

class Tensor:
    def __init__(self, name, parent=None):
        self.name = name
        self.value = 0
        self.parent = parent
    
    def update(self):
        if self.parent is not None:
            self.parent.update()

class Node:
    def __init__(self, name, op_type):
        self.name = name
        self.op_type = op_type
        self.is_computed = False
    
    def update(self):
        if not self.is_computed:
            self.is_computed = True

            for x in self.input:
                x.update()

            self.forward()

class SparseConvolution(Node):
    def __init__(self, name, x, attributes):
        super().__init__(name, "SparseConvolution")
        self.attributes = attributes
        self.input = [x]
        self.output = Tensor(f"{name}.output", self)

    def forward(self):
        self.output.value = self.input[0].value * 0.5
        print(f"Do {self.op_type} x[{self.input[0].value}] * 0.5, output = {self.output.value}")

class BatchNormalization(Node):
    def __init__(self, name, x, attributes):
        super().__init__(name, "BatchNormalization")
        self.input = [x]
        self.attributes = attributes
        self.output = Tensor(f"{name}.output", self)

    def forward(self):
        self.output.value = self.input[0].value + 0.1
        print(f"Do {self.op_type} x[{self.input[0].value}] + 0.1, output = {self.output.value}")


class Engine:
    def __init__(self):
        self.inputs  = []
        self.outputs = []
        self.nodes = []

    def add_input(self, name):
        x = Tensor(name)
        self.inputs.append(x)
        return x

    def mark_output(self, x):
        self.outputs.append(x)
        return x

    def add_spconv(self, name, x, attributes=None):
        x = SparseConvolution(name, x, attributes)
        self.nodes.append(x)
        return x

    def add_bn(self, name, x, attributes=None):
        x = BatchNormalization(name, x, attributes)
        self.nodes.append(x)
        return x

    def forward(self, x):

        for n in self.nodes:
            n.is_computed = False

        self.inputs[0].value = x
        self.outputs[0].update()
        return self.outputs[0].value


engine = Engine()
x = engine.add_input("input")
spconv0 = engine.add_spconv("spconv0", x)
bn1     = engine.add_bn("bn1", spconv0.output)
spconv1 = engine.add_spconv("spconv1", bn1.output)
bn1     = engine.add_bn("bn1", spconv1.output)
pred    = engine.add_spconv("pred", bn1.output)
engine.mark_output(pred.output)

print(engine.forward(1))

运行效果如下:

图3-11 infer0输出
上述代码演示了一个简化的 ONNX 执行图构建过程。其中,Engine 类用于构建和运行计算图,通过添加输入张量和各种操作节点来定义计算图的结构。每个节点通过重写 forward() 方法来定义其具体的计算操作,并通过调用 update() 方法逐层更新计算图中的张量值。通过设置输入张量的值并调用 forward() 方法,可以实现计算图的前向传播,得到输出结果。

3.5 onnx解析并创建执行图


由于 onnx 格式储存的计算图是一个很友好的方式,因此配合好 onnx 的格式与我们写的 add 系列的 api,我们就可以直接实现一个 onnx 解析器,直接解析并放到 engine 里面。

演示的示例代码如下

import numpy as np
import onnx

class Tensor:
    def __init__(self, name, parent=None):
        self.name = name
        self.value = 0
        self.parent = parent
    
    def update(self):
        if self.parent is not None:
            self.parent.update()

class Node:
    def __init__(self, name, op_type):
        self.name = name
        self.op_type = op_type
        self.is_computed = False
    
    def update(self):
        if not self.is_computed:
            self.is_computed = True

            for x in self.input:
                x.update()

            self.forward()

class SparseConvolution(Node):
    def __init__(self, name, x, attributes):
        super().__init__(name, "SparseConvolution")
        self.attributes = attributes
        self.input = [x]
        self.output = Tensor(f"{name}.output", self)

    def forward(self):
        self.output.value = self.input[0].value * 0.5
        print(f"Do {self.op_type} x[{self.input[0].value}] * 0.5, output = {self.output.value}")

class ReLU(Node):
    def __init__(self, name, x):
        super().__init__(name, "ReLU")
        self.input = [x]
        self.output = Tensor(f"{name}.output", self)

    def forward(self):
        self.output.value = max(0, self.input[0].value)
        print(f"Do {self.op_type} max(0, x[{self.input[0].value}]), output = {self.output.value}")


class Add(Node):
    def __init__(self, name, a, b):
        super().__init__(name, "Add")
        self.input = [a, b]
        self.output = Tensor(f"{name}.output", self)

    def forward(self):
        self.output.value = self.input[0].value + self.input[1].value
        print(f"Do {self.op_type} a[{self.input[0].value}] + b[{self.input[1].value}], output = {self.output.value}")


class BatchNormalization(Node):
    def __init__(self, name, x, attributes):
        super().__init__(name, "BatchNormalization")
        self.input = [x]
        self.attributes = attributes
        self.output = Tensor(f"{name}.output", self)

    def forward(self):
        self.output.value = self.input[0].value + 0.1
        print(f"Do {self.op_type} x[{self.input[0].value}] + 0.1, output = {self.output.value}")


class Engine:
    def __init__(self):
        self.inputs  = []
        self.outputs = []
        self.nodes = []

    def add_input(self, name):
        x = Tensor(name)
        self.inputs.append(x)
        return x

    def mark_output(self, x):
        self.outputs.append(x)
        return x

    def add_spconv(self, name, x, attributes=None):
        x = SparseConvolution(name, x, attributes)
        self.nodes.append(x)
        return x

    def add_relu(self, name, x):
        x = ReLU(name, x)
        self.nodes.append(x)
        return x
    
    def add_add(self, name, a, b):
        x = Add(name, a, b)
        self.nodes.append(x)
        return x

    def add_bn(self, name, x, attributes=None):
        x = BatchNormalization(name, x, attributes)
        self.nodes.append(x)
        return x

    def forward(self, x):

        for n in self.nodes:
            n.is_computed = False

        self.inputs[0].value = x
        self.outputs[0].update()
        return self.outputs[0].value


def load_engine(onnxfile):

    model = onnx.load(onnxfile)
    engine = Engine()
    name_to_tensor = {}

    for i in model.graph.input:
        x = engine.add_input(i.name)
        name_to_tensor[x.name] = x
        
    for n in model.graph.node:
        if n.op_type == "SparseConvolution":
            layer = engine.add_spconv(n.name, name_to_tensor[n.input[0]], n)
            name_to_tensor[n.output[0]] = layer.output
        elif n.op_type == "BatchNormalization":
            layer = engine.add_bn(n.name, name_to_tensor[n.input[0]], n)
            name_to_tensor[n.output[0]] = layer.output
        elif n.op_type == "Relu":
            layer = engine.add_relu(n.name, name_to_tensor[n.input[0]])
            name_to_tensor[n.output[0]] = layer.output
        elif n.op_type == "Add":
            layer = engine.add_add(n.name, name_to_tensor[n.input[0]], name_to_tensor[n.input[1]])
            name_to_tensor[n.output[0]] = layer.output
        else:
            raise RuntimeError(f"Unsupport op_type {n.op_type}")

    for o in model.graph.output:
        engine.mark_output(name_to_tensor[o.name])

    return engine    
    

engine = load_engine("scn.onnx")
print(engine.forward(1.0))

运行效果如下:

图3-12 infer1输出

至此,整个 onnx 解析器已经完成。你应该在 C++ 上复现这个流程就可以实现无 trt 推理了

4 bevfusion 输出

    print("Tracing model inference...")
    print("> Do inference...")
    with torch.no_grad():
        register_tensor(voxels)
        enable_trace = True
        y = model(voxels, coors, batch_size)  # 执行这里,调用符号函数
        enable_trace = False

    print("Tracing done!")

Tracing model inference...
> Do inference...
   --> SparseConvolutionQunat0[subm] -> Input 0, Output 1
Backend TkAgg is interactive backend. Turning interactive mode on.
   --> SparseConvolutionQunat1[subm] -> Input 1, Output 2
   --> SparseConvolutionQunat2[subm] -> Input 2, Output 3
   --> QuantAdd0 -> Input 3 + 1, Output 4
   --> ReLU0 -> Input 4, Output 5
   --> SparseConvolutionQunat3[subm] -> Input 5, Output 6
   --> SparseConvolutionQunat4[subm] -> Input 6, Output 7
   --> QuantAdd1 -> Input 7 + 5, Output 8
   --> ReLU1 -> Input 8, Output 9
   --> SparseConvolutionQunat5[conv] -> Input 9, Output 10
   --> SparseConvolutionQunat6[subm] -> Input 10, Output 11
   --> SparseConvolutionQunat7[subm] -> Input 11, Output 12
   --> QuantAdd2 -> Input 12 + 10, Output 13
   --> ReLU2 -> Input 13, Output 14
   --> SparseConvolutionQunat8[subm] -> Input 14, Output 15
   --> SparseConvolutionQunat9[subm] -> Input 15, Output 16
   --> QuantAdd3 -> Input 16 + 14, Output 17
   --> ReLU3 -> Input 17, Output 18
   --> SparseConvolutionQunat10[conv] -> Input 18, Output 19
   --> SparseConvolutionQunat11[subm] -> Input 19, Output 20
   --> SparseConvolutionQunat12[subm] -> Input 20, Output 21
   --> QuantAdd4 -> Input 21 + 19, Output 22
   --> ReLU4 -> Input 22, Output 23
   --> SparseConvolutionQunat13[subm] -> Input 23, Output 24
   --> SparseConvolutionQunat14[subm] -> Input 24, Output 25
   --> QuantAdd5 -> Input 25 + 23, Output 26
   --> ReLU5 -> Input 26, Output 27
   --> SparseConvolutionQunat15[conv] -> Input 27, Output 28
   --> SparseConvolutionQunat16[subm] -> Input 28, Output 29
   --> SparseConvolutionQunat17[subm] -> Input 29, Output 30
   --> QuantAdd6 -> Input 30 + 28, Output 31
   --> ReLU6 -> Input 31, Output 32
   --> SparseConvolutionQunat18[subm] -> Input 32, Output 33
   --> SparseConvolutionQunat19[subm] -> Input 33, Output 34
   --> QuantAdd7 -> Input 34 + 32, Output 35
   --> ReLU7 -> Input 35, Output 36
   --> SparseConvolutionQunat20[conv] -> Input 36, Output 37
   --> ToDense0[[180, 180, 2]][[1, 128, 180, 180, 2]] -> Input 37, Output 38
   --> Permute0[(0, 1, 4, 2, 3)][[1, 128, 2, 180, 180]] -> Input 38, Output 39
   --> Reshape0[(1, 256, 180, 180)] -> Input 39, Output 40
Tracing done!

<think>好的,我现在遇到了一个PyTorch的运行时错误:RuntimeError: permute(sparse_coo): number of dimensions in the tensor input does not match the length of the desired ordering of dimensions i.e. input.dim() = 4 is not equal to len(dims) = 3。这让我有点困惑,需要仔细分析问题所在。 首先,错误信息指出在使用permute操作时,张量的维度数与permute参数的数量不匹配。具体来说,输入张量的维度是4,而permute的参数列表长度是3。根据引用[3],permute的参数列表长度必须等于张量的维度数。例如,对于一个形状为$torch.Size([8, 12, 197, 64])$的4维张量,正确的permute操作需要4个参数,如`permute(0,1,3,2)`,而如果参数数量不对,就会报错。 接下来,我需要检查代码中permute的使用情况。可能是在某个地方错误地传递了3个参数,而张量是4维的。例如,假设原本应该写成`permute(0,1,3,2)`,但错误地写成了`permute(0,1,3)`,这样参数数量不足就会导致错误。 另外,根据错误信息中的“sparse_coo”,可能张量是稀疏格式的。PyTorch中稀疏张量对某些操作支持有限,可能需要先将张量换为密集张量再进行permute操作。可以通过`.to_dense()`方法换,但需要注意内存使用情况。 还有可能是在模型结构中的某个层或操作中,错误地处理了张量的维度。例如,在Transformer模型中,注意力机制通常涉及维度变换,如果在这里参数设置错误,也会引发问题。引用[4]提到的Transformer模型实现可能需要特别注意维度匹配。 接下来,我需要逐步排查: 1. **检查所有permute操作的参数数量**:确保每个permute的参数数量与张量的维度数一致。例如,4维张量需要4个参数。 2. **确认张量类型是否为稀疏张量**:如果是稀疏张量,考虑换为密集张量后再进行操作,或者检查是否支持该操作。 3. **使用print或调试工具输出张量的shape**:在permute操作前打印张量的形状,确认维度数是否符合预期。 4. **查看模型结构中的维度变换**:特别是在自定义层或复杂结构中,确保维度变换逻辑正确。 例如,假设错误出现在以下代码段: ```python x = x.permute(0, 2, 1) # 错误,原张量是4维的 ``` 应修改为: ```python x = x.permute(0, 1, 3, 2) # 正确的参数数量 ``` 如果张量是稀疏的,可能需要: ```python x = x.to_dense().permute(0, 1, 3, 2) ``` 此外,参考引用[1]和[2],在处理张量形状变换时,reshape和view等操作可能需要确保张量是连续的。虽然错误直接指向permute,但之前操作可能导致张量不连续,间接引发问题,不过此处错误更直接的是参数数量问题。 总结解决步骤: - 核对permute参数数量与张量维度数 - 换稀疏张量为密集格式(如适用) - 检查模型各层的维度处理逻辑</think>要解决PyTorch中`permute`操作导致的`RuntimeError`,需遵循以下步骤: ### 错误原因分析 错误信息: ``` RuntimeError: permute(sparse_coo): number of dimensions in the tensor input does not match the length of the desired ordering of dimensions i.e. input.dim() = 4 is not equal to len(dims) = 3 ``` 表明: 1. **参数数量不匹配**:输入的4维张量使用了仅3个参数的`permute`操作,如`permute(0,1,2)`。 2. **稀疏张量限制**:若张量是稀疏格式(`sparse_coo`),PyTorch对稀疏张量的`permute`操作有严格限制[^3]。 --- ### 解决方案 #### 1. 检查`permute`参数数量 - **规则**:`permute`的参数数量必须等于张量的维度数。例如,4维张量需要4个参数。 ```python # 错误示例:4维张量仅用3个参数 x = torch.rand(2,3,4,5) x.permute(0,1,2) # 参数数量错误 # 正确示例:提供4个参数 x.permute(0,1,3,2) # 交换最后两个维度 ``` #### 2. 处理稀疏张量 - **换为密集张量**:若张量为稀疏格式,先使用`.to_dense()`换为密集张量: ```python sparse_tensor = ... # 假设是稀疏张量 dense_tensor = sparse_tensor.to_dense() dense_tensor.permute(0,1,3,2) ``` - **注意**:稀疏张量可能不支持某些维度交换操作[^3]。 #### 3. 调试维度信息 - **打印张量形状**:在`permute`操作前输出张量的维度: ```python print(x.shape) # 例如输出torch.Size([8,12,197,64]) x = x.permute(0,1,3,2) # 正确参数数量 ``` #### 4. 检查模型结构中的维度逻辑 - **例如Transformer中的注意力机制**: ```python # 假设q是形状为[8,12,197,64]的张量 q = q.permute(0,1,3,2) # 交换最后两维 # 错误的写法:q.permute(0,1,3) (参数不足) ``` 需确保多头注意力(Multi-Head Attention)中的维度变换逻辑正确[^4]。 --- ### 示例修正 假设原错误代码为: ```python # 输入张量形状:[batch, seq_len, heads, dim] q = torch.rand(8, 197, 12, 64) # shape: (8,197,12,64) q = q.permute(0, 2, 1) # 错误:参数数量不足(应为4个) ``` 修正后: ```python q = q.permute(0, 2, 1, 3) # 正确参数数量 # 或调整维度顺序 q = q.permute(0, 2, 1, 3).contiguous() # 确保连续性 ``` --- ### 总结步骤 1. **核对参数数量**:确保`permute`参数数与张量维度一致。 2. **处理稀疏性**:必要时换张量为密集格式。 3. **验证模型逻辑**:检查Transformer等复杂模型中的维度操作。 4. **调试输出**:通过打印形状定位错误位置。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值