AI

深度学习计算

动手深度学习第五章

Posted by LXG on March 19, 2026

深度学习计算

深度学习 VS 半导体

深度学习的发展路径,和半导体/计算机工业几乎是“同构”的, 两者都是从“底层物理” → “抽象封装” → “工程化体系”

半导体/计算机 深度学习
晶体管 单个神经元
逻辑门 激活函数 + 基本运算
标准电路(加法器等) Layer(Conv / FC)
模块化设计(IP核) Block(ResNet / Transformer)
编程语言(C / Python) PyTorch / TensorFlow
操作系统 深度学习框架 + 运行时
芯片制造工艺 GPU / AI加速器

层和块



import torch
from torch import nn
from torch.nn import functional as F

# 概念补充:
# 1) 层(layer):神经网络中的基本计算单元,例如 Linear、ReLU。
#    每一层接收输入并产生输出,完成一次特定变换。
# 2) 块(block):由一个或多个“层”按一定顺序组合成的更大功能模块。
#    例如可以把“Linear + ReLU”看成一个简单块;多个块再拼成完整模型。
# 3) 在 PyTorch 中,nn.Sequential 常用于把若干层/块按前向顺序串起来。

# 使用 nn.Sequential 按顺序堆叠网络层:
# 1) 输入层到隐藏层:20 -> 256
# 2) 激活函数:ReLU
# 3) 隐藏层到输出层:256 -> 10
# 这里每个 nn.Linear 或 nn.ReLU 都是“层”;
# 这 3 层组合在一起可以看作一个简单的前馈“块”。
net = nn.Sequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10))

# 构造一批随机输入数据:batch_size=2,每个样本有 20 个特征
X = torch.rand(size=(2, 20))

# 前向传播:将输入 X 送入网络,得到预测输出 Y
# 数据会按顺序依次流经这个块中的每一层。
Y = net(X)

# 打印输出张量(形状为 2x10,对应 2 个样本各自的 10 维输出)
print(Y)

自定义块


class MLP(nn.Module):
    # 用模型参数声明层。这里,我们声明两个全连接的层
    def __init__(self):
        # 调用MLP的父类Module的构造函数来执行必要的初始化。
        # 这样,在类实例化时也可以指定其他函数参数,例如模型参数params(稍后将介绍)
        super().__init__()
        self.hidden = nn.Linear(20, 256)  # 隐藏层
        self.out = nn.Linear(256, 10)  # 输出层

    # 定义模型的前向传播,即如何根据输入X返回所需的模型输出
    def forward(self, X):
        # 注意,这里我们使用ReLU的函数版本,其在nn.functional模块中定义。
        return self.out(F.relu(self.hidden(X)))

net = MLP()
Y = net(X)
print(Y)

nn.Module

nn.Module 是 PyTorch 里所有模型、层、Block 的基类

函数 用途
forward 定义计算
parameters 获取参数
train / eval 模式切换
to / cuda 设备迁移
state_dict 保存模型
load_state_dict 加载模型
apply 初始化
named_parameters 精细控制
modules 遍历结构
hook 调试/可视化

顺序块


class MySequential(nn.Module):
    def __init__(self, *args):
        super().__init__()
        # args 里传进来的是一堆“层”(比如 Linear、ReLU 等)
        # 我们把这些层一个个取出来,按顺序保存起来
        for idx, module in enumerate(args):
            # module 是一个层(nn.Module 的子类实例)
            # self._modules 是 PyTorch 内部用来“注册子模块”的地方(类似一个字典)
            # 只有放到这里,PyTorch 才能自动管理参数、支持GPU、保存模型等
            self._modules[str(idx)] = module

    def forward(self, X):
        # 按照添加的顺序,把输入数据 X 依次传给每一层
        # 就像流水线一样:上一层输出 → 下一层输入
        for block in self._modules.values():
            X = block(X)  # 等价于:X = block.forward(X)
        return X  # 最终输出
    
net = MySequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10))
Y = net(X)
print(Y)

维度 MLP MySequential
本质 Block(功能模块) Container(容器)
是否有逻辑 ✅ 有 ❌ 没有
灵活性 ⭐⭐⭐⭐ ⭐⭐
结构 可任意设计 只能顺序
使用场景 复杂模型 简单堆叠

在前向传播函数中执行代码


class FixedHiddenMLP(nn.Module):
    def __init__(self):
        super().__init__()

        # ===============================
        # 固定权重(不会参与训练)
        # ===============================
        # torch.rand((20, 20)):生成一个 20x20 的随机矩阵
        # requires_grad=False:表示这个张量不参与反向传播(不会被优化器更新)
        # 👉 作用:
        #   - 相当于一个“常量权重”
        #   - 可以理解为:固定的变换矩阵 / 规则矩阵 / 查表
        # 👉 注意:
        #   - 不会出现在 model.parameters() 中
        #   - optimizer 也不会更新它
        self.rand_weight = torch.rand((20, 20), requires_grad=False)

        # ===============================
        # 可训练的全连接层
        # ===============================
        # nn.Linear(20, 20):
        #   输入维度 20 → 输出维度 20
        # 👉 内部包含:
        #   - weight(20x20)
        #   - bias(20)
        # 👉 这些参数是可训练的(requires_grad=True)
        self.linear = nn.Linear(20, 20)

    def forward(self, X):
        # ===============================
        # Step 1:第一次线性变换
        # ===============================
        # X shape: (batch_size, 20)
        # 输出: (batch_size, 20)
        # 👉 相当于:
        #   X = X @ W + b
        X = self.linear(X)

        # ===============================
        # Step 2:自定义计算(非标准层)
        # ===============================
        # torch.mm(X, self.rand_weight):
        #   矩阵乘法(batch_size,20)×(20,20)→(batch_size,20)
        #
        # +1:
        #   给每个元素加常数偏置
        #
        # F.relu(...):
        #   ReLU激活函数(负数变0)
        #
        # 👉 注意:
        #   - 这里没有使用 nn.Linear
        #   - 完全手写计算过程(更底层)
        # 👉 本质:
        #   X = ReLU(X @ 固定矩阵 + 常数)
        X = F.relu(torch.mm(X, self.rand_weight) + 1)

        # ===============================
        # Step 3:参数共享(重点)
        # ===============================
        # 再次使用同一个 linear 层
        #
        # 👉 说明:
        #   - 这里不是新的一层
        #   - 而是复用同一组参数(weight 和 bias)
        #
        # 👉 等价于:
        #   X = W(WX + b) + b
        #
        # 👉 应用场景:
        #   - RNN(循环使用同一权重)
        #   - 模型压缩(减少参数量)
        X = self.linear(X)

        # ===============================
        # Step 4:动态控制流(重点)
        # ===============================
        # X.abs().sum():
        #   所有元素取绝对值再求和 → 一个标量
        #
        # while 循环:
        #   如果总和 > 1,就不断缩小
        #
        # 👉 作用:
        #   防止数值过大(类似归一化)
        #
        # 👉 关键点:
        #   - 这是“运行时逻辑”
        #   - 每次 forward 执行时动态决定循环次数
        #
        # 👉 为什么 PyTorch 可以?
        #   因为 forward 本质是 Python 代码(动态图)
        while X.abs().sum() > 1:
            X /= 2

        # ===============================
        # Step 5:输出标量
        # ===============================
        # 将所有元素求和,返回一个标量
        #
        # 👉 说明:
        #   - 最终输出不是向量,而是一个数
        #   - 常用于 loss-like 结构 或测试梯度
        return X.sum()
维度 普通 MLP FixedHiddenMLP
层结构 固定 半自由
参数 全部可训练 部分固定
forward 简单流程 程序逻辑
参数共享
控制流

