浙大疏锦行CPU 与 GPU 训练对比解密 GPU 小数据集反而更慢 __call__魔法方法详解前言今天基于鸢尾花数据集搭建 MLP 多层感知机分别在 CPU、GPU 上训练模型踩了一个经典大坑小数据集、简单网络GPU 训练速度反而不如 CPU。我们分析背后 3 大开销并且学习 PyTorch 底层核心设计nn.Module的__call__魔法方法。一、完整代码CPU 版本训练鸢尾花 MLPimport torch import torch.nn as nn import torch.optim as optim from sklearn.datasets import load_iris from sklearn.model_selection import train_test_split from sklearn.preprocessing import MinMaxScaler import numpy as np import time import matplotlib.pyplot as plt # 加载鸢尾花数据集 iris load_iris() X iris.data # 特征数据 y iris.target # 标签数据 # 划分训练集和测试集 X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.2, random_state42) # 归一化神经网络对输入特征尺度敏感 scaler MinMaxScaler() X_train scaler.fit_transform(X_train) X_test scaler.transform(X_test) #测试集只用transform防止数据泄露 # 转为pytorch张量分类标签用LongTensor X_train torch.FloatTensor(X_train) y_train torch.LongTensor(y_train) X_test torch.FloatTensor(X_test) y_test torch.LongTensor(y_test) # 搭建MLP模型 class MLP(nn.Module): def __init__(self): super(MLP, self).__init__() self.fc1 nn.Linear(4, 10) # 输入4特征隐藏层10神经元 self.relu nn.ReLU() self.fc2 nn.Linear(10, 3) # 输出3分类 # CrossEntropyLoss自带Softmax输出层不用激活函数 def forward(self, x): out self.fc1(x) out self.relu(out) out self.fc2(out) return out # 实例化模型、损失函数、优化器 model MLP() criterion nn.CrossEntropyLoss() optimizer optim.SGD(model.parameters(), lr0.01) num_epochs 20000 losses [] start_time time.time() for epoch in range(num_epochs): # 前向传播两种写法model.forward(X_train) 或者 model(X_train) outputs model(X_train) loss criterion(outputs, y_train) # 反向传播更新参数 optimizer.zero_grad() # 梯度清零pytorch梯度会累积 loss.backward() optimizer.step() losses.append(loss.item()) if (epoch 1) % 100 0: print(fEpoch [{epoch1}/{num_epochs}], Loss: {loss.item():.4f}) time_all time.time() - start_time print(fTraining time: {time_all:.2f} seconds) # 绘制loss曲线 plt.plot(range(num_epochs), losses) plt.xlabel(Epoch) plt.ylabel(Loss) plt.title(Training Loss over Epochs) plt.show()我的机器i9-12900KFCPU 训练耗时约2.90s二、GPU 版本训练代码核心改动模型、张量全部.to(device)迁移到 GPU张量与模型必须在同一个设备否则直接报错Tensor is on CPU, but expected it to be on GPUimport torch import torch.nn as nn import torch.optim as optim from sklearn.datasets import load_iris from sklearn.model_selection import train_test_split from sklearn.preprocessing import MinMaxScaler import time import matplotlib.pyplot as plt # 判断CUDA是否可用设置设备 device torch.device(cuda:0 if torch.cuda.is_available() else cpu) print(f使用设备: {device}) # 查看CUDA硬件信息 if torch.cuda.is_available(): print(CUDA可用) device_count torch.cuda.device_count() print(f可用的CUDA设备数量: {device_count}) current_device torch.cuda.current_device() print(f当前CUDA设备索引: {current_device}) print(fGPU名称: {torch.cuda.get_device_name(current_device)}) print(fCUDA版本: {torch.version.cuda}) print(cuDNN版本:, torch.backends.cudnn.version()) # 加载并预处理数据 iris load_iris() X iris.data y iris.target X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.2, random_state42) scaler MinMaxScaler() X_train scaler.fit_transform(X_train) X_test scaler.transform(X_test) # 张量迁移到GPU X_train torch.FloatTensor(X_train).to(device) y_train torch.LongTensor(y_train).to(device) X_test torch.FloatTensor(X_test).to(device) y_test torch.LongTensor(y_test).to(device) # 模型定义不变 class MLP(nn.Module): def __init__(self): super(MLP, self).__init__() self.fc1 nn.Linear(4, 10) self.relu nn.ReLU() self.fc2 nn.Linear(10, 3) def forward(self, x): out self.fc1(x) out self.relu(out) out self.fc2(out) return out # 模型迁移到GPU model MLP().to(device) criterion nn.CrossEntropyLoss() optimizer optim.SGD(model.parameters(), lr0.01) num_epochs 20000 losses [] start_time time.time() for epoch in range(num_epochs): outputs model(X_train) loss criterion(outputs, y_train) optimizer.zero_grad() loss.backward() optimizer.step() # 每200轮记录loss减少GPU-CPU同步 if (epoch 1) % 200 0: losses.append(loss.item()) if (epoch 1) % 100 0: print(fEpoch [{epoch1}/{num_epochs}], Loss: {loss.item():.4f}) time_all time.time() - start_time print(fTraining time: {time_all:.2f} seconds) plt.plot(range(len(losses)), losses) plt.xlabel(Epoch) plt.ylabel(Loss) plt.title(Training Loss over Epochs) plt.show()大坑GPU 跑这个任务反而更慢RTX3080Ti 跑原始代码耗时 11s远大于 CPU 的 2.9s。三大核心开销数据传输开销显存↔内存GPU 计算前数据、模型参数要从电脑内存拷贝到 GPU 显存调用loss.item()时必须把 GPU 上张量传回 CPU这是同步阻塞操作。20000 次循环反复传输开销巨大。GPU Kernel 启动开销每一次矩阵乘法、激活函数都要在 GPU 上启动一个计算内核 kernel启动存在固定开销。小计算量场景下启动开销占比极高。并行能力浪费鸢尾花数据集很小网络极简单GPU 成千上万个计算单元大部分空闲强大的并行算力完全发挥不出来。✅ GPU 适合大数据集、深层大模型、大量矩阵 / 卷积并行计算。玩具小数据集CPU 单核性能更强。优化实验减少 loss.item () 调用表格记录间隔轮记录次数次剩余时长秒10020010.4320010010.0210002010.122000109.74现象减少记录次数总耗时下降不呈线性关系原因loss.item()是 CPU-GPU 同步阻塞操作但 GPU kernel 调度、硬件延迟是不稳定的。除了 item () 开销kernel 启动开销是固定存在的所以减少 item () 次数不会等比例减少总时间。三、PyTorch 底层核心__call__魔法方法问题self.fc1 是 nn.Linear 实例对象为什么可以写self.fc1(x)self.fc1 nn.Linear(4,10) out self.fc1(x) # 对象直接加括号调用像函数一样Python 中只要类实现__call__魔法方法它的实例就可以像函数一样obj()调用。nn.Linear继承nn.Module父类nn.Module内置写好了__call__。 伪代码简化理解 nn.Moduleclass nn.Module: def __call__(self, *args,**kwargs): # 1.执行前向钩子pre-forward hook # 2.调用你写的forward方法 result self.forward(*args, **kwargs) # 3.执行后向钩子post-forward hook return result执行逻辑self.fc1(x)→ 隐式调用self.fc1.__call__(x)→ 内部自动执行self.fc1.forward(x)⚠️ 重点官方推荐model(x)而不是model.forward(x)直接调用 forward () 会跳过所有钩子函数丢失回调、日志等功能。nn.ReLU 也是同理torch.relu()是原生函数nn.ReLU()是封装好的类实例依靠__call__实现relu_layer(x)写法统一所有网络层的调用接口。Python __call__基础 Demo# 无参数__call__ class Counter: def __init__(self): self.count 0 def __call__(self): self.count 1 return self.count counter Counter() print(counter()) #1 print(counter()) #2 # 带参数__call__ class Adder: def __call__(self, a, b): return a b adder Adder() print(adder(3,5)) #8四、CPU/GPU 硬件简单科普CPU 选型要点架构代际新架构指令集、缓存更好性能更强制程工艺数字越小晶体管密度高能效更好性能核 P 核强单核适合小模型、游戏能效核 E 核低功耗后台多任务核心 / 线程数、主频GPU 深度学习选型要点代RTX40xx30xx20xx新架构优化 Tensor Core型号等级90 旗舰 80 高端 70 甜点 60 入门Ti 为增强版本显存 VRAM深度学习最重要存储模型参数、特征图、输入数据。显存不足会无法加载大模型只能用极小 batch梯度噪声变大。五、总结小数据集 简单网络GPU 不一定更快存在显存传输、kernel 启动固定开销大模型大数据才能发挥 GPU 并行优势。.to(device)把模型 / 张量迁移 GPU必须保证输入张量和模型在同一个设备。loss.item()会触发 GPU 到 CPU 的数据同步循环内频繁调用会显著拖慢 GPU 训练。__call__魔法方法让类实例可以像函数一样obj()调用PyTorch nn.Module 依靠这个机制实现model(x)并且支持钩子。CrossEntropyLoss 内部自带 Softmax多分类网络输出层不要加激活函数。完整项目代码放在文中可直接复制运行观察 CPU/GPU 训练耗时差异。