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 = Falsedataloader
一般而言,需要自定义一个继承自 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属性依旧为None。create_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
modulemust have its parameters and buffers ondevice_ids[0]before running thisDataParallelmodule
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.trace与torch.jit.script
其中jit.trace方法只能追踪input_example作为my_model的输入时所进行的所有运算过程, 因此如果运算过程中存在分支或循环时, 不能保证得到的traced_model与原始的my_model完全一致, 而jit.script方法是通过分析my_model的代码来得到中间表示的,因此可以处理分支或者循环。但这并不代表jit.script的功能完全覆盖了jit.trace,参考。
jit.trace与jit.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) 的计算公式为:
torch.var(x) 的计算方式与 torch.std(x) 是一致的,即:
torch.nn.LayerNorm(附手动实现)
以二维数据为例,即 x 的形状为 (B, C),调用方法为:
更复杂的情形可以按如下方式手动实现 layer_norm
torch.nn.Module 源码剖析【TODO】
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_hookregister_module_forward_hookregister_module_backward_hookregister_module_full_backward_hook
而在 nn.Module 的代码中,存在如下与 hook 或 register 有关的函数
register_backward_hookregister_full_backward_hookregister_forward_pre_hookregister_forward_hook_register_state_dict_hook_register_load_state_dict_pre_hook
获取 hook
_get_backward_hooks_maybe_warn_non_full_backward_hook
__init__ 函数
__init__ 函数自定义模型都要继承自 nn.Module,并且在子类中一般都有如下操作
相关源码如下:
__call__
__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 中的 Parameterself._modules: 仅包含当前类内的 submodule 的 key-value, 不继续展开self._buffers: 仅包含当前类内的 buffer 不包含 _modules 中的 bufferself.add_module: 不在__getattr__中被调用state_dict: 递归包含全部的 parameters 和 buffers
理解这些东西的核心在于 __getattr__ 与 __setattr__:
在 nn.Module 中, __setattr__ 方法的执行大致逻辑【还是有些绕】为:
首先获取 self._parameters (备注: 具体的获取方式在实现上是 self.__dict__.get('_parameters'), 下同):
如果
value是nn.Parameter类型, 就将name从self.__dict__, self._buffers, self._modules, self._non_persistent_buffers_set中移除, 然后调用self.register_parameter(除去一些检查外实际执行的事情只有self._parameters[name]=value)如果
value是nn.Module类型, 就将name从self.__dict__, self._buffers, self._parameters, self._non_persistent_buffers_set中移除, 然后直接使用self._modules[name]=value如果
value是nn.Tensor类型, 且name在self._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 名字和值, 即递归各层级的 _paramtersself.named_parameters(prefix="", recurse=True): 即上一个, 仅返回值, 不返回 nameself.named_buffers(prefix="", recurse=True): 包含 buffer 名字和值, 即递归各层级的 _buffersself.buffers(recurse=True): 即上一个, 仅返回值, 不返回 nameself.named_modules(memo=set(), prefix='', remove_duplicate=True): 递归实现, 包含自身【tie weight的情况可能要另外用一个小节研究一下】self.modules(): 即上一个, 仅返回值, 不返回 nameself.named_children(): 即迭代返回 _modules, 注意: 只实现了remove_duplicate=Trueself.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:cudaxpucputypefloatdoublehalfbfloat16to_emptytoshare_memory
state_dict 相关
self.__setstate__load_state_dictstate_dict_save_to_state_dict
zero_grad、requires_grad、train、eval
zero_grad
requires_grad_
train
eval
__repr__ 相关
__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?
目前 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?