Posts Pytorch关键模块解读
Post
Cancel

Pytorch关键模块解读

目录

  1. 前言
  2. torch.autograd: 梯度计算
  3. BN & SyncBN: BN与多卡同步BN
  4. torch.utils.data: 解析数据处理全流程
  5. nn.Module: 核心网络模块接口
  6. DP & DDP: 模型并行和分布式训练
  7. torch.optim: 优化算法接口
  8. torch.cuda.amp: 自动混合精度
  9. cpp_extension: C++/CUDA算子实现和调用全流程

前言

主要参考:MMLab知乎源码解读 img

torch.autograd: 梯度计算

梯度计算主要涉及以下几个模块:

  • torch.autograd.function (函数的反向传播) Pytorch 中的模块nn.Module 通常是包裹了autograd function ,以其作为真正实现的部分,如nn.ReLU 实际使用的是:torch.nn.functional.relu(F.relu)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    
        from torch.nn import functional as F
    
        class ReLU(Module):
            __constants__ = ['inplace']
            inplace: bool
    
            def __init__(self, inplace: bool = False):
                super(ReLU, self).__init__()
                self.inplace = inplace
    
            def forward(self, input: Tensor) -> Tensor:
                # F.relu实际包裹的函数类型为builtin_function_or_method
                # 其内部定义了forward,backward函数,描述了梯度前向/反向传播的过程
                # 通常使用C++实现(如ATen)
                return F.relu(input, inplace=self.inplace)
    

    基于torch.autograd.function 中定义的Function 类作为基类可以实现自定义的autograd function 函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
        class Exp(Function): # 此层计算e^x
    
            @staticmethod
            def forward(ctx, i): # 模型前向
                result = i.exp()
                ctx.save_for_backward(result) # 保存所需内容,以备backward时使用,所需的结果会被保存在saved_tensors元组中;此处仅能保存tensor类型变量,若其余类型变量(Int等),可直接赋予ctx作为成员变量,也可以达到保存效果
    
            @staticmethod
            def backward(ctx, grad_output): # 模型梯度反传
                result, = ctx.save_tensors # 取出forward中保存的result
                return grad_output * result
    
    
            # 使用样例
            x = torch.tensor([1.], requires_grad=True) # 注意设置tensor的requires_grad为True
            ret = Exp.apply(x) # 使用apply方法调用自定义autograd function
            print(ret) # (tensor[2.7183], grad_fn=<ExpBackward>)
            ret.backward() # 反传梯度
            print(x.grad) # tensor([2.7183])
    
  • torch.autograd.functional (计算图的反向传播) Tensor类的backward方法实际调用的是torch.autograd.backward 接口。pytorch实现中,autograd会记录生成前variable的所有操作,并建立一个有向无环图(DAG)。反向传播的过程中,autograd会沿着图从当前变量(根结点F)溯源,可以通过链式求导法则计算所有叶子节点的梯度。以下简单实现在计算图上进行autograd:
    1
    2
    3
    4
    5
    6
    7
    8
    
        # 样例仅适用于每个op只产生一个输出的情况,且效率很低
        def autograd(grad_fn, gradient):
            auto_grad = {}
            queue = [[grad_fn, gradient]]
            while queue != []:
                item = queue.pop()
                gradients = item[0](item[1])
                functions = [[x[0] for x in item[0].next_functions]]
    
  • torch.autograd.gradcheck (数值梯度检查)
  • torch.autograd.anomaly_mode (在自动求导时检测错误产生路径)
  • torch.autograd.grad_mode (设置是否需要梯度)
  • model.eval和torch.no_grad()
  • torch.autograd.profiler (提供function级别的统计信息)

BN & SyncBN: BN与多卡同步BN

简单谈下BN的优势:

  • 防止过拟合:结合mini-batch训练,能够避免对单个样本的过拟合
  • 加快收敛:原始是BN的归一化效果
  • 防止梯度弥散/爆炸:归一化是输入分布位于激活函数的非饱和区(饱和区的激活函数输出值很小,容易造成梯度弥散/爆炸)

BN的公式: img

BN实现细节:

  • BN默认打开track_running_stats ,因此每次forward时都会依据当前minibatch的统计量来更新running_mean和running_var。更新的方式通过momentum参数进行更新(默认0.1): img

