Pytorch

pytorch官方文档:

tutorial部分/doc的notes部分

doc的python api部分用于查接口(部分内容可以作为代码框架的参考)

doc的Language Binding(C++部分/Java部分)

libraries部分包含torchvision等

community部分没探索过

复现性

import torch
import random
import numpy as np
torch.manual_seed(0)
random.seed(0)
np.random.seed(0)
torch.use_deterministic_algorithms(True)
torch.backends.cudnn.benchmark = False

dataloader

官方文档

一般而言,需要自定义一个继承自 torch.utils.data.Dataset 的类,该类必须实现 __len____getitem__ 方法。dataset 参数为该自定义类的实例。

关键代码如下(均不是完整代码,仅包含关键部分)

总结一下:

入参主要分为如下几块:

  • 下标索引迭代器相关:sampler, shuffle, batch_size, drop_last, batch_sampler

    • loader.sampler 作为迭代器时(必定有),每次 next 返回的应该是一个下标。而 loader.batch_sampler 作为迭代器时(loader.batch_sampler可能本身是None),每次 next 返回的应该是一个 batch 的下标列表。

  • 数据后处理(作用主要是用来整合一个batch内或单条数据):collate_fn

    • collate_fn 是一个 Callable

  • 其他:num_workers, pin_memory, timeout, worker_init_fn, prefetch_factor, persistent_workers

例子:

AutoGrad、计算图、自定义算子

自定义算子

参考:pytorch 官方文档

例子1:自己实现二维卷积

梯度惩罚

参考 amp

此处直接调用了 torch.autograd.grad 得到需要计算的梯度,并使用了 create_graph=True,这与普通的 Tensor.backward 的差异在于:

  • grad_params 即为求得的所有参数的梯度,但此时所有参数的 grad 属性依旧为 Nonecreate_grad=True 表示为计算梯度所需的算子也将建立计算图

  • loss.backward() 的作用是求得所有参数的梯度,并更新至它们的 grad 属性上,并且销毁整个计算图

从源码上看

因此在本质上,Tensor.backward 实际上就是 torch.autograd.backward,它与 torch.autograd.grad 都是调用 Variable._execution_engine.run_backward 函数,只不过前者调用方式为:

后者的调用方式为

torch.nn.Module

finetune

待补充

load and save

注记:不推荐使用如下方式保存模型,会出现无法 load 的情况,具体原因尚不明确。参见问答

optimizer

使用范例如下:(引用自官方文档),注意官方推荐 zero_grad()->forward->loss.backward()->optimizer.step() 的方式进行参数更新, 并且 scheduler.step 放在整个 epoch 的最后进行更新

torch.optim

如果模型的各个参数需要使用不同的学习率, 则可以使用如下两种方式

optimizer.zero_grad 的实际执行逻辑是对所有的 paramter 进行:parameter.grad=None

torch.optim.lr_scheduler

使用自定义的学习率调整策略可以使用 LambdaLR

对不同的参数设置了不同的学习率及学习率调整策略时, get_lr 会返回不同参数组的当前学习率

梯度剪裁

梯度剪裁的用法例子如下

自动混合精度训练 (Automatic Mixed Precision)

参见 https://buxianchen.github.io/drafts/2023-11-04-fp16-training.html

cuda and distributed

入门

环境变量

设定使用的 GPU,PCI_BUS_ID 是表示 0,1 代表的是物理 GPU ID 为 0,1 的两块 GPU。参考

简介

参考官方文档

术语释义

参考官方文档

TensorFloat-32(TF32) on Ampere devices

pytorch 1.7 之后,可以通过设置这两个参数为 True 提升 32 位浮点运算的速度,默认值即为 True。设置为 True 之后,运算精度会变低许多,但速度会快很多

Asynchronous execution

CUDA streams

CUDA streams 是 Nvidia CUDA C 官方文档中所用的术语,一个 CUDA stream 表示的是一串在 GPU 上执行的命令。这一串命令将保证按次序执行,但用户可以创建多个 CUDA streams,不同的 CUDA stream 中的指令是并发执行的。在 Pytorch 中,每块 GPU 都有一个默认的 CUDA stream,其特别之处是会在必要的时候自动做同步。但用户也可以自己创建新的 CUDA stream,并将命令放在创建的 CUDA stream 中执行。此时,必要的同步操作需要用户自己做,例如下面的程序没有做好同步,因此计算出的 B 是错误的。