动态图和静态图

  • 动态图 = 写代码时就执行(边跑边建图)
  • 静态图 = 先画好计算图,再一次性执行

动态图(PyTorch)


forward() 执行时:
  一边计算
  一边构建计算图
  
每次 forward 都可能不一样

静态图


Step1:定义计算图
Step2:编译优化
Step3:执行

图是固定的

维度 动态图(PyTorch) 静态图(ONNX / RKNN)
构建方式 运行时 事先定义
控制流(if/while) ✅ 支持 ❌ 基本不支持
调试 ✅ 非常方便 ❌ 很难
灵活性 ⭐⭐⭐⭐ ⭐⭐
性能优化 一般 ⭐⭐⭐⭐
部署 ❌ 需转换 ✅ 直接运行

为什么工业界更喜欢静态图?

🚀 可以做优化

比如:

  • 算子融合(conv + relu)
  • 内存复用
  • 图裁剪

适合硬件加速

  • GPU
  • NPU(RK3588)
  • DSP

这些硬件要求:计算图必须固定

深度学习三层世界


① 研究阶段(PyTorch)
   ↓(动态图)
② 导出阶段(ONNX)
   ↓(静态图)
③ 部署阶段(RKNN / TensorRT)


PC训练(PyTorch)
↓
导出 ONNX
↓
PC上用 RKNN Toolkit 转换
↓
部署到板子
↓
NPU推理

ONNX 是“深度学习模型的中间语言”,负责把 PyTorch 模型翻译成硬件能理解的格式

👉 ONNX 就像“深度学习的 USB 接口”

各种框架:

  • PyTorch
  • TensorFlow
  • MXNet

👉 都可以导出 ONNX

各种硬件:

  • RKNN(瑞芯微)
  • TensorRT(NVIDIA)
  • OpenVINO(Intel)

👉 都可以读取 ONNX

模块嵌套 + 模型组合


class NestMLP(nn.Module):
    def __init__(self):
        super().__init__()
        self.net = nn.Sequential(nn.Linear(20, 64), nn.ReLU(),
                                 nn.Linear(64, 32), nn.ReLU())
        self.linear = nn.Linear(32, 16)

    def forward(self, X):
        return self.linear(self.net(X))

chimera = nn.Sequential(NestMLP(), nn.Linear(16, 20), FixedHiddenMLP())
chimera(X)

拆解 NestMLP


输入 X
 ↓
NestMLP(子网络)
 ↓
Linear(16 → 20)
 ↓
FixedHiddenMLP(复杂逻辑块)
 ↓
输出


 X
 ↓
Linear(20 → 64)
 ↓
ReLU
 ↓
Linear(64 → 32)
 ↓
ReLU
 ↓
Linear(32 → 16)

参数管理

参数访问


import torch
from torch import nn

net  = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 1))
X = torch.rand(size=(2, 4))
Y = net(X)
print(Y)

print(net[2].state_dict())  # 打印输出层(第二层)的参数字典

运行结果


tensor([[0.2472],
        [0.1516]], grad_fn=<AddmmBackward0>)

OrderedDict([('weight', tensor([[ 0.1771,  0.2093,  0.2210, -0.2538, -0.0810, -0.2088,  0.1381,  0.1358]])), ('bias', tensor([0.2921]))])

目标参数


# 访问输出层的偏置参数(bias),它是一个 nn.Parameter 对象,包含了参数值和梯度信息
print(type(net[2].bias))
# 打印 bias 参数的值(一个张量),以及它的数据部分(不包含梯度信息)
print(net[2].bias)
# 直接访问 bias 参数的值(一个张量),这是我们在训练过程中需要更新的参数
print(net[2].bias.data)

print(net[2].bias.grad)  # 打印 bias 参数的梯度信息,初始时为 None,因为还没有进行反向传播

运行结果


