【Scala PyTorch深度学习】PyTorch On Scala系列课程 第十章 20 :高级神经网络Transformer【AI Infra 3.0】[PyTorch Scala硕士研一课程]
章节 6: 自定义扩展与互操作性
PyTorch 提供了丰富的工具集,但特定应用场景常常需要标准库中没有的操作或性能优化。 本章将介绍如何将 PyTorch 的功能扩展到其标准 Python 应用编程接口之外。
我们将构建自定义运算符,使用 C++ 和 CUDA 来应对对计算效率要求高或需要专用算法的场合。您将获得直接操作 PyTorch C++ 后端 (ATen) 的经验,学习管理 PyTorch 张量和 NumPy 数组之间的数据传输,并通过扩展 torch.nn.Module 和 torch.optim.Optimizer 来组织自定义网络组件和优化方法。此外,还将介绍使用外部函数接口 (FFI) 与现有 C 库进行交互的技术。学完本章,您将能够把自定义代码和外部库集成到您的 PyTorch 工作流程中。

PyTorch Scala 高校计算机硕士研一课程
构建定制C++扩展
虽然PyTorch的Python应用编程接口提供了极大的灵活性和易用性,但在某些情况下,使用C++能够带来显著优势。您可能会遇到Python开销成为限制因素的性能瓶颈,需要结合现有的高性能C++库,或者实现标准PyTorch运算符难以或无法高效表达的定制操作。提供了直接在C++中构建定制的PyTorch运算符的实践指导。
核心机制是,用C++编写您的运算符逻辑,如果需要自动微分,可能需要同时定义前向和后向传播,然后创建绑定,使Python可以调用此C++代码。PyTorch在内部使用功能强大的Pybind11库来处理这些Python-C++之间的调用。
为何使用C++扩展?
- 性能: 对于计算密集型操作,即使使用PyTorch优化过的张量操作,C++的执行速度也能够显著优于Python。消除紧密循环或复杂算法的Python解释器开销可以带来显著的速度提升。
- 集成: 如果您有现有的C++代码库或库执行与您的模型相关的特定计算,编写C++扩展提供了一种直接的方法将它们结合到您的PyTorch工作流程中,而无需用Python重新编写。
- 定制内核: 某些低级硬件优化或专门算法可能只能通过C++或CUDA(稍后介绍)有效实现。C++为此类任务提供必要的控制。
C++张量:torch::Tensor
在您的C++扩展代码中,您将主要使用torch::Tensor类,该类定义在ATen库(PyTorch的C++张量库)中。它是Python torch.Tensor的C++对应物。您可以使用通常与Python API相似的C++应用编程接口来访问其数据,查询其属性(如形状、数据类型、设备),并对其执行操作。
#include <torch/extension.h>
#include <vector>
// 接受并返回张量的C++函数签名示例
torch::Tensor custom_cpp_op(torch::Tensor input1, torch::Tensor input2) {
// 检查张量属性(示例)
TORCH_CHECK(input1.sizes() == input2.sizes(), "Input tensors must have the same shape");
TORCH_CHECK(input1.scalar_type() == torch::kFloat32, "Input tensors must be float32");
// 执行操作
torch::Tensor output = input1 + input2 * 2.0;
return output;
}
设置构建流程
为了使您的C++代码在Python中可用,您需要将其编译成Python可以导入的共享库。PyTorch在torch.utils.cpp_extension中提供工具来简化此流程,并与Python的标准setuptools良好集成。
您通常会创建一个setup.py文件:
# setup.py
from setuptools import setup
from torch.utils.cpp_extension import BuildExtension, CppExtension
setup(
name='my_custom_ops', # 您的Python包的名称
ext_modules=[
CppExtension(
'my_custom_ops._cpp', # 在Python中导入的模块名称
['custom_ops.cpp'] # 您的C++源文件列表
),
],
cmdclass={
'build_ext': BuildExtension
}
)
name:将要安装的包的名称。ext_modules:要构建的扩展列表。CppExtension用于纯C++扩展。- 第一个参数(
'my_custom_ops._cpp')定义了包含C++绑定的Python模块的完整名称。通常,C++部分会使用一个前导下划线。 - 第二个参数是C++源文件(
.cpp)的列表。
- 第一个参数(
cmdclass:指定使用PyTorch的定制BuildExtension类,该类负责查找PyTorch头文件/库并设置适当的编译器标志。
编写C++源文件
您的C++源文件(例如,custom_ops.cpp)需要包含必要的PyTorch头文件,并定义您希望公开的函数,以及Pybind11绑定。
// custom_ops.cpp
#include <torch/extension.h>
#include <vector>
// 定义您的定制操作逻辑
torch::Tensor custom_linear(torch::Tensor x, torch::Tensor weight, torch::Tensor bias) {
// 示例:基本类型和形状检查(根据需要添加更多检查)
TORCH_CHECK(x.dim() == 2, "Input x must be 2D");
TORCH_CHECK(weight.dim() == 2, "Input weight must be 2D");
TORCH_CHECK(bias.dim() == 1, "Input bias must be 1D");
TORCH_CHECK(x.size(1) == weight.size(1), "Input dimension mismatch: x.size(1) != weight.size(1)");
TORCH_CHECK(weight.size(0) == bias.size(0), "Output dimension mismatch: weight.size(0) != bias.size(0)");
// 执行线性操作:Y = X * W^T + b
// 注意:PyTorch的C++ API通常与Python相似,例如,matmul, add_
return torch::addmm(bias, x, weight.t());
}
// PYBIND11_MODULE是一个宏,用于创建Python模块的入口点。
// 第一个参数(TORCH_EXTENSION_NAME)是一个占位符,在编译时会被替换为
// setup.py中定义的模块名称('my_custom_ops._cpp')。
// 第二个参数(m)是模块对象。
PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {
m.def(
"linear", // Python函数名称
&custom_linear, // 指向C++函数的指针
"Custom Linear operation (C++)" // 可选的文档字符串
);
// 如果需要,可以在此处使用 m.def() 添加更多函数
}
- 头文件:
<torch/extension.h>是主要的头文件,包含了PyTorch (ATen) 和 Pybind11的必要部分。 - 函数定义: 编写操作
torch::Tensor对象的标准C++函数。使用PyTorch的C++应用编程接口进行张量操作(例如,torch::addmm,torch::mul,元素级运算符)。使用TORCH_CHECK宏进行输入验证。 - 绑定:
PYBIND11_MODULE块很重要。它定义了将在Python模块中可用的函数。TORCH_EXTENSION_NAME是一个特殊宏,由PyTorch构建过程自动定义,以匹配setup.py中指定的模块名称。m.def("python_name", &cpp_function_pointer, "docstring")将C++函数(cpp_function_pointer)映射到Python函数名(python_name)。
编译和使用扩展
要编译此扩展,请在您的终端中,进入包含setup.py和custom_ops.cpp的目录并运行:
python setup.py install
或者,为了方便开发,您可以使用python setup.py develop,它以可编辑模式安装包,允许您修改C++代码并重新编译,而无需重新安装。
成功编译后,您可以在Python中导入和使用您的定制函数,就像使用任何其他PyTorch函数一样:
import torch
import my_custom_ops._cpp as custom_ops # 导入编译后的C++模块
// 创建一些示例张量
val x = torch.randn(128, 768, requires_grad=true)
val weight = torch.randn(512, 768, requires_grad=true)
val bias = torch.randn(512, requires_grad=true)
// 使用定制C++函数
val output = custom_ops.linear(x, weight, bias)
println("输出形状:", output.shape)
// 示例:计算梯度(需要定义反向传播 - 见下文)
// output.sum().backward()
// println("梯度形状(权重):", weight.grad.shape)
与Autograd集成
上述简单示例只实现了前向传播。如果您需要PyTorch通过您的定制C++操作自动计算梯度,您必须定义相应的后向传播。这需要您在C++中创建一个定制的torch::autograd::Function子类,其思路与在Python中定义定制自动微分函数(在第1章中介绍)相似,但使用C++语法。您将在此C++类中实现静态的forward和backward方法。这是更高级的步骤,通常在您的C++操作是需要训练的大型网络的一部分时才需要。我们将在讨论定制CUDA扩展时提及这方面的内容,因为其原理类似。
构建C++扩展是优化代码关键部分或结合外部库的有力方法,能够提升PyTorch模型的性能上限。尽管这需要使用更底层的C++应用编程接口和构建系统,但torch.utils.cpp_extension工具显著简化了此流程。
构建自定义 CUDA 扩展
虽然自定义 C++ 扩展提供了将受 CPU 约束的逻辑或外部 C++ 库整合进去的途径,但深度学习中的性能瓶颈通常存在于 GPU 上。当标准 PyTorch 操作不足以满足需求,或者您需要以最高的 GPU 效率实现一种新颖的算法时,编写自定义 CUDA 内核就变得必须。学习如何创建、构建和直接将自定义 CUDA C++ 代码并入您的 PyTorch 工作流中。
CUDA 扩展的主要目的是性能。您可能有一个特定的数学运算、一个数据处理程序,或者一个来自现有 CUDA 研究代码的内核,希望直接在 GPU 上执行它们,而无需数据在 CPU 之间来回传输的开销,也无需依赖可能并非最优的标准 PyTorch 操作序列。
CUDA 扩展的工作流程
整合自定义 CUDA 代码涉及多个步骤,类似于 C++ 扩展,但增加了 GPU 编程的复杂性:
- 编写 CUDA 内核: 用 CUDA C++(
.cu文件)实现您的核心逻辑。这涉及编写函数(__global__用于从主机启动的内核,__device__用于从 GPU 调用的函数),这些函数在 GPU 内存中的数据上进行操作。 - 编写 C++ 包装器: 创建一个 C++ 函数(
.cpp文件),它作为 PyTorch 和您的 CUDA 内核之间的接口。该包装器将:- 接收 PyTorch 张量作为输入。
- 检查张量属性(设备、数据类型、连续性)。
- 获取原始数据指针(
tensor.data_ptr()),以便访问 GPU 内存。 - 计算 CUDA 内核启动参数(网格大小、块大小)。
- 启动 CUDA 内核,传递数据指针和任何其他所需的参数。
- 将生成的张量返回给 PyTorch。
- 创建 Python 绑定: 使用 PyTorch 的 C++ 扩展实用工具(
torch.utils.cpp_extension)编译 CUDA 内核和 C++ 包装器,并使包装函数可以在 Python 中调用。这可以通过即时 (JIT) 编译或通过setup.py脚本完成。
示例:自定义 CUDA 向量加法
我们用一个简单的向量加法内核来阐明这一点。
1. CUDA 内核 (vector_add_kernel.cu)
#include <cuda.h>
#include <cuda_runtime.h>
#include <stdio.h> // 如果内核调试需要 printf
// 用于元素级向量加法的 CUDA 内核
__global__ void vector_add_kernel(const float* a, const float* b, float* c, int n) {
int index = blockIdx.x * blockDim.x + threadIdx.x;
int stride = gridDim.x * blockDim.x;
for (int i = index; i < n; i += stride) {
c[i] = a[i] + b[i];
}
}
// 一个调用内核的简单函数(可以更复杂)
// 注意:为简洁起见省略了错误检查 (cudaGetLastError),但在生产环境中很重要。
void vector_add_cuda_launcher(const float* a, const float* b, float* c, int n) {
int threadsPerBlock = 256;
// 使用整数向上取整除法
int blocksPerGrid = (n + threadsPerBlock - 1) / threadsPerBlock;
// 启动内核
vector_add_kernel<<<blocksPerGrid, threadsPerBlock>>>(a, b, c, n);
// 可选:如果需要立即同步设备,在内核启动后同步
// cudaDeviceSynchronize(); // 请注意对性能的影响
}
2. C++ 包装器 (vector_add.cpp)
此文件连接 PyTorch 和我们的 CUDA 启动函数。
#include <torch/extension.h>
#include <vector>
// CUDA 前向声明(假设 vector_add_cuda_launcher 在其他地方定义,例如在 .cu 文件或头文件中)
void vector_add_cuda_launcher(const float* a, const float* b, float* c, int n);
// 符合 PyTorch C++ API 的 C++ 接口函数
// 注意:AT_ASSERT 宏确保张量在正确的设备上并具有预期的类型/形状。
torch::Tensor vector_add(torch::Tensor a, torch::Tensor b) {
// 输入验证
TORCH_CHECK(a.device().is_cuda(), "Input tensor a must be a CUDA tensor");
TORCH_CHECK(b.device().is_cuda(), "Input tensor b must be a CUDA tensor");
TORCH_CHECK(a.is_contiguous(), "Input tensor a must be contiguous");
TORCH_CHECK(b.is_contiguous(), "Input tensor b must be contiguous");
TORCH_CHECK(a.dtype() == torch::kFloat32, "Input tensor a must be float32");
TORCH_CHECK(b.dtype() == torch::kFloat32, "Input tensor b must be float32");
TORCH_CHECK(a.sizes() == b.sizes(), "Input tensors must have the same shape");
// 在与输入相同的设备上创建输出张量
torch::Tensor c = torch::empty_like(a);
int n = a.numel(); // 元素总数
// 调用 CUDA 启动函数
vector_add_cuda_launcher(
a.data_ptr<float>(),
b.data_ptr<float>(),
c.data_ptr<float>(),
n);
return c;
}
// 绑定函数:将 'vector_add' C++ 函数作为 'vector_add_cuda' 暴露给 Python
PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {
m.def("forward", &vector_add, "CUDA vector addition forward");
// 如果有反向传播,您也应在此处进行绑定。
}
3. Python 绑定和构建(使用 JIT)
对于简单情况,最方便的编译和加载方式是使用 torch.utils.cpp_extension.load。
import torch
import time
import torch.utils.cpp_extension as cpp_extension
// 加载 CUDA 扩展,如果需要则进行 JIT 编译
// 'verbose=True' 会显示编译命令
val vector_add_module = cpp_extension.load(
name='vector_add_cuda',
sources=['vector_add.cpp', 'vector_add_kernel.cu'],
verbose=True
)
// 在 GPU 上准备输入张量
val device = torch.device('cuda')
val size = 10000000 // 大向量大小
val a = torch.randn(size, device=device, dtype=torch.float32)
val b = torch.randn(size, device=device, dtype=torch.float32)
// --- 使用 PyTorch 默认加法 ---
val start_time = time.time()
val c_pytorch = a + b
torch.cuda.synchronize() // 等待 GPU 操作完成
val pytorch_time = time.time() - start_time
println(f"PyTorch default add time: {pytorch_time:.6f} seconds")
// --- 使用自定义 CUDA 扩展 ---
val start_time = time.time()
val c_cuda = vector_add_module.forward(a, b)
torch.cuda.synchronize() // 等待 GPU 操作完成
val cuda_time = time.time() - start_time
println(f"Custom CUDA add time: {cuda_time:.6f} seconds")
// 验证结果(允许小的浮点差异)
val diff = torch.abs(c_pytorch - c_cuda).max()
println(f"Maximum difference between PyTorch and CUDA results: {diff.item()}")
assert torch.allclose(c_pytorch, c_cuda, atol=1e-6), "Results differ significantly!"
println("CUDA extension test passed!")
本示例使用了 JIT 编译器。对于较大项目或分发,您通常会使用 setup.py 文件:
setup.py 示例
import setuptools.setup
import torch.utils.cpp_extension.{BuildExtension, CUDAExtension}
val setup(
name='vector_add_cuda',
ext_modules=[
CUDAExtension('vector_add_cuda', [ # 模块名称必须与 PYBIND11_MODULE 匹配
'vector_add.cpp',
'vector_add_kernel.cu',
]),
],
cmdclass={
'build_ext': BuildExtension
})
然后,您可以使用 python setup.py install 来构建它。安装后,您可以像常规 Python 模块一样导入它:import vector_add_cuda。
Python 脚本(torch.utils.cpp_extension.load 或 setup.py)NVCC(CUDA 编译器)调用C++ 编译器(例如 g++)调用共享对象文件(.so 或 .pyd)- 可由 Python 调用的模块加载/导入C++ 包装器(vector_add.cpp)- 张量检查- 获取 data_ptr()- 启动内核CUDA 内核(vector_add_kernel.cu)- global 函数- GPU 逻辑包含/链接编译链接编译链接
PyTorch CUDA 扩展的构建过程。Python 工具通过系统编译器(如 g++ 和 NVCC)协调 C++ 和 CUDA 代码的编译,生成可加载的共享库。
CUDA 扩展的注意事项
- 数据类型: 内核通常专门用于特定的数据类型(例如
float、half)。C++ 包装器必须处理类型检查,并可能分派到不同的内核版本或执行类型转换。在 C++ 和 CUDA 中使用模板有助于处理此问题。 - 张量连续性: CUDA 内核通常期望连续的内存块。
TORCH_CHECK(tensor.is_contiguous(), ...)断言很有用。如果张量不连续,您可能需要在 Python 代码中调用.contiguous(),或者在内核中谨慎处理非连续内存访问(这通常效率会低得多)。 - 设备管理: 确保张量在目标 CUDA 设备上(
tensor.device().is_cuda()),并且输出张量也在同一设备上创建。 - 内核启动参数: 选择最佳的
gridDim(块数)和blockDim(每块线程数)会显著影响性能,并取决于特定的 GPU 架构和内核逻辑。这通常需要进行实验。 - 同步: CUDA 内核启动是异步的。CPU 代码在启动内核后会继续执行。如果后续操作(在 CPU 或 GPU 上)依赖于内核的结果,则需要同步。
torch.cuda.synchronize()会等待当前流上所有先前的 CUDA 操作完成。但是,过度使用它会损害性能。通常,当数据复制回 CPU 或被另一个 PyTorch CUDA 操作使用时,同步是隐式处理的。 - 错误处理: 代码需要进行错误检查。在内核启动和其他 CUDA API 调用之后,在您的 C++/CUDA 代码中使用
cudaGetLastError()来捕获运行时错误。这些错误可以作为异常传播回 Python。 - 构建系统: 尽管 JIT 加载便于开发,但
setup.py为复杂构建、链接外部库和分发提供了更好的控制。 - 自动求导整合: 上述示例只实现了前向传播。为了使您的自定义 CUDA 操作可微分,您需要实现一个相应的
backward函数(通常需要另一个自定义 CUDA 内核),并使用torch::autograd::Function进行绑定,类似于第 1 章中讨论的自定义 C++ 自动求导函数,但由 CUDA 内核来处理计算。
构建自定义 CUDA 扩展需要熟悉 CUDA C++ 编程以及 PyTorch 的 C++ API。然而,它提供了对 GPU 执行的终极控制,为您的深度学习模型中专门的、计算密集型操作实现了显著的性能提升。
使用 ATen 库
PyTorch Python 接口的底层是 ATen,这是一个驱动其张量计算的 C++ 基本库。虽然 Python API 提供了便利性和灵活性,但在开发高性能定制 C++ 或 CUDA 扩展时,或者需要对 Python 中未完全公开的操作进行更细致的控制时,直接与 ATen 库交互就变得有必要。理解 ATen 有助于阐明 PyTorch 内部如何执行操作,并提供构建真正优化的底层组件的工具。
ATen 是 PyTorch 中主要的张量库。它在 C++ 中定义了 Tensor 对象,并实现了数百个作用于这些张量的数学操作。可以将其看作是为 torch.add、torch.matmul 或复杂神经网络层等函数执行实际数值工作的驱动力。你通常调用的 Python 函数通常作为封装器,最终调度到 ATen 的 C++ 实现。
为何与 ATen 交互?
在你的 C++ 代码中直接使用 ATen 函数具有以下优点:
- 性能: 对于计算密集型操作,特别是那些在 Python 中速度较慢的涉及循环或复杂索引模式的操作,使用 ATen 的 C++ 实现可以通过避免 Python 解释器开销来带来显著的加速。
- 访问底层操作: ATen 提供的函数和张量操作的范围比公共 Python API 更广。这对于实现新颖算法或高度优化的核函数可能很有用。
- 定制扩展的支撑: 在构建定制 C++ 或 CUDA 操作符时(如本章其他部分所述),你必然会用到 ATen 张量和函数。ATen 作为 PyTorch 体系与你的定制 C++/CUDA 代码之间的连接点。
ATen 的结构与调度
ATen 采用复杂的调度机制,将张量操作路由到合适的后端实现(CPU、CUDA,可能还有其他)。当你调用 at::add(tensor1, tensor2) 这样的操作时,ATen 会检查输入张量的属性,主要是它们的设备(CPU 或 CUDA)和数据类型(float、int 等)。根据这些属性,它会动态选择并执行正确的底层核函数。
Python: torch.add(a, b)ATen: at::add(t_a, t_b)ATen 调度器CPU 核函数(例如,Eigen/MKL)如果设备=CPUCUDA 核函数(例如,cuBLAS/cuDNN/定制)如果设备=CUDA
操作从 Python 经由 ATen 调度器到后端特定核函数的流程。
这种机制使得 PyTorch 能够在利用硬件特定优化的同时,保持一致的 API。在编写定制扩展时,你通常会为特定后端(如 CUDA 核函数)实现函数,并将它们注册到调度器,让 ATen 在合适的时候找到并使用你的定制代码。
在 C++ 中使用 ATen
要在 C++ 代码中使用 ATen,你主要需要包含 ATen 的主头文件:
#include <ATen/ATen.h>
这个头文件引入了张量和函数所需的定义。在 C++ 中,PyTorch 张量由 at::Tensor 类表示。你可以像在 Python 中一样创建和操作这些张量:
// 示例:在 C++ 中创建和使用 ATen 张量
// 在 CPU 上创建一个 2x3 的全一张量
at::Tensor tensor_a = at::ones({2, 3}, at::kFloat);
// 在 CPU 上创建一个 2x3 的随机数张量
at::Tensor tensor_b = at::randn({2, 3}, at::kFloat);
// 使用 ATen 函数执行加法
at::Tensor tensor_c = at::add(tensor_a, tensor_b);
// 执行元素级乘法
at::Tensor tensor_d = at::mul(tensor_c, 2.0); // 乘以一个标量
// 打印张量属性(需要 <iostream>)
std::cout << "张量 D:\n" << tensor_d << std::endl;
std::cout << "张量 D 数据类型: " << tensor_d.scalar_type() << std::endl;
std::cout << "张量 D 设备: " << tensor_d.device() << std::endl;
ATen 提供了与大多数 PyTorch 操作对应的函数,名称通常相似(例如,at::matmul、at::relu、at::sigmoid)。这些函数直接操作 at::Tensor 对象。
与 PyTorch C++ 扩展的集成
ATen 是 PyTorch C++ 扩展的支撑。当你使用 pybind11 定义一个要绑定到 Python 的 C++ 函数时(通常使用 torch/extension.h 完成),你在 C++ 中接收到的 torch::Tensor 参数本质上是 at::Tensor 的封装器。你可以直接将它们与 ATen 函数一起使用。
考虑一个用于定制扩展的简单 C++ 函数:
#include <torch/extension.h>
#include <ATen/ATen.h>
// 一个使用 ATen 的简单 C++ 函数
torch::Tensor scaled_add(torch::Tensor x, torch::Tensor y, float scale_factor) {
// x 和 y 是 torch::Tensor,但与 at:: 函数兼容
// 使用 ATen 函数执行操作
at::Tensor scaled_x = at::mul(x, scale_factor);
at::Tensor result = at::add(scaled_x, y);
return result; // 返回类型为 torch::Tensor
}
// 绑定代码(通常在单独的文件或代码块中)
PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {
m.def("scaled_add", &scaled_add, "执行缩放加法: scale_factor * x + y");
}
在这个例子中,x 和 y 是从 Python 接收的 torch::Tensor 对象。它们可以直接传递给 at::mul 和 at::add 等 ATen 函数。结果在内部是 at::Tensor,但作为 torch::Tensor 返回,PyTorch 会自动处理以供 Python 使用。torch::Tensor 作为面向用户的 C++ API 张量类型,与底层的 ATen 实现结合。
需注意的事项
- 命名空间: 你会经常遇到
at::和torch::这两种命名空间。at::专门指 ATen 库组件(底层张量操作)。torch::通常指更广泛的 PyTorch C++ API,包含自动微分功能、模块(torch::nn)以及torch::Tensor封装器本身。对于定制核函数中使用的基本张量操作,你将主要使用at::。 - 张量类型和设备: 在 C++ 中工作时,你需要明确指定张量数据类型(
at::ScalarType,例如at::kFloat、at::kHalf、at::kInt)和设备(at::Device,例如at::kCPU、at::kCUDA)。ATen 函数通常要求输入张量在同一设备上,并且可能具有特定的数据类型要求。你可以使用tensor.to(at::kCUDA)或tensor.to(at::kFloat)等方法检查和转换张量。 - 内存管理:
at::Tensor和torch::Tensor都使用引用计数进行内存管理,类似于 Python 张量。当张量在 Python 和 C++ 之间传递,或在 C++ 函数中使用时,它们的引用计数会自动管理。除非执行了显式复制操作,内存通常是共享的(而不是复制)。 - 自动微分: 需要注意的是,如上所示直接调用 ATen 函数通常不会在 PyTorch 计算图中记录操作。如果你的涉及 ATen 调用的定制 C++ 操作需要支持自动微分,你必须将其封装在
torch::autograd::Function中,定义定制的forward和backward方法。这在第 1 章(“PyTorch 内部机制与自动微分”)中有介绍,在将定制计算核函数集成到可训练模型中时不可或缺。ATen 提供了计算构件,而torch::autograd::Function则将其与梯度跟踪系统结合。
直接使用 ATen 赋予你强大的功能,用于性能优化和扩展 PyTorch 的主要功能。它是 PyTorch 张量计算发生的层面,在构建提升性能和能力上限的定制 C++ 和 CUDA 扩展时,理解它很有帮助。虽然它需要仔细处理类型、设备和自动微分系统,但掌握 ATen 的交互使用可以充分发挥 PyTorch 在高级深度学习工程中的全部潜能。
PyTorch 与 NumPy 的连接
NumPy 是 Python 科学计算堆栈的根基,它提供强大的 N 维数组对象和丰富的数学函数集。深度学习工作流程常涉及使用 NumPy 预处理数据、与基于 NumPy 构建的库集成,或使用基于 NumPy 的工具分析模型输出。因此,PyTorch 张量与 NumPy 数组之间的高效连接是高级使用者的一项常见需求。PyTorch 为这些转换提供直接机制,尽可能减少数据拷贝。
将 PyTorch 张量转换为 NumPy 数组
获取 PyTorch 张量的 NumPy 表示最直接的方法是调用张量上的 .numpy() 方法。
import torch
import numpy as np
// 创建一个 CPU 张量
val cpu_tensor = torch.randn(2, 3)
println(f"原始 PyTorch 张量 (CPU):\n{cpu_tensor}")
// 转换为 NumPy 数组
val numpy_array = cpu_tensor.numpy()
println(f"转换后的 NumPy 数组:\n{numpy_array}")
println(f"NumPy 数组类型: {type(numpy_array)}")
对于驻留在 CPU 上的张量,这种转换的一个重要方面是内存共享。生成的 NumPy 数组和原始 PyTorch 张量共享相同的底层内存缓冲区。这使得转换速度极快,因为无需复制数据。然而,这也意味着对其中一个对象的修改会反映到另一个对象上。
// 演示内存共享 (CPU)
println("正在修改 PyTorch 张量...")
cpu_tensor.add_(1) // 原地加法
println(f"修改后的 PyTorch 张量:\n{cpu_tensor}")
println(f"PyTorch 张量修改后的 NumPy 数组:\n{numpy_array}")
// 演示内存共享 (CPU)
println("\n正在修改 NumPy 数组...")
numpy_array[0, 0] = 99.0
println(f"修改后的 NumPy 数组:\n{numpy_array}")
println(f"NumPy 数组修改后的 PyTorch 张量:\n{cpu_tensor}")
这种内存共享行为高效但需要谨慎处理,以避免意外的副作用。
GPU 张量: NumPy 数组本质上是基于 CPU 的结构。因此,如果您的张量位于 GPU 上,您必须显式地使用 .cpu() 方法将其移动到 CPU,然后才能调用 .numpy()。此操作涉及从 GPU 到 CPU 内存的数据拷贝,并打破了在 CPU 张量中观察到的内存共享特性。
import torch
import numpy as np
if torch.cuda.is_available() then
// 创建一个 GPU 张量
val gpu_tensor = torch.randn(2, 3, device='cuda')
println(f"\n原始 PyTorch 张量 (GPU):\n{gpu_tensor}")
// 直接在 GPU 张量上调用 .numpy() 会引发错误
// numpy_array_gpu_fail = gpu_tensor.numpy() // 这将导致 TypeError
// 先移动到 CPU,然后转换
val cpu_tensor_copy = gpu_tensor.cpu()
val numpy_array_from_gpu = cpu_tensor_copy.numpy()
println(f"转换后的 NumPy 数组 (从 GPU 经由 CPU):\n{numpy_array_from_gpu}")
// 由于是拷贝,修改是独立的
println("\n正在修改从 GPU 张量派生出的 NumPy 数组...")
numpy_array_from_gpu[0, 0] = -50.0
println(f"修改后的 NumPy 数组:\n{numpy_array_from_gpu}")
println(f"原始 GPU 张量 (未改变):\n{gpu_tensor}")
println(f"中间 CPU 张量 (未受 NumPy 修改影响):\n{cpu_tensor_copy}")
else:
println("\nCUDA 不可用,跳过 GPU 张量转换示例。")
与 Autograd 的交互: .numpy() 转换要求张量不属于需要梯度计算的计算图,或者已明确分离。NumPy 操作不在 PyTorch 自动求导引擎的范围内。如果您尝试在 requires_grad=True 的张量上调用 .numpy(),PyTorch 将引发 RuntimeError。要执行此转换,您必须首先使用 .detach() 将张量从图中分离。
// 需要梯度的张量
val grad_tensor = torch.tensor([[1.0, 2.0], [3.0, 4.0]], requires_grad=True)
// 直接调用 .numpy() 将失败
// numpy_fail = grad_tensor.numpy() // 这将导致 RuntimeError
// 先分离,然后转换
val detached_tensor = grad_tensor.detach()
val numpy_from_grad = detached_tensor.numpy()
println(f"\n从分离张量获得的 NumPy 数组:\n{numpy_from_grad}")
// 原始张量仍需要梯度
println(f"原始张量 requires_grad: {grad_tensor.requires_grad}")
// 分离的张量不需要
println(f"分离的张量 requires_grad: {detached_tensor.requires_grad}")
请记住,分离会创建一个新张量,它共享相同的数据,但与梯度历史断开。
将 NumPy 数组转换为 PyTorch 张量
要从 NumPy 数组创建 PyTorch 张量,主要函数是 torch.from_numpy()。
// 创建一个 NumPy 数组
val numpy_array_orig = np.array([[1.5, 2.5], [3.5, 4.5]], dtype=np.float32)
println(f"\n原始 NumPy 数组:\n{numpy_array_orig}")
// 转换为 PyTorch 张量
val pytorch_tensor = torch.from_numpy(numpy_array_orig)
println(f"转换后的 PyTorch 张量:\n{pytorch_tensor}")
println(f"PyTorch 张量类型: {pytorch_tensor.dtype}")
与 CPU 张量的 .numpy() 转换类似,torch.from_numpy() 默认情况下与原始 NumPy 数组共享内存,前提是数组的数据类型与 PyTorch 兼容。这允许高效的数据交换。
// 演示内存共享 (NumPy -> PyTorch)
println("\n正在修改 NumPy 数组...")
numpy_array_orig[0, 0] = -10.0
println(f"修改后的 NumPy 数组:\n{numpy_array_orig}")
println(f"NumPy 数组修改后的 PyTorch 张量:\n{pytorch_tensor}")
// 演示内存共享 (NumPy -> PyTorch)
println("\n正在修改 PyTorch 张量...")
pytorch_tensor.add_(5) // 原地加法
println(f"修改后的 PyTorch 张量:\n{pytorch_tensor}")
println(f"PyTorch 张量修改后的 NumPy 数组:\n{numpy_array_orig}")
数据类型注意事项: PyTorch 从 NumPy 数组的 dtype 推断张量的 dtype。常见的类型如 np.float32、np.float64、np.int32、np.int64 和 np.uint8 直接映射到其 PyTorch 等效类型(torch.float32、torch.float64 等)。请注意,NumPy 中的默认浮点类型通常是 np.float64,而 PyTorch 通常默认为 torch.float32。如果您在 PyTorch 中需要特定的 dtype(例如用于模型输入的 torch.float32),请确保您的 NumPy 数组在转换前具有相应的类型 (np.float32),或者使用 .to(torch.float32) 转换结果张量。
// float64 示例
val numpy_float64 = np.array([1.0, 2.0, 3.0]) // 默认 np.float64
val tensor_float64 = torch.from_numpy(numpy_float64)
println(f"\n从 np.float64 数组得到的张量数据类型: {tensor_float64.dtype}") // torch.float64
// 如果需要,转换为 float32
val tensor_float32 = tensor_float64.to(torch.float32)
println(f"转换为 float32 后的张量: {tensor_float32.dtype}") // torch.float32
创建副本: 如果您明确需要 PyTorch 张量是 NumPy 数据的副本,而不是共享内存,您可以使用 torch.tensor() 或 torch.as_tensor() 并传入适当的参数。torch.tensor() 总是拷贝数据。
// 使用 torch.tensor() 创建一个拷贝
val tensor_copy = torch.tensor(numpy_array_to_copy)
println(f"\n使用 torch.tensor() 创建的张量副本:\n{tensor_copy}")
// 使用 torch.tensor() 创建一个拷贝
val tensor_copy = torch.tensor(numpy_array_to_copy)
println(f"\n使用 torch.tensor() 创建的张量副本:\n{tensor_copy}")
// 修改原始 NumPy 数组
numpy_array_to_copy[0] = 500
println(f"修改后的 NumPy 数组: {numpy_array_to_copy}")
println(f"PyTorch 张量副本 (未受影响): {tensor_copy}")
当您希望修改张量而不影响原始 NumPy 数组(反之亦然),或者当 NumPy 数组的数据类型不受直接支持并在张量创建时需要转换时,拷贝是必要的。
移动到 GPU: 使用 torch.from_numpy() 从 NumPy 数组创建张量后,您可以轻松地使用 .to() 方法将其移动到 GPU。此操作必然涉及从 CPU 到 GPU 内存的数据拷贝。
// 从 NumPy 数组创建张量
val numpy_array_for_gpu = np.random.rand(3, 4).astype(np.float32)
val tensor_on_cpu = torch.from_numpy(numpy_array_for_gpu)
println(f"\n从 NumPy 数组创建的张量:\n{tensor_on_cpu}")
println(f"张量设备: {tensor_on_cpu.device}")
// 如果 CUDA 可用,将张量移动到 GPU
if torch.cuda.is_available():
val tensor_on_gpu = tensor_on_cpu.to('cuda')
println(f"\n张量已移动到 GPU:\n{tensor_on_gpu}")
println(f"张量设备: {tensor_on_gpu.device}")
else:
println("\nCUDA 不可用,跳过移动到 GPU 示例。")
性能和最佳实践
内存共享机制使得 CPU PyTorch 张量与 NumPy 数组之间的转换速度极快(零拷贝)。这非常适合数据不需要修改,或者修改应反映在两个对象中的情况。
然而,请记住以下几点:
- GPU 数据传输: 在 CPU 和 GPU 之间移动数据(
.cpu(),.to('cuda'))总是涉及拷贝并产生开销。尽量减少这些传输。如果需要大量计算,请尝试完全在目标设备上的 PyTorch 中进行。 - 显式拷贝: 创建拷贝(
torch.tensor()、.clone()、np.copy())所需时间与数据大小成比例。当需要内存独立性时,请有意识地使用拷贝。 - 数据加载: 如果初始数据集已经是 NumPy 格式(例如从磁盘加载),
torch.from_numpy()对于加载它们非常有效。 - 后处理/可视化: 当您需要使用 NumPy 或 Matplotlib/Scikit-learn 等库分析模型输出或中间激活时,使用
.detach().cpu().numpy()。 - 注意共享内存: 始终注意张量和 NumPy 数组是否共享内存。通过一个对象修改,意外影响到另一个对象可能导致难以追踪的隐蔽错误。如有疑问,请创建拷贝。
熟练掌握 PyTorch 与 NumPy 之间的高效连接对于构建实用的深度学习流程很重要。理解内存共享、数据拷贝、设备放置以及与自动求导系统的交互等细节,能让您编写出高效且正确的代码,发挥两个库的优点。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)