一般可以使用 torch.cuda.synchronize()torch.cuda.Stream.synchronize()torch.cuda.Stream.wait_stream(stream) 等方法进行同步。完整 API 参见官方文档

torch.nn.DataParallel

使用

例子 1

从代码上看,实际上只需要增加一行即可

例子 2

原理

torch 1.9.0 版本关于 DataParallel 的函数原型为:

其中 dim 表示输入的数据将会在这一维度被平均分配到各个 GPU 中。

The parallelized module must have its parameters and buffers on device_ids[0] before running this DataParallel module

torch.nn.parallel.DistributedDataParallel

参见 https://buxianchen.github.io/drafts/2023-11-03-pytorch-ddp.html

TorchElastic

TorchElastic 原本是一个独立的包,但 Pytorch 1.9.0 版本将 TorchElastic 进行了集成,位于 torch.distributed.elastic 子模块下。参见上一节关于 torch/distributed/run.py 的介绍。

RPC

DataParallel、DistributedDataParallel、TorchElastic 均属于 DataPallel,RPC 为模型并行

c10d

c10d 是两大类并行方法的共同底层依赖(通信机制), 这里只介绍 torch.distributed 的一些函数

模型量化

待补充

torchscript

大致理解: torchscript将python脚本里写的模型(即自定义的torch.nn.Module子类)转换为一个中间表示,将这个中间表示保存后,可以使用其他语言或者环境对中间表示进行解析。

主要的方法有两个:torch.jit.tracetorch.jit.script

其中jit.trace方法只能追踪input_example作为my_model的输入时所进行的所有运算过程, 因此如果运算过程中存在分支或循环时, 不能保证得到的traced_model与原始的my_model完全一致, 而jit.script方法是通过分析my_model的代码来得到中间表示的,因此可以处理分支或者循环。但这并不代表jit.script的功能完全覆盖了jit.trace参考

jit.tracejit.script嵌套,并将模型保存

C++ Fronted API

  • 一种方案是使用C++ API进行完整的训练与模型保存(torchscript中定义的格式)

  • 另一种方案是使用Python训练并保存(必须使用torchscript定义的格式保存), 然后使用C++ API进行导入

Pytorch Internal

out=xxx

参考 stackoverflow 问答

inplace=True

pytorch 中某些函数允许使用 inplace=True,但前提条件是这个 tensor 在反向求导时是不需要的:

例如,对于 relu 函数,y=relu(x)y 对于 x 的局部导数可以直接通过 dy_dx=(y>0) 得到,而无需知道 x 的值。因此可以使用:

另一个例子

准确理解这个问题跟计算图相关,尤其是 backward 与 forward 之间的关系。参考关于自定义算子的官方文档

detach

注意,使用 detach 后,返回的新张量与原来的张量共享内存。详情参考官方文档,摘录如下

Returned Tensor shares the same storage with the original one. In-place modifications on either of them will be seen, and may trigger errors in correctness checks. IMPORTANT NOTE: Previously, in-place size / stride / storage changes (such as resize_ / resize_as_ / set_ / transpose_) to the returned tensor also update the original tensor. Now, these in-place changes will not update the original tensor anymore, and will instead trigger an error. For sparse tensors: In-place indices / values changes (such as zero_ / copy_ / add_) to the returned tensor will not update the original tensor anymore, and will instead trigger an error.

non-blocking 参数

参考问答问答pytorch官方例程

小技巧

共享参数

某些文本生成模型会共享embedding层与输出层的参数, huggingface transformers 将这一操作成为 tie weight

常用函数

cuda 检查相关

torch.nn.Module的默认模式为train, 但为了保险起见, 请手动用model.train()model.eval()进行切换.

torch.nn.Dropout(p)的行为:

  • 测试阶段: 直接将输入原封不动地输出

  • 训练阶段: 以p的概率将输入的分量置为0, 其余分量变为1/(1-p)

