DEBUG.WIKI
V.PS:让项目靠近用户,亚太、欧洲、北美 KVM VPS
IPPeak:从目标地区访问网页,住宅代理、SEO 监测、广告验证
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国

模型已搬到 GPU,PyTorch 仍报 CPU 和 CUDA 设备不一致

模型和输入已搬到 GPU,某些隐藏层仍留在 CPU。检查实际报错运算、子模块注册、to 的返回值及临时张量,避免优化器遗漏参数。

输入已经调用了 .to(device),模型也执行了 model.to(device),前向计算仍然报错:

RuntimeError: Expected all tensors to be on the same device,
but found at least two devices, cpu and cuda:0!

尤其容易误判的一种情况是:next(model.parameters()).is_cuda 为真,藏在普通 Python 列表中的某些层却还在 CPU 上。这个检查只看到了第一个已注册参数,并没有检查整个前向计算。

本文针对 PyTorch 的普通 nn.Module,相关行为按 2.8 接口核对。主动把模型拆到多个设备的模型并行、自动设备映射和分布式框架有自己的边界,不能要求整个模型一律搬到一块卡。

定位报错运算的输入与权重

先看堆栈最后进入的是线性层、卷积、损失计算,还是前向过程中的自定义运算。错误发生在线性层时,要核对进入该层的输入,以及这一层的 weight、bias;只看训练循环最外面的输入不够。

对于已有的 model 和 inputs,下面的诊断片段可列出注册参数与缓冲区的设备,不会打印训练数据:

print("inputs:", inputs.device)
for name, parameter in model.named_parameters():
    print("parameter:", name, parameter.device)
for name, buffer in model.named_buffers():
    print("buffer:", name, buffer.device)

这份列表仍只包括已注册的对象。如果代码中明明有 hidden_layers,输出却没有相应参数名,就要回到模型定义查看它们是如何保存的。对疑似遗漏的层,直接读取该层的 weight.device,不要用列表中的其他层代替它。

张量的 .to() 还需要注意返回值:设备不同的时候,它会返回转换后的张量。写了 inputs.to(device) 却继续使用原来的 inputs,仍可能把 CPU 输入交给 GPU 模型。通常应写成 inputs = inputs.to(device)。模块的 model.to(device) 则会原地修改注册的参数和缓冲区,两者行为不要混淆。

普通列表中的层改为 ModuleList

下面是一个完整的模型结构示例。隐藏层放进 nn.ModuleList,输出层直接赋给模块属性;二者都会被注册:

import torch
from torch import nn


class Regressor(nn.Module):
    def __init__(self):
        super().__init__()
        self.hidden_layers = nn.ModuleList([
            nn.Linear(2, 16),
            nn.Linear(16, 8),
        ])
        self.output = nn.Linear(8, 1)

    def forward(self, inputs):
        for layer in self.hidden_layers:
            inputs = torch.relu(layer(inputs))
        return self.output(inputs)


device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
model = Regressor().to(device)
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
inputs = torch.randn(4, 2, device=device)
prediction = model(inputs)

如果改回 self.hidden_layers = [nn.Linear(...), ...],仅仅把层装进普通列表不会完成子模块注册。model.to(device) 无法依靠这个列表自动移动其中的层,model.parameters() 也不会自动包含它们。即使手动逐层搬到 GPU,让报错消失,优化器仍可能遗漏这些参数。

按名称保存子模块时,可以采用 nn.ModuleDict;无需自定义循环、只按顺序执行时,nn.Sequential 也适合。不要为了消除错误,把普通张量一律包装成可训练参数。

更改注册方式会影响参数集合和 state_dict 键。保留原模型定义、检查点与训练配置,核对新旧键名后再决定如何恢复权重;不要用忽略所有缺失键的方式声称旧检查点已完整加载。优化器应在模型结构确定并放到目标设备后创建,旧训练状态也需要单独核对。

模型内部新建的张量也有设备

注册了全部子模块,仍可能在 forward() 中临时创建 CPU 张量。临时张量需要与某个输入一起运算时,可以明确指定设备;若形状与类型规则允许,也可以用输入张量的 new_* 方法。下面是针对已有输入 inputs 的局部示例:

offset = inputs.new_zeros((inputs.shape[0], 1))

new_zeros 默认沿用输入的设备和数据类型。只有这正是业务需要时才采用;例如索引需要整数类型,不能盲目沿用浮点输入的类型。

需要长期跟随模型移动、但不参与训练的张量,可在初始化中用 register_buffer 注册。单纯写成普通 Tensor 属性,并不保证它随 model.to() 移动。训练目标、损失函数中带状态的张量也应在实际报错位置核对,而不是只迁移模型输入。

如果 torch.cuda.is_available() 原本就为假,应先处理CUDA 可用性问题。这里的 CPU 后备分支只是示例选择,不能据此认定业务已经用上 GPU。

修改后先核对注册参数列表包含了所有预期层,再用一批受控输入检查前向、损失与反向传播。报错消失之外,还应确认预期参数确实在优化器中、梯度能按训练设计产生;检查通过后再继续训练,避免把“能运行”误认为“所有层都在学习”。