正文
时间紧任务重有延毕风险
想提前完成大小论文为之后读博或工作做准备的
想通过发表sci论文、提升科研能力bao研、考研的本科同学
大家感兴趣可以直接添加小助手微信:ai0808q
通过后回复“咨询”既可。
第6期课程大纲
操作(Operations)
张量是如何进行操作的?
现在我们已经有了数据集,接下来看看这些张量是如何进行操作的。
从高层来看,PyTorch 提供了像 torch.mm 这样的操作来进行矩阵乘法。
虽然它看起来像是一个单一的函数,但实际上,根据设备(CPU、CUDA 等)和数据类型(fp32、int 等)的不同,它有多种不同的实现方式。
这种分离也是为什么通常不能对位于不同设备上的张量进行操作的原因。
这样做会导致运行时错误,因为不同设备上的张量之间的操作是不受支持的。
仅从 Python 代码来看,大家可能无法看到这种选择所带来的复杂性和性能影响。
实际上PyTorch 是用 C++ 实现的,在 C++ 中调用函数主要有两种方式:静态调用和动态调用。
在静态调用中,函数在编译时就已经确定,这使得调用速度很快,但要求 PyTorch 做出一些它并不适合做出的假设(例如设备类型、张量类型、数据类型等)。
换句话说,PyTorch 只有在运行时才拥有确定调用哪个函数所需的所有信息,因此,PyTorch 依赖于动态分发(dynamic dispatch)。
然而,动态分发也有其成本,虽然如今动态库的额外存储开销很少成为问题,但管理向后兼容性却是一个挑战。
在运行旧模型时,找到正确的依赖项组合是一件非常痛苦的事。
这意味着在设置 PyTorch 环境时,你需要非常小心地列出所有可能的依赖项——仅依赖默认的 pip wheel 可能会导致版本不匹配和难以捉摸的错误。
一旦 PyTorch 根据设备和数据类型确定了要运行的操作,它就会将任务委托给一个底层内核——通常是用 C++ 编写的。
下面我们来看看这些内核是如何构建的,以及如何编写自己的内核。
编写内核(Writing Kernels)
我们之前讨论的分发最终会连接到为 PyTorch 手动编写的底层内核,这些内核非常强大(可以查看这个文件夹中针对每种计算类型的内核)。https://github.com/pytorch/pytorch/tree/main/aten/src/ATen/native
如果你想编写 PyTorch 内核,有一些工具可以帮助你轻松实现。
首先当你填写正确的模式(模式代码在此定义
时
https://github.com/pytorch/pytorch/blob/main/aten/src/ATen/native/native_functions.yaml),
将你的内核连接到高层 PyTorch 函数的包装代码会自动为你生成,这允许你立即将你的实现连接到相应的高层 PyTorch 操作。
其次PyTorch 提供了宏来处理常见行为,例如底层的 TORCH_CHECK 宏对于编写更好的调试消息和防止因无效内存访问而导致的崩溃非常有用。
TORCH_CHECK 本身可以看作是一个抽象的 if 和 throw 语句,并附带一个自定义消息。
最后要介绍的是 TensorAccessor 类,这个内核中的底层类会传入数据、维度和数据类型。
它能够正确处理步长,使得使用起来比直接使用原始指针更加方便。
即使底层内存不是连续的,它也能正确处理步长。
自动微分(Autograd)
本文介绍的最后一个特性是张量的自动微分(autograd)。
这或许是 PyTorch 对于机器学习库来说最重要的特性,自动微分(Automatic Gradients 的缩写)让程序员无需手动指定如何通过模型计算导数。