torch.nn.functional.normalize

默认对第 1 维进行归一化,例如:

torch.Tensor.type_as 函数

易错记录

clone、detach

torch.std 与 torch.var

这两个函数均计算的是无偏估计,以一维数据为例,即 x 的形状为 (d,)torch.std(x) 的计算公式为:

x.std(x)=torch.std(x)=i=1d(xixˉi)2d1x.std(x)=torch.std(x)=\sqrt\frac{\sum_{i=1}^{d}(x_i-\bar{x}_i)^2}{d-1}

torch.var(x) 的计算方式与 torch.std(x) 是一致的,即:

x.var(x)=torch.var(x)=i=1d(xixˉi)2d1x.var(x)=torch.var(x)=\frac{\sum_{i=1}^{d}(x_i-\bar{x}_i)^2}{d-1}

torch.nn.LayerNorm(附手动实现)

以二维数据为例,即 x 的形状为 (B, C),调用方法为:

更复杂的情形可以按如下方式手动实现 layer_norm

torch.nn.Module 源码剖析【TODO】

  • 版本:torch 1.9.0 (以下目的是按类别穷举 nn.Module 的方法)

  • 相关代码:torch/nn/modules/module.py

takeaway

buffer 与 parameter

pytorch 问答 里有关于这个的内容

原则时: 为保持干净的代码结构, buffer 中存的是不需要求导的 tensor

hook

可以使用如下方式使得每次调用 nn.Module 的 forward 函数时,都将输入打印出来。

这种影响全局的注册 hook 的方法有如下几个:

  • register_module_forward_pre_hook

  • register_module_forward_hook

  • register_module_backward_hook

  • register_module_full_backward_hook

而在 nn.Module 的代码中,存在如下与 hook 或 register 有关的函数

  • register_backward_hook

  • register_full_backward_hook

  • register_forward_pre_hook

  • register_forward_hook

  • _register_state_dict_hook

  • _register_load_state_dict_pre_hook

获取 hook

  • _get_backward_hooks

  • _maybe_warn_non_full_backward_hook

__init__ 函数

自定义模型都要继承自 nn.Module,并且在子类中一般都有如下操作

相关源码如下:

__call__

源码如下:

parameters、buffer、modules、children

相关的 API 有如下:

  • self.__setattr__(name: str, value: Union[Parameter, Module, Tensor, Any])

  • self.__getattr__(name: str): 由于确保了 _buffers, _parameters, _modules__dict__ 不产生冲突, 所以实现很直白, 依次按 name 查找上述四个部分即可

  • self.__delattr__

  • self.register_parameter: 在 __getattr__ 中被调用

  • self.register_buffer: 不在 __getattr__ 中被调用

  • self._parameters: 仅包含当前类内的 Parameter 不包含 _modules 中的 Parameter

  • self._modules: 仅包含当前类内的 submodule 的 key-value, 不继续展开

  • self._buffers: 仅包含当前类内的 buffer 不包含 _modules 中的 buffer

  • self.add_module: 不在 __getattr__ 中被调用

  • state_dict: 递归包含全部的 parameters 和 buffers

理解这些东西的核心在于 __getattr____setattr__

nn.Module 中, __setattr__ 方法的执行大致逻辑【还是有些绕】为:

首先获取 self._parameters (备注: 具体的获取方式在实现上是 self.__dict__.get('_parameters'), 下同):

  • 如果 valuenn.Parameter 类型, 就将 nameself.__dict__, self._buffers, self._modules, self._non_persistent_buffers_set 中移除, 然后调用 self.register_parameter(除去一些检查外实际执行的事情只有 self._parameters[name]=value)

  • 如果 valuenn.Module 类型, 就将 nameself.__dict__, self._buffers, self._parameters, self._non_persistent_buffers_set 中移除, 然后直接使用 self._modules[name]=value

  • 如果 valuenn.Tensor 类型, 且 nameself._buffer 中, 则执行 self._buffer[name]=value, 否则执行 object.__setattr__(self, name, value)