Pytorch 0.4.1后,加入num_batches_tracked属性,目的是统计BN一共forward了多少minibatch。当momemtum被设置为None时,就由num_batches_tracked来控制历史统计量和当前minibatch的影响占比: img

BN的性能和batch size有很大的关系。batch size越大,BN的统计量也会越准。但是对于输入数据量较大的情况,单张显卡的batch size通常较小。解决该问题的一个方案是:SyncBN,即所有卡共享一个BN,得到全局的统计量。 Pytorch的SynBN实现分别在torch/nn/modules/batchnorm.pytorch/nn/modules/_functions.py 。其中,前者主要负责检查输入的合法性,以及根据momentum等设置进行传参,调用后者。而后者则负责计算单卡统计量以及进程间通信。 具体实现中,单卡上的BN会计算单卡对应输入的均值,方差,然后做Normalize;SyncBN则会得到全局的统计量,其实现的过程为:

  • 单卡分别按照各自输入进行sum/square sum
  • 进行所有卡的synchronize,得到global mean & std进行同步
  • 单卡按照global mean & std进行各自的normalize img

backward,计算weightbias 的梯度以及dydy/du 计算x的梯度,基本步骤

  • all reduce计算梯度之和
  • 根据总的size,对梯度计算平均
  • backward进行梯度计算

torch.utils.data: 解析数据处理全流程

torch的data模块核心是迭代器。其关键组件包括:

  • Dataset:负责对raw data source进行封装,通常包含两种:Map-style datasets和iterable datasets两种,前者通过__getitem__ 实现数据获取,后者通过__iter__ 实现数据获取。Map-style datasets的应用较多,iterable datasets适用的情况是:随机读取的代价很大甚至不可能,且batch size取决于获取的数据。此外还包括:ConcatDatasets/ChainDataset/SubSet/TensorDataset等其他Dataset
  • Sampler:负责提供一种遍历数据集所有元素索引的方式。可支持用于自定义或者PyTorch提供。
  • DataLoader:PyTorch数据加载的核心,复杂加载数据,同时支持Map-style和Iterable-style Dataset,支持单进程/多进程,还可以设置loading order,batch size,pin memory等参数。
  • 三者关系:Dataloader负责总的调度,命令Sampler定义遍历索引的方式,然后通过索引去Dataset中提取元素。

几个核心点:

  • 批处理(默认True):通过collate_fn实现,主要是将输入样本整理为一个batch,一般做下面三件事情:
    • 添加新的批次维度
    • 将Numpy数组和Python数值转化为PyTorch张量
    • 保留原始数据结构。
  • 多进程(multi-process):
    • 单进程:num_workers为0,可以显示更多可读的错误跟踪,对于调试很有用。
    • 多进程:num_workers控制,dataset,collate_fn,worker_init_fn都会传到每个worker中,每个worker都会用独立的进程。对于map-style数据,shuffle会在主线程完成,然后将用Sampler产生的index传到每个worker中。对于iterable-style数据,因为每个worker都有相同的data复制样本,并在每个进程中进行不同操作,所以需要通过get_worker_info()来进行辅助处理防止每个进程输出的数据重复。另外,由于多进程使用CUDA和共享CUDA张量的时候可能发生问题,建议采用pin_memory=True,以使数据能够快速传输到支持CUDA的GPU,不建议在使用多线程的情况下返回CUDA的tensor。
  • Memory Pinning:
    • 锁页内存:锁页内存存放的内容在任何情况都不会和虚拟内存进行交换(虚拟内存指硬盘)。非锁页内存则会在内存不足的时候,将数据存放在虚拟内存中。pin_memory=True 时,内存中的Tensor转义到GPU的显存就会更快些。
  • Prefetch:
    • Dataloader通过指定prefetch_factor (默认为2)来进行数据的预取。其适用于多进程加载。

nn.Module: 核心网络模块接口

前置介绍Pytorch中的几个关键名词概念:parameter/buffer,引用自:知乎Pytorch解读。parameter指反向传播需要被optimizer更新的;buffer指反向传播不需要被optimizer更新。 torch的各个组件关系概览: img torch模块的组织和实现的几个常见特点:

  • 基类定义接口,通过继承来处理不同维度的输入
  • 每个类都有对应的nn.functional 函数,forward函数中将arguments/parameters传给nn.functional 的对应函数来实现forward功能。
  • 继承nn.Module 的模块主要重载init / forward / extra_repr 函数,含有parameters的模块还会实现reset_parameters 函数来初始化参数。

