CPU性能基准测试
首先回顾昨天在CPU上的训练结果。使用鸢尾花数据集训练多层感知机,20000个epochs的训练时间为19.09秒。这为我们的对比分析提供了重要的基准线。
通过代码了解一下测试环境的CPU配置:
python
import wmi
c = wmi.WMI()
processors = c.Win32_Processor()
for processor in processors:
print(f"CPU 型号: {processor.Name}")
print(f"核心数: {processor.NumberOfCores}")
print(f"线程数: {processor.NumberOfLogicalProcessors}")
# 输出:12th Gen Intel(R) Core(TM) i7-1255U
# 核心数: 10,线程数: 12
这是我的Intel第12代处理器,采用混合架构设计,结合了性能核心和能效核心,在处理小规模任务时具有良好的单核性能。
GPU训练环境配置
将训练迁移到GPU需要理解PyTorch的设备管理机制。核心概念是所有参与计算的张量和模型必须位于同一设备上:
python
import torch
# 检测GPU环境
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print(f"使用设备: {device}")
# 数据迁移到GPU
X_train = torch.FloatTensor(X_train).to(device)
y_train = torch.LongTensor(y_train).to(device)
# 模型迁移到GPU
model = MLP().to(device)
这里需要注意.to(device)
方法的行为差异:对于张量会返回新的副本,对于模型则直接修改原对象的参数位置。
意外的性能对比结果
运行相同的训练代码后,得到了比较意外的结果:
- CPU训练时间:19.09秒
- GPU训练时间:21.23秒
GPU居然比CPU更慢,但是跟据我们的经验GPU擅长并行运算,进行深度学习等科学计算相比CPU更有优势,训练速度应该更快。
GPU性能劣势的根本原因
GPU在小规模任务上表现不佳主要源于三个开销:
数据传输开销 是最主要的瓶颈。每次GPU计算都需要CPU内存到GPU显存的数据传输,更关键的是代码中的loss.item()
操作会在每个epoch都进行GPU到CPU的同步传输:
python
for epoch in range(num_epochs):
outputs = model(X_train)
loss = criterion(outputs, y_train)
optimizer.zero_grad()
loss.backward()
optimizer.step()
losses.append(loss.item()) # 这里触发GPU->CPU传输!
对于20000个epochs,这种频繁的数据同步累积了大量开销。
核心启动开销是第二个因素。GPU的每个操作都需要启动计算核心,当实际计算量很小时,启动开销在总时间中占比显著。
计算资源浪费是第三个问题。鸢尾花数据集只有150个样本,无法充分利用GPU的大量并行计算单元。
性能优化实验验证
为了验证数据传输开销的影响,进行了对照实验,减少不必要的GPU-CPU同步操作:
python
# 优化版本:减少数据传输频率
for epoch in range(num_epochs):
outputs = model(X_train)
loss = criterion(outputs, y_train)
optimizer.zero_grad()
loss.backward()
optimizer.step()
# 只在打印时才进行数据传输
if (epoch + 1) % 100 == 0:
print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {loss.item():.4f}')
优化后的GPU训练时间降至14.86秒,接近CPU性能。这清晰地证明了数据传输开销的重大影响。
进一步的实验显示,记录频率与性能的关系并非简单的线性关系,这涉及GPU异步执行的复杂机制。
GPU优势的发挥条件
GPU的真正优势在于处理大规模并行计算任务。当满足以下条件时,GPU的性能优势才会显现:
处理包含数万张图片的大型数据集时,数据传输开销被大量计算掩盖。训练具有数百万参数的深度网络时,复杂的矩阵运算能充分利用GPU的并行架构。使用较大的批量处理时,能够填满GPU的计算单元。
简单来说,只有当计算量足够大时,GPU的并行优势才能超越各种固定开销。
PyTorch调用机制解析
python
class MLP(nn.Module):
def __init__(self):
super(MLP, self).__init__()
self.fc1 = nn.Linear(4, 10) # 创建对象
def forward(self, x):
out = self.fc1(x) # 像函数一样调用对象
return out
这里self.fc1
是一个对象,但可以像函数一样调用。这是因为nn.Module
实现了__call__
方法,当我们调用self.fc1(x)
时,实际执行的是self.fc1.__call__(x)
,进而调用forward
方法完成计算。
这种设计保证了接口的一致性,让所有PyTorch组件都能以统一方式调用。