<class 'torch.nn.parameter.Parameter'>
Parameter containing:
tensor([-0.2366], requires_grad=True)
tensor([-0.2366])
None

一次性访问所有参数


# 打印第一层的参数名称和形状
print(*[(name, param.shape) for name, param in net[0].named_parameters()])
# 打印整个网络的所有参数名称和形状
print(*[(name, param.shape) for name, param in net.named_parameters()])

# 遍历并打印模型所有参数的详细信息
print('\n模型所有参数详细信息:')
for name, param in net.named_parameters():
	print(f'参数名: {name}')
	print(f'参数形状: {tuple(param.shape)}')
	print(f'是否需要梯度: {param.requires_grad}')
	print(f'参数值:\n{param.data}')
	print('-' * 60)

运行结果


('weight', torch.Size([8, 4])) ('bias', torch.Size([8]))
('0.weight', torch.Size([8, 4])) ('0.bias', torch.Size([8])) ('2.weight', torch.Size([1, 8])) ('2.bias', torch.Size([1]))

模型所有参数详细信息:
参数名: 0.weight
参数形状: (8, 4)
是否需要梯度: True
参数值:
tensor([[-0.3850,  0.0084,  0.3014,  0.4221],
        [ 0.3175, -0.3265, -0.3164, -0.1454],
        [-0.0524,  0.4046,  0.1173,  0.3668],
        [ 0.4771, -0.3530,  0.2446, -0.3993],
        [ 0.4056, -0.4030, -0.2511,  0.1087],
        [-0.1805,  0.0254, -0.3561,  0.2133],
        [-0.4724, -0.2627, -0.4040,  0.1246],
        [-0.0196,  0.4235,  0.4254, -0.2781]])
------------------------------------------------------------
参数名: 0.bias
参数形状: (8,)
是否需要梯度: True
参数值:
tensor([-0.3310,  0.2453,  0.2516,  0.1448, -0.4960, -0.0500,  0.2982,  0.2216])
------------------------------------------------------------
参数名: 2.weight
参数形状: (1, 8)
是否需要梯度: True
参数值:
tensor([[ 0.3306,  0.2917,  0.2612, -0.1305,  0.2356, -0.3299, -0.0175, -0.2364]])
------------------------------------------------------------
参数名: 2.bias
参数形状: (1,)
是否需要梯度: True
参数值:
tensor([0.1262])
------------------------------------------------------------

为什么是 (8, 4)?

权重 shape = (输出维度, 输入维度)


🧩 从“神经元连接”理解

你这一层是:

输入:4个特征
输出:8个神经元

🧠 每个输出神经元做什么?

👉 每个神经元都会:

对 4 个输入做加权求和

举个例子(第1个神经元):
h1 = w11*x1 + w12*x2 + w13*x3 + w14*x4 + b1

👉 需要多少权重?

4 个
🔁 那一共有多少个神经元?
8 个
📦 所以总权重:
8 × 4 = 32 个参数

👉 排列成矩阵就是:

(8, 4)

输入 输出 weight shape bias shape
Linear(4,8) 4 8 (8,4) (8,)
Linear(8,1) 8 1 (1,8) (1,)

从嵌套块中收集参数


def block1():
    return nn.Sequential(nn.Linear(4, 8), nn.ReLU(),
                         nn.Linear(8, 4), nn.ReLU())

def block2():
    net = nn.Sequential()
    for i in range(4):
        # 在这里嵌套
        net.add_module(f'block {i}', block1())
    return net

rgnet = nn.Sequential(block2(), nn.Linear(4, 1))
rgnet(X)

print(rgnet)

print(rgnet[0][1][0].bias.data)  # 访问第一个 block 中第二层的 bias 参数值

运行结果