nn.Module 实现:

  • 常用接口:
    • __init__ :初始化一系列重要的成员变量
    • 状态转化:通过self.training 来区分训练/测试两种状态
    • 参数转化/转移:包括CPU/GPU的转移以及不同类型的转化,主要通过self._apply (function)来实现的。self._apply 包括三个步骤:self.children() 来实现递归调用对self._parameters 中的参数及其gradient通过function进行处理对self._buffers 中的buffer逐个通过function进行处理。
  • 属性的增删改查:
    • 属性修改包含三个函数:add_module (增加子神经网络模块)/register_parameter (增加通过BP更新的parameter)/register_buffer (增加不通过BP更新的buffer)。常用的方式为:self.xxx = xxx 来进行属性的增加/修改,本质上是调用nn.Module 重载的函数 __setattr__ ;但是需要注意的是:self.xxx = torch.Tensor() 是一种不被推荐的行为,因为该行为新增的attribute不属于self._parameters/self._buffers,而是被认为是普通的attribue,在进行状态转化时,self.xxx会被遗漏而导致device或者type不一样的bug。
    • 属性删除:__delattr__ 实现,会挨个检查self._parametersself._buffersself._modules 和普通的attribute并将name从中删除。
  • Forward & Backward
    • Hooks:nn.Module类实现了3个通用的hook注册函数,用于注册被应用于全局的hook。该三个函数分别注册进3个全局的OrderedDict:_global_backward_hooks / _global_forward_pre_hooks / _global_forward_hooks 。同时,nn.Module 也支持注册应用于自身的forward/backward hook:self._backward_hooks / self._forward_pre_hooks / self._forward_hooks 。各个hook的调用顺序如下: img
  • 模块存取:
    • Hooks:_register_state_dict_hook :在self.state_dict() 的最后对模块导出的state_dict进行修改;_register_load_state_dict_pre_hook :在_load_from_state_dict 中最先被执行。
    • 实现细节:
      • state_dict() 可用于获取模型当前的状态信息。其中,版本信息(_version )存于metadata中。数据保存通过_save_to_state_dict() 实现,保存内容包括self._parameters 以及self._buffers 中的persistent buffer。
      • load_state_dict() 函数用于读取checkpoint。本质上是调用_load_from_state_dict 函数来加载所需权重。每个子模块可以重定义自身的_load_from_state_dict 函数,来避免BC-breaking (向后兼容)等问题。

DP & DDP: 模型并行和分布式训练

  • DP:
    • 基本使用:model=nn.DataParallel (model).
    • 原理:DP基于单机多卡,其中device[0]负责梯度整合和传播。包含三个核心过程:各卡分别计算损失和精度;所有梯度整合到device[0];device[0]进行参数更新,同时其他卡拉取device[0]参数进行参数更新。其中各卡计算损失和精度的过程是并行的。
    • 实现:前向传播时,通过Scatter函数将数据从device[0]分配并复制到不同的卡,后用Replicate函数将模型从device[0]复制到不同的卡,然后各卡分别调用forward计算损失和梯度。反向传播的时候,通过gather函数将梯度收集到device[0]然后在device[0]更新参数。
  • DDP:
    • 基本使用:
1
2
3
4
5
6
7
8
# 初始化后端
torch.distributed.init_process_group(backend='nccl', world_size=ws, init_method='env://')
torch.cuda.set_device(local_rank)
device = torch.device(f'cuda:{local_rank}')
model = nn.Linear(2, 3).to(device)