__getattr__ 方式的执行逻辑是依次从 _parameters, _modules, _buffers 中进行查找, 否则报错(备注,在这三者搜索之前会用默认的 __getattribute__ 进行搜索)

总之最终的结果如下:

以下的例子用于加深理解

以下均为生成器(这里的描述包含了完整参数)

  • self._named_members(get_members_fn, prefix='', recurse=True): 下面四个方法的辅助方法, 使用了 self.named_modules 来实现

  • self.parameters(recurse=True): 包含 Parameter 名字和值, 即递归各层级的 _paramters

  • self.named_parameters(prefix="", recurse=True): 即上一个, 仅返回值, 不返回 name

  • self.named_buffers(prefix="", recurse=True): 包含 buffer 名字和值, 即递归各层级的 _buffers

  • self.buffers(recurse=True): 即上一个, 仅返回值, 不返回 name

  • self.named_modules(memo=set(), prefix='', remove_duplicate=True): 递归实现, 包含自身【tie weight的情况可能要另外用一个小节研究一下】

  • self.modules(): 即上一个, 仅返回值, 不返回 name

  • self.named_children(): 即迭代返回 _modules, 注意: 只实现了remove_duplicate=True

  • self.children(): 即上一个, 仅返回值, 不返回 name

使用 name 获取信息:

  • self.get_submodule: 仅在后面两个函数里发挥作用

  • self.get_parameter: 仅对外, 在其他地方不使用

  • self.get_buffer: 仅对外, 在其他地方不使用

apply、_apply、cuda、float、half ...

  • self.apply(fn: Callable[['Module'], None]): 常用于初始化参数, 对外接口。注意 apply 没有调用 _apply

  • _apply:

    • cuda

    • xpu

    • cpu

    • type

    • float

    • double

    • half

    • bfloat16

    • to_empty

    • to

    • share_memory

state_dict 相关

  • self.__setstate__

  • load_state_dict

  • state_dict

  • _save_to_state_dict

zero_grad、requires_grad、train、eval

  • zero_grad

  • requires_grad_

  • train

  • eval

__repr__ 相关

  • _get_name()

  • extra_repr()

  • __repr__

其他

  • __dir__

  • _replicate_for_data_parallel

Docker

devel 与 runtime 的主要区别在于后者没有 nvcc

AllReduce

reduce的含义是规约,即多个数据规约为一个数据,例如求和,求平均,求最大值等。而allreduce指的是将多个机器上的数据跨机器做规约,并且最终的规约值要放在每个机器上各一份。典型的例子是对多个机器求向量和,即:

目标:

Naive

步骤如下:

  • 机器1首先接受来自其他机器的所有数字并计算最终结果

  • 将计算结果从机器1分发至其余机器

分析:

  • 通信成本

  • 计算成本

  • 时间成本

    24个数据通信时间+12次加法计算时间

Ring AllReduce

步骤如下

  • Scatter-reduce

  • Allgather

    类似于第一步,但接收到数据后不做运算,只进行覆盖更新

分析

  • 通信成本

  • 计算成本

  • 时间成本

    假定所有机器处理速度完全相同时,花费时间为:12个数据通信时间,3次加法时间,3次覆盖时间

Tricks and Discusions

为什么 torchvision 使用 PIL 而非 CV2?

可参考stackoverflow问答

目前 torchvision 也可以选择图像处理的后端,按官方说法:accimage 一般比 PIL 要快一些,但支持的操作不完善

pytorch 与 cuda/cudnn 的对应

pytorch 论坛 的解答中有如下解释

Yes, you just need to install the NVIDIA drivers and the binaries will come with the other libs. If you want to build from source, you would need to install CUDA, cuDNN etc.

如果采用二进制的形式安装(pip install 属于这一类),那么只需要事先安装好显卡驱动即可,安装包里已经内置了 CUDA 与 cuDNN(但并非完整,属于阉割版的cudatoolkit)。这也可能解释了为什么 pytorch 的官方 Docker 镜像例如 pytorch/pytorch:1.9.0-cuda10.2-cudnn7-runtime 标签名写的是 cudnn 7 而实际上包含的 cudnn_version.h 里显示的是 8.2.1 版本。

Last updated

Was this helpful?