Sequential(
  (0): Sequential(
    (block 0): Sequential(
      (0): Linear(in_features=4, out_features=8, bias=True)
      (1): ReLU()
      (2): Linear(in_features=8, out_features=4, bias=True)
      (3): ReLU()
    )
    (block 1): Sequential(
      (0): Linear(in_features=4, out_features=8, bias=True)
      (1): ReLU()
      (2): Linear(in_features=8, out_features=4, bias=True)
      (3): ReLU()
    )
    (block 2): Sequential(
      (0): Linear(in_features=4, out_features=8, bias=True)
      (1): ReLU()
      (2): Linear(in_features=8, out_features=4, bias=True)
      (3): ReLU()
    )
    (block 3): Sequential(
      (0): Linear(in_features=4, out_features=8, bias=True)
      (1): ReLU()
      (2): Linear(in_features=8, out_features=4, bias=True)
      (3): ReLU()
    )
  )
  (1): Linear(in_features=4, out_features=1, bias=True)
)

tensor([ 0.0196,  0.1868,  0.4370, -0.1543,  0.3939, -0.4881, -0.0998, -0.0232])

参数初始化


# 参数初始化:默认情况下,nn.Linear 的权重参数是根据某种分布随机初始化的。我们可以自定义初始化方法来改变这些参数的初始值。
def init_normal(m):
    if type(m) == nn.Linear:
        # 对于 nn.Linear 层,我们使用正态分布来初始化权重参数,均值为 0,标准差为 0.01
        nn.init.normal_(m.weight, mean=0, std=0.01)
        # 将偏置参数初始化为 0
        nn.init.zeros_(m.bias)
# 这里我们使用 net.apply() 方法来递归地将 init_normal 函数应用到 net 中的每一个子模块(层)。
# 这样,net 中所有的 nn.Linear 层的权重和偏置都会按照我们定义的方式进行初始化。
net.apply(init_normal)
print(net[0].weight.data[0], net[0].bias.data[0])

自定义层

不带参数的层



import torch
import torch.nn.functional as F
from torch import nn

class CenteredLayer(nn.Module):
    def __init__(self):
        super().__init__()

    def forward(self, X):
        return X - X.mean()

layer = CenteredLayer()

print(layer(torch.FloatTensor([1, 2, 3, 4, 5])))

# 将 CenteredLayer 嵌入到一个更大的网络中,看看它的效果
net = nn.Sequential(nn.Linear(8, 128), CenteredLayer())

# 生成一个随机输入张量,形状为 (4, 8),表示 4 个样本,每个样本有 8 个特征
X = torch.rand(size=(4, 8))
print('输入 X 的均值:', X.mean().item())  # 打印输入张量的均值,看看它是否接近于 0.5(因为是均匀分布)
Y = net(X)

# 打印输出张量 Y 的均值,看看 CenteredLayer 是否成功地将输出中心化了
print('输出 Y 的均值:', Y.mean().item()) # 输出应该接近于 0,因为 CenteredLayer 的作用就是将输入减去其均值,使输出的均值为 0

运行结果


tensor([-2., -1.,  0.,  1.,  2.])
输入 X 的均值: 0.47072863578796387
输出 Y 的均值: -1.862645149230957e-09

带参数的层


class MyLinear(nn.Module):
    def __init__(self, in_units, units):
        super().__init__()
        # 在 MyLinear 的构造函数中,我们定义了两个参数:weight 和 bias。
        # 这两个参数都是 nn.Parameter 对象,这意味着它们是模型的可训练参数,会在训练过程中被优化器更新。
        self.weight = nn.Parameter(torch.randn(in_units, units))
        self.bias = nn.Parameter(torch.randn(units,))

    def forward(self, X):
        # 在 MyLinear 的前向传播函数中,我们使用 torch.matmul 来计算输入 X 与权重矩阵 self.weight 的矩阵乘积,然后加上偏置 self.bias。
        linear = torch.matmul(X, self.weight.data) + self.bias.data
        # 最后,我们使用 ReLU 激活函数对线性变换的结果进行非线性处理,返回激活后的输出。
        return F.relu(linear)