# 这里通过规定device_id采用了单卡单进程
model = DDP(model, device_ids=[local_rank], output_device=local_rank).to(device)
1
2
3
4
5
-   原理:DDP也是数据并行的方式,即每张卡中都有模型和输入。通过Reducer来管理梯度同步。通过构建时注册autograd hook来进行梯度同步,当一个梯度计算好后,相应的hook会通知DDP进行规约。当某一梯度值满后(对应单个bucket),Reducer会启动异步的allreduce去计算所有进程的平均值。整个过程中,DDP可以边计算边通信,提高效率。当所有的bucket的梯度都完成计算后,Reducer会等所有allreduce完成,然后将得到的梯度更新:写到param.grad。
-   实现:backend通常采用NCCL,不过只支持GPU Tensor的通信。具体组成主要包含了三个部分:
    -   constructor:负责将rank 0的state_dict()广播,保证网络的初始状态一致;初始化buckets,控制parameters在buckets的逆序排列,提高桶通信的效率;为每个parameter加上grad_accumulator以及在autograd_graph注册autograd_hook,负载在backward时进行梯度同步
    -   forward:包含了正常的forward操作以及检查unused_parameters的操作:DDP会在forward结束时对traverse autograd graph找到没用过的parameters并标记为ready:开销很大,但是在动态图中是很有必要的,因为动态图可能会发生改变。
    -   autograd_hook:挂在autograd graph中负责在backward时负责梯度同步。 ```

torch.optim: 优化算法接口

基本概念:

  • 组成部分: 优化器:训练阶段模型可学习参数的更新策略。 学习率:lr
  • 调用过程:optimizer.zero_grad() 清空梯度->loss.backward() 反向传播->optimizer.step() 更新模型参数。

简单的调用示例:

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
31
32
33
34
35
36
37
    import torch
    import numpy as np
    import warnings
    warnings.filterwarnings('ignore') # ignore warnings

    x = torch.linspace(-np.pi, np.pi, 2000)
    y = torch.sin(x)

    p = torch.tensor([1, 2, 3])
    xx = x.unsqueeze(-1).pow(p)

    model = torch.nn.Sequential(
        torch.nn.Linear(3, 1),
        torch.nn.Flatten(0, 1)
    )
    loss_fn = torch.nn.MSELoss(reduction='sum')

    learning_rate = 1e-3
    optimizer = torch.optim.RMSprop(model.parameters(), lr=learning_rate)
    for t in range(1, 1001):
        y_pred = model(xx)
        loss = loss_fn(y_pred, y)
        if t % 100 == 0:
            print('No.{: 5d}, loss: {:.6f}'.format(t, loss.item()))
            optimizer.zero_grad() # 梯度清零
            loss.backward() # 反向传播计算梯度
            optimizer.step() # 梯度下降法更新参数
            No.  100, loss: 26215.714844
            No.  200, loss: 11672.815430
            No.  300, loss: 4627.826172
            No.  400, loss: 1609.388062
            No.  500, loss: 677.805115
            No.  600, loss: 473.932159
            No.  700, loss: 384.862396
            No.  800, loss: 305.365143
            No.  900, loss: 229.774719
            No. 1000, loss: 161.483841

实现细节:

  • optimizer:
    • 支持的optimizer:SGD/ASGD/Adadelta/Adagrad/Adam/AdamW/Adamax/SparseAdam/RMSprop/Rprop/LBFGS
    • 基类Optimizer:公用方法包括:add_param_group/step/zero_grad/state_dict/load_state_dict。其中step方法由子类(具体的optimizer类)实现。
  • lr_scheduler:
    • 支持的lr_scheduler:StepLR/MultiStepLR/ExponentialLR/ReduceLROnPlateau/CyclicLR/OneCycleLR/CosineAnnealingLR/CosineAnnealingWarmRestarts/LambdaLR/MultiplicativeLR
    • 基类_LRScheduler:公用方法包括:step/get_lr/get_last_lr/print_lr/state_dict/load_state_dict。其中get_lr需要子类实现,state_dict和load_state_dict可能子类会重写。step函数为核心方法,该方法的调用逻辑为:last_epoch进行自增,然后调用子类的get_lr方法获得该epoch时的学习率,并更新到optimizer的param_groups属性之中,最后记录下最后一次调整的学习率到self._last_lr

torch.cuda.amp: 自动混合精度

自动混合精度训练:训练FP32模型时,一部分算子操作精度为FP16,其余算子精度为FP32,而具体哪些算子用FP16还是FP32,通过amp自动安排实现。该方案的优势是:不改变模型,不降低模型训练精度的前提下,可以 缩短训练时间,降低存储要求 。因此能够支持更多的batch size/更大模型和尺寸更大的输入进行训练。

  1. 混合精度训练机制
    • amp机制包含两个核心功能:自动选择合适的数值精度/对于FP16的梯度数值溢出问题,提供梯度scaling操作。
    • autocast作为Python上下文管理器和装饰器来使用,用于指定脚本中某个区域,或者某些函数,按照自动混合精度来运行。其操作过程如下(训练过程中拷贝为FP16模型,FP16算子基于FP16数据进行操作,FP32算子输入输出仍为FP6,计算精度为FP32,之后基于混合精度进行反向传播得到FP16梯度,最后将FP16梯度和FP32参数进行参数更新): img
    • scaling操作:FP16精度的表示范围较窄,存在问题是大量非0梯度会遇到溢出问题。解决方案为:对梯度乘2N系数,称之为scale factor,将梯度shift到FP16的表示范围。该操作通过GradScaler实现,其功能在于反向传播前给loss乘scale factor,然后在梯度更新前进行unscale操作。最终amp的训练流程可归纳为:

      • 维护一个FP32数值精度模型的副本
      • 在每个iteration
        • 拷贝并且转换为FP16模型
        • 前向传播(FP16的模型)
        • loss乘scale factor s
        • 反向传播(FP16的模型参数和参数梯度)
        • 参数梯度乘 1/s(unscaling)
        • 利用FP16的梯度更新FP32的模型参数

      对于scale factor的选取的动态策略:根据loss动态变化。GradScaler涉及的scale factor每隔N个iteration乘一个大于1的系数,再scale loss;并且每次更新前检查溢出问题(inf/nan),如果有,scale factor乘一个小于1的系数并跳过iteration的参数更新环节,如果没有,则正常更新参数。训练流程如下:

      • 维护一个FP32数值精度模型的副本
      • 初始化s
      • 在每个iteration
        1. 拷贝并且转换成FP16模型
        2. 前向传播(FP16的模型参数)
        3. loss乘scale factor s
        4. 反向传播(FP16的模型参数和参数梯度)
        5. 检查有无inf/nan的参数梯度
          • 有:降低s,回到步骤1
          • 无:继续至6
        6. 参数梯度乘1/s
        7. 利用梯度更新FP32的模型参数
  2. 实现细节
    • API调用,基本操作如下:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      
          # amp依赖Tensor core架构,所以model参数必须是cuda tensor类型
          model = Net().cuda()
          optimizer = optim.SGD(model.parameters(), ...)
          # GradScaler对象用来自动梯度缩放
          scaler = GradScaler()
      
          for epoch in epochs:
              for input, target in data:
                  optimizer.zero_grad()
                  # 在autocast enable区域运行forward
                  with autocast():
                      # model做一个FP16的副本,forward
                      output = model(input)
                      loss = loss_fn(output, target)
                  # 用scaler, scale loss(FP16), backward得到scaled的梯度(FP16)
                  scaler.scale(loss).backward()
                  # scaler更新参数,会先自动unscale梯度
                  # 如果有nan或inf,自动跳过
                  scaler.step(optimizer)
                  # scaler factor更新
                  scaler.update()
      
  3. autocast类:作为上下文管理器和装饰器来使用,为算子自动安排按照FP16/FP32进行运算。autocast算子包含三类:FP16/FP32/按照FP16-FP32更大精度操作。对于autocast的enable区域 计算 得到的FP16数值精度变量在enable区域需要显示转化为FP32。另外,autocast作为装饰器使用(通常对于data parallel模型),设计为“thread local”,所以在main thread上设autocast区域是不work的。正确的姿势是对forward进行装饰:
    1
    2
    3
    4
    5
    
        class MyModel(nn.Module):
            ...
            @autocast()
            def forward(self, input):
                ...
    

    或者在forward函数中设autocast区域:

    1
    2
    3
    4
    5
    
        class MyModel(nn.Module):
            ...
            def forward(self, input):
                with autocast():
                    ...
    

    对于用户自定义的autograd函数,需要用amp.custom_fwd 装饰forward函数,amp.custom_bwd 装饰backward函数。

  4. GradScaler类:几个关键方法包括:scale(output)/step/update:
    • scale:对outputs乘scale factor,并返回,如果enable=False,就原样返回。

    • step:梯度unscale,如果之前未手动调用unscale方法的话;检查梯度溢出,如果没有nan/inf,就执行optimizer的step,如果有就跳过。

    • update:在每个iteration结束前调用,如果参数更新则跳过,否则会将scale factor乘以backoff_factor。或者到了该增长的iteration时,将scale factor乘growth_factor,或者用new_scale直接更新scale factor。简单的使用样例:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      
          # Gradient clipping
          scaler = GradScaler()
      
          for epoch in epochs:
              for input, target in data:
                  optimizer.zero_grad()
                  with autocast():
                      output = model(input)
                      loss = loss_fn(output, target)
      
                  scaler.scale(loss).backward()
      
                  # unscale梯度,可以不影响clip的threshold
                  scaler.unscale_(optimizer)
      
                  # clip梯度
                  torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm)
      
                  # unscale_()之前已被显式调用,scaler正常执行step更新参数,有nan/inf则会跳过
                  scaler.step(optimizer)
                  scaler.update()
      

cpp_extension: C++/CUDA算子实现和调用全流程

Pytorch的C++/CUDA扩展能够对功能模块进行加速。下文以nms的实现来解释C++/CUDA算子的调用流程。 调用nms的样例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    from mmcv import _ext as ext_module
    from torch.autograd import Function

    def nms(boxes, scores, iou_threshold, offset=0):
        inds = NMSop.apply(boxes, scores, iou_threshold, offset)
        dets = torch.cat((boxes[inds], scores[inds].reshape(-1, 1)), dim=1)
        return dets, inds

    class NMSop(torch.autograd.Function):
        @staticmethod
        def forward(ctx, bboxes, scores, iou_threshold, offset):
            inds = ext_module.nms(
                bboxes, scores, iou_threshold=float(iou_threshold), offset=offset)
            return inds

        @staticmethod
        def symbolic(g, bboxes, scores, iou_threshold, offset):
            pass # onxx转换相关

NMSop中的核心部分调用了mmcv._ext.nms ,存在于mmcv/_ext.cpython-xxx.so 文件中,通过 MMCV_WITH_OPS=True python setup.py build_ext --inplace 编译得到。 其中,setup.py入口为 setup 函数,该函数的一个主要参数为 ext_modules ,通过 get_extensions 函数得到,该函数会分别基于不同环境:C++/CUDA调用不同的拓展:CppExtension/CUDAExtension。两个拓展函数会将系统目录的库/头文件加入默认的编译搜索路径中,同时补充其他编译信息,最后生成gcc/nvcc命令:

1
2
3
4
5
6
7
8
    setup(
        name='mmcv',
        install_requires=install_requires,
        # 需要编译的c++/cuda扩展
        ext_modules=get_extensions(),
        # cmdclass为python setup.py --build_ext命令指定行为
        cmdclass={'build_ext': torch.utils.cpp_extension.BuildExtension}
    )

编译完成后,需要进行将C++/CUDA的二进制文件和Python进行连接,主要通过pybind11库实现,该库是用于在C++代码中创建Python的连接的库。pybind.cpp中的核心代码如下:

1
2
3
4
5
6
7
8
9
10
    #include <torch/extension.h>
    // 函数声明,具体实现在其他文件
    Tensor nms(Tensor boxes, Tensor scores, float iou_threshold, int offset);

    // TORCH_EXTENSION_NAME在编译命令中传入为_ext,表示了声明了名为_ext的Python module
    // nms定义为_ext下的子模块
    PYBIND11_MODEL(TORCH_EXTENSION_NAME, m) {
        m.def("nms", &nms, "nms (CPU/CUDA)", py::arg("boxes"), py::arg("scores"),
              py::arg("iou_threshold"), py::arg("offset"));
    }

接下来便是实现具体的C++/CUDA算子,以下示例C++实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    #include <torch/extension.h>
    using namespace at; // ATen(A Tensor Library),为python扩展c++负责声明和定义Tensor运算的相关逻辑库
    Tensor nms_cpu(Tensor boxes, Tensor scores, float iou_threshold, int offset) {
        // 仅显示核心代码
        for int64_t _i = 0; _i < nboxes, _i++ {
                // 遍历所有检测框,称为主检测框
                if (select[_i] == false) continue;
                for (int64_t _j = _i + 1; _j < nboxes; _j++) {
                    // 对每个主检测框,遍历其他检测框,称为次检测框
                    // 这里只用遍历上三角元素即可,节省计算
                    if (select[_j] == false) continue;
                    auto ovr = inter / (iarea + area[j] - inter);
                    // 如果次检测框和主检测框iou过大,则去除次检测框
                    if (ovr >= iou_threshold) select[_j] = false;
                }
            }
        return order_t.masked_select(select_t);
    }
This post is licensed under CC BY 4.0 by the author.