引言

某一天心血来潮准备拿laptop跑一个刚剪枝完的demo模型,装完pytorch之后想测试一下1660ti的性能,便跑了一下yolov5作为benchmark,没想到推理得到的矩阵都是NaN,着实令人费解,下面说说踩坑的经历以及结果。

🐛 错误以及重现

在带有 Cuda 11.1 的 GTX 1660 Ti 上运行时,半精度推理返回多个模型的 NaN

测试代码及环境

环境:

Collecting environment information...
PyTorch version: 1.8.1+cu111
Is debug build: False
CUDA used to build PyTorch: 11.1
ROCM used to build PyTorch: N/A

OS: Ubuntu 18.04.2 LTS (x86_64)
GCC version: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0
Clang version: 6.0.0-1ubuntu2 (tags/RELEASE_600/final)
CMake version: version 3.20.2

Python version: 3.7 (64-bit runtime)
Is CUDA available: True
CUDA runtime version: 11.1.105
GPU models and configuration: GPU 0: NVIDIA GeForce GTX 1660
Nvidia driver version: 465.19.01
cuDNN version: Probably one of the following:
/usr/lib/x86_64-linux-gnu/libcudnn.so.8.0.5
/usr/lib/x86_64-linux-gnu/libcudnn_adv_infer.so.8.0.5
/usr/lib/x86_64-linux-gnu/libcudnn_adv_train.so.8.0.5
/usr/lib/x86_64-linux-gnu/libcudnn_cnn_infer.so.8.0.5
/usr/lib/x86_64-linux-gnu/libcudnn_cnn_train.so.8.0.5
/usr/lib/x86_64-linux-gnu/libcudnn_ops_infer.so.8.0.5
/usr/lib/x86_64-linux-gnu/libcudnn_ops_train.so.8.0.5
HIP runtime version: N/A
MIOpen runtime version: N/A

Versions of relevant libraries:
[pip3] numpy==1.20.3
[pip3] torch==1.8.1+cu111
[pip3] torchvision==0.9.1+cu111
[conda] blas                      1.0                         mkl  
[conda] cudatoolkit               11.1.74              h6bb024c_0    nvidia
[conda] ffmpeg                    4.3                  hf484d3e_0    pytorch
[conda] mkl                       2021.2.0           h06a4308_296  
[conda] mkl-service               2.3.0            py39h27cfd23_1  
[conda] mkl_fft                   1.3.0            py39h42c9631_2  
[conda] mkl_random                1.2.1            py39ha9443f7_2  
[conda] numpy                     1.20.1           py39h93e21f0_0  
[conda] numpy-base                1.20.1           py39h7d8b39e_0  
[conda] pytorch                   1.8.1           py3.9_cuda11.1_cudnn8.0.5_0    pytorch
[conda] torchaudio                0.8.1                      py39    pytorch
[conda] torchvision               0.9.1                py39_cu111    pytorch

测试用例:

import torch
import urllib
from PIL import Image
from torchvision import transforms

model = torch.hub.load('pytorch/vision:v0.9.0', 'mobilenet_v2', pretrained=True)
url, filename = ("https://github.com/pytorch/hub/raw/master/images/dog.jpg", "dog.jpg")
input_image = Image.open(filename)
preprocess = transforms.Compose([
    transforms.Resize(256),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])
input_tensor = preprocess(input_image)
input_batch = input_tensor.unsqueeze(0)

model = model.cuda().float()
input_batch = input_batch.cuda().float()

with torch.no_grad():
    output = model(input_batch)
print("FP32 output:", output)

model = model.cuda().half()
input_batch = input_batch.cuda().half()

with torch.no_grad():
    output = model(input_batch)
print("FP16 output:", output)

预期输出

我认为 FP16 的输出与 FP32 的输出大致相同,但上面的运行结果都为nan(为清楚起见,以下为截断后的输出):

FP32 output: tensor([[-1.8283e+00, -1.4972e+00, -1.1716e+00, ..., -5.8914e-01,  9.7267e-01,  1.9510e+00]],
       device='cuda:0')

FP16 output: tensor([[nan, nan, nan, ..., nan, nan, nan, nan, nan]],
       device='cuda:0', dtype=torch.float16)

用其他网络进行测试,仍然产生相同的错误:

import torch

model = torch.hub.load('pytorch/vision:v0.9.0', 'resnet18', pretrained=True)
input_tensor = torch.rand((1,3,512,512))

model = model.cuda().half()
input_tensor = input_tensor.cuda().half()
with torch.no_grad():
    output = model(input_tensor)

print("FP16 output:", output)

model = model.cuda().float()
input_tensor = input_tensor.cuda().float()
with torch.no_grad():
    output = model(input_tensor)

print("FP32 output:", output)

事实上,在其他 GPU(例如 1080ti、2080ti)上运行相同的测试均可以得到有效的fp16输出,而在另一台1660Ti的机器上进行测试也产生了上述相同的错误。

令人费解的是,在带有 torch 1.8.1 / cuda 10.2 的 1660 上进行相同的测试无法重现该问题,只要cuda版本高于11则问题复现。

错误原因以及解决方案

经过不懈的努力,我从Nvidia官网上的公告发现了蛛丝马迹:这其实是Nvidia的锅,GTX1600系列中的FP16核心对于 Turing Minor 来说是全新的,在过去的任何 NVIDIA GPU 架构中都没有出现过(TU116专用阉割核心)。GTX 1600 系列有专用的 FP16 CUDA 内核,但其 1408 个内核中只有 128 个是这些专用的 FP16 内核。它们的目的在功能上与通过 Turing Major 上的张量核心运行 FP16 操作相同:允许 NVIDIA 在每个 SM 分区内同时执行 FP16 操作和 FP32 或 INT32 操作。而且因为它们只是 FP16 核心,所以它们非常小。NVIDIA 没有给出具体细节,但仅从吞吐量来看,它们应该只是它们所取代的张量核心大小的一小部分。

NVIDIA声称这个问题已经在8.2.2版本的cudnn上解决了,但非常遗憾,问题依旧存在。

用人话说就是,Nvidia在GTX 1600系列的显卡中使用了特殊的fp16运算单元,导致GPU在计算fp16时会发生错误。

解决的办法就是:在训练或者推理时使用全精度(fp32)即可解决问题,这意味着你需要关闭AMP(自动混合精度(Automatic Mixed Precision)):amp=False

--precision full --no-half
#在训练或者推理时加上这一行参数即可

总结

虽说推理/训练是正常了,但VRAM的占用变大了100%,失去fp16的加持,运算速度也慢了不少,对于1660这区区6G的显存,至于为什么cuda10.2上没有这个问题,原因是cuda10.2压根就不支持混合精度运算… 不支持混合精度运算意味着没调用fp16的计算单元(全程fp32计算)。