# 现在我们创建一个 MyLinear 层的实例,输入特征数为 5,输出特征数为 3
linner = MyLinear(5, 3)
print("权重矩阵的值:", linner.weight)
print("偏置向量的值:", linner.bias)

# 生成一个随机输入张量,形状为 (2, 5),表示 2 个样本,每个样本有 5 个特征
X = torch.rand(size=(2, 5))
# 将输入张量 X 送入 MyLinear 层,得到输出张量 Y
print('输入 X 的形状:', X.shape)  # 打印输入张量的形状,应该是 (2, 5)
print("输入 X 的值:", X)
Y = linner(X)
print('输出 Y 的形状:', Y.shape)  # 打印输出张量的形状,应该是 (2, 3)
print("输出 Y 的值:", Y)

运行结果


权重矩阵的值: Parameter containing:
tensor([[ 1.2916, -1.6615,  1.6200],
        [-2.2510,  1.4186, -0.6703],
        [ 1.0313,  2.8234, -0.6531],
        [ 0.3943,  0.6854,  1.1011],
        [ 1.3886,  2.2099, -0.6222]], requires_grad=True)
偏置向量的值: Parameter containing:
tensor([ 0.4404, -1.9364, -1.0052], requires_grad=True)
输入 X 的形状: torch.Size([2, 5])
输入 X 的值: tensor([[0.1078, 0.5892, 0.1004, 0.9521, 0.8323],
        [0.5682, 0.8048, 0.3928, 0.8261, 0.9732]])
输出 Y 的形状: torch.Size([2, 3])
输出 Y 的值: tensor([[0.8880, 1.4959, 0.0000],
        [1.4448, 2.0870, 0.0000]])

读写文件

加载和报错张量


import torch
from torch import nn
from torch.nn import functional as F

x = torch.arange(4.0)
print(x)
torch.save(x, 'x-file')  # 将张量 x 保存到文件 'x-file' 中

x2 = torch.load('x-file')  # 从文件 'x-file' 中加载张量,赋值给 x2
print(x2)

y = torch.zeros(4)
torch.save([x, y], 'x-file')  # 将张量列表 [x, y] 保存到文件 'x-y-file' 中
x2, y2 = torch.load('x-file')  # 从文件 'x-y-file' 中加载张量列表,分别赋值给 x2 和 y2
print(x2, y2)

mydict = {'x': x, 'y': y}
torch.save(mydict, 'mydict-file')  # 将字典 mydict 保存到文件 'mydict-file' 中
mydict2 = torch.load('mydict-file')
print(mydict2)

运行结果


tensor([0., 1., 2., 3.])
tensor([0., 1., 2., 3.])
tensor([0., 1., 2., 3.]) tensor([0., 0., 0., 0.])
{'x': tensor([0., 1., 2., 3.]), 'y': tensor([0., 0., 0., 0.])}

加载和保存模型参数


class MLP(nn.Module):
    def __init__(self):
        super().__init__()
        self.hidden = nn.Linear(20, 256)
        self.out = nn.Linear(256, 10)

    def forward(self, X):
        # 注意,这里我们使用ReLU的函数版本,其在nn.functional模块中定义。
        return self.out(F.relu(self.hidden(X)))

net = MLP()
X = torch.rand(size=(2, 20))
Y = net(X)
print("输出 Y 的值:", Y)
torch.save(net.state_dict(), 'mlp.params')  # 将模型 net 的参数字典(state_dict)保存到文件 'mlp.params' 中
clone = MLP()  # 创建一个新的 MLP 实例
clone.load_state_dict(torch.load('mlp.params'))  # 从文件 'mlp.params` 中加载参数到 clone 模型中
clone.eval()  # 将 clone 模型设置为评估模式(如果模型中有 dropout 或 batchnorm 层,这一步很重要)
Y_clone = clone(X)  # 使用 clone 模型对输入 X 进行前向传播,得到输出 Y_clone
print("克隆模型的输出 Y_clone 的值:", Y_clone)
print(Y_clone == Y)  # 打印比较 clone 模型的输出 Y_clone 与原模型的输出 Y 是否相等,应该是 True,因为它们使用了相同的参数

