深度学习 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 计算很快,但数据搬运和同步很慢,所以要尽量减少“小操作 + 数据来回搬”。
- 少搬数据(CPU ↔ GPU)
- 少同步(print / numpy)
- 多做大计算(batch + 向量化)
- 少做小计算(循环)
统一内存概念
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 通信方式之一
0
次点赞