运行结果


输出 Y 的值: tensor([[ 0.1912, -0.1981, -0.0371, -0.0774, -0.0759, -0.2247,  0.1514,  0.0502,
         -0.0837,  0.1798],
        [ 0.2087, -0.0696, -0.0181, -0.0339, -0.1508, -0.1322,  0.0666, -0.1152,
         -0.0871,  0.0437]], grad_fn=<AddmmBackward0>)
克隆模型的输出 Y_clone 的值: tensor([[ 0.1912, -0.1981, -0.0371, -0.0774, -0.0759, -0.2247,  0.1514,  0.0502,
         -0.0837,  0.1798],
        [ 0.2087, -0.0696, -0.0181, -0.0339, -0.1508, -0.1322,  0.0666, -0.1152,
         -0.0871,  0.0437]], grad_fn=<AddmmBackward0>)
tensor([[True, True, True, True, True, True, True, True, True, True],
        [True, True, True, True, True, True, True, True, True, True]])

GPU



import torch
from torch import nn

torch.device('cpu')  # 返回当前可用的设备(CPU 或 GPU),如果没有 GPU 可用,则返回 CPU
torch.device('cuda')  # 返回当前可用的设备(CPU 或 GPU),如果有多个 GPU,默认使用第一个 GPU(cuda:0)
torch.device('cuda:0')  # 返回当前可用的设备(CPU 或 GPU),指定使用第一个 GPU(cuda:0)
torch.device('cuda:1')  # 返回当前可用的设备(CPU 或 GPU),指定使用第二个 GPU(cuda:1)

print(torch.cuda.is_available())  # 检查当前系统是否有可用的 GPU,如果有则返回 True,否则返回 False
print(torch.cuda.device_count())  # 返回当前系统中可用的 GPU 数量

def try_gpu(i=0):
    if torch.cuda.device_count() >= i + 1:  # 检查是否有足够的 GPU 可用
        return torch.device(f'cuda:{i}')  # 返回指定 GPU 的设备对象
    return torch.device('cpu')  # 如果没有足够的 GPU 可用,则返回 CPU 设备对象

def try_all_gpus():
    # 创建一个包含所有可用 GPU 设备对象的列表
    devices = [torch.device(f'cuda:{i}') for i in range(torch.cuda.device_count())]
    # 如果没有 GPU 可用,则返回包含 CPU 设备对象的列表
    return devices if devices else [torch.device('cpu')]

print(try_gpu())  # 尝试获取第一个 GPU 设备,如果没有则返回 CPU 设备
print(try_all_gpus())  # 获取所有可用 GPU 设备的列表,如果没有则返回包含 CPU 设备的列表

运行结果


True
1
cuda:0
[device(type='cuda', index=0)]

张量与GPU


x = torch.tensor([1.0, 2.0, 3.0])  # 创建一个包含三个元素的张量,数据类型为 float
print(x.device)  # 返回张量 x 所在的设备,默认情况下是 CPU

# 将张量 x 移动到 GPU 设备上(如果有可用的 GPU),否则保持在 CPU 上
X = torch.rand(size=(2, 3), device=try_gpu())
print(X.device)
Y = torch.rand(size=(2, 3), device=try_gpu(1))
print(Y.device)

运行结果


cpu
cuda:0
cpu

数据复制


Z = x.cuda(0)  # 将张量 x 移动到默认 GPU 设备上(如果有可用的 GPU),否则保持在 CPU 上
print(Z.device)

GPU 计算很快,但数据搬运和同步很慢,所以要尽量减少“小操作 + 数据来回搬”。

  1. 少搬数据(CPU ↔ GPU)
  2. 少同步(print / numpy)
  3. 多做大计算(batch + 向量化)
  4. 少做小计算(循环)

统一内存概念

CPU 和 GPU 共享同一块内存地址空间,不需要你手动拷贝数据,

自动内存拷贝需要:cache flush / DMA同步

维度 优点(为什么用) 缺点(为什么不用)
🧠 编程复杂度 不需要手动 .cpu() / .cuda() 行为“黑盒”,不易调优
🚀 开发效率 上手快,代码简洁 难以定位性能瓶颈
📦 数据管理 自动迁移(不用管数据在哪) 数据什么时候搬不可控
⚡ 性能稳定性 小规模/简单任务表现不错 大规模任务可能抖动严重
🔁 数据拷贝 减少显式 memcpy 实际仍然会拷贝(只是自动)
📄 内存模型 统一地址空间,方便复杂结构 page fault 代价高
🧩 访问方式 支持 CPU/GPU 混合访问 频繁跨设备访问会很慢
🔍 调试难度 代码逻辑清晰 性能问题难分析(隐式迁移)
🧮 计算效率 避免不必要拷贝 可能触发大量小规模迁移(更慢)
📱 硬件适配 SoC(RK3588等)天然友好 PC 独显(PCIe)收益有限
🧵 并发执行 简化多设备协作 容易产生隐式同步(阻塞)

各个厂商解决数据搬运问题的方案

方向 技术方案 代表厂商 本质
🧠 不搬 统一内存(Unified Memory) NVIDIA / Apple 共享地址空间
🚀 快搬 NVLink / Infinity Fabric NVIDIA / AMD 提高带宽
📦 少搬 Cache / SRAM 所有厂商 数据复用
🔄 边算边搬 异步执行 / Pipeline NVIDIA / Google overlap
🧮 就地算 近存计算(Near-Memory) Google / 华为 数据不出芯片
📡 零拷贝 DMA / Zero-copy 嵌入式厂商 避免复制
🧩 融合算子 Operator Fusion TensorRT / XLA 减少中间结果

传统复制方式


GPU0 (显存)
   ↓ PCIe
CPU RAM
   ↓ PCIe
GPU1 (显存)

现代方式


GPU0 显存
   ↓ NVLink
GPU1 显存

x = x.cuda(0)
y = y.cuda(1)

z = x + y   # 可能触发跨GPU拷贝

PCIe 4.0 ≈ 32 GB/s NVLink ≈ 100~900 GB/s(不同代)

查看GPU连接方式命令


(base) xt@xt-2288H-V5:~$ nvidia-smi topo -m
	GPU0	GPU1	CPU Affinity	NUMA Affinity	GPU NUMA ID
GPU0	 X 	SYS	0-7,16-23	0		N/A
GPU1	SYS	 X 	8-15,24-31	1		N/A

Legend:

  X    = Self
  SYS  = Connection traversing PCIe as well as the SMP interconnect between NUMA nodes (e.g., QPI/UPI)
  NODE = Connection traversing PCIe as well as the interconnect between PCIe Host Bridges within a NUMA node
  PHB  = Connection traversing PCIe as well as a PCIe Host Bridge (typically the CPU)
  PXB  = Connection traversing multiple PCIe bridges (without traversing the PCIe Host Bridge)
  PIX  = Connection traversing at most a single PCIe bridge
  NV#  = Connection traversing a bonded set of # NVLinks

服务器连接方式


GPU0
  ↓ PCIe
CPU0 内存
  ↓ UPI/QPI(跨CPU)
CPU1 内存
  ↓ PCIe
GPU1

两个 GPU 之间是 SYS 连接(跨 NUMA),不支持 P2P,通信必须经过 CPU 且跨 CPU,是最慢的一种 GPU 通信方式之一