在NVIDIA A100 GPU上利用硬件JPEG解码器和NVIDIA nvJPEG库
根据调查,普通人产生的1.2万亿张图像可以通过电话或数码相机捕获。这样的图像的存储,尤其是以高分辨率的原始格式,会占用大量内存。
JPEG指的是联合图像专家组,该组织于2017年庆祝成立25周年。JPEG标准指定了编解码器,该编解码器定义了如何将图像压缩为字节的位流并解压缩回图像。
JPEG编解码器的主要目的是最小化照片图像文件的文件大小。JPEG是一种有损压缩格式,这意味着它不存储原始图像的完整像素数据。JPEG的优点之一是,它可以微调所使用的压缩量。如果正确使用,这会产生良好的图像质量,同时还会使最小的合理文件大小成为可能。
JPEG压缩的关键组件如下:
- 色彩空间转换可以分离亮度(Y)和色度(Cb,Cr)分量。Cb和Cr的下采样可以减小文件大小,而质量损失几乎没有引起注意,因为人的感知对这些图像分量不太敏感。这不是核心标准的一部分,但是定义为JFIF格式的一部分。
- 基于块的离散余弦变换(DCT)允许以较低的频率压缩数据。
- 量化允许对高频细节进行舍入系数。通常,可以丢失这些细节,因为人眼通常无法轻松地区分高频内容。
- 逐行编码在部分解码位流后预览整个图像的低质量版本。
以下照片(图1)显示了JPEG压缩导致的图像质量损失。原始的蝴蝶图像为BMP格式(512×512,24位,769 kB,无压缩),然后以JPEG格式显示同一图像,质量压缩系数为50%,二次采样为4:2:0,24位,图片大小为33 KB。
图1a. 原始蝴蝶图像(无压缩,大小512×512,24位),769 KB。
图1b. 压缩蝴蝶图像(质量压缩系数50%,二次采样4:2:0,24位),33 KB。
JPEG如何运作
图2显示了JPEG编码器的一种常见配置。
图2.使用GPU CUDA软件和CPU的并行利用的JPEG编码过程图。
首先,JPEG编码以RGB彩色图像开始。
第二步涉及将颜色转换为代表亮度(亮度)的YCbCr颜色空间Y和代表色度(红色和蓝色投影)的Cb和Cr通道。然后,将Cb和Cr通道以预定因子(通常为2或3)进行下采样。下采样提供了压缩的第一阶段。
在下一阶段,将每个通道划分为8×8的块,并计算DCT,这是一种类似于傅立叶变换的频率空间变换。DCT本身是无损且可逆的,将一个8×8空间块转换为64个通道。
然后,对DCT系数进行量化,该过程是有损的并且包括第二压缩阶段。量化由JPEG质量参数控制,较低的质量设置对应于更严格的压缩,因此文件更小。
量化阈值特定于每个空间频率,并且已经过精心设计。对低频的压缩比对高频的压缩要少,因为人眼对大范围内的细微W误差比对高频信号的大小变化更敏感。
最后阶段是使用Huffman编码无损地压缩量化的DCT系数,并将其存储在JPEG文件中,如图2所示。
图3显示了NVIDIA GPU上的JPEG解码过程。
图3. JPEG解码过程采用了GPU CUDA和软件的并行利用。用于霍夫曼解码的混合(CPU / GPU)方法克服了串行过程的停顿。
JPEG解码过程从压缩的JPEG位流开始,并提取头信息。
然后,霍夫曼解码处理串行过程,因为从比特流中一次解码一个DCT系数。
下一步将解量化和逆DCT处理为8×8块。
上采样步骤处理YCbCr转换并产生解码的RGB图像。
NVIDIA通过基于CUDA技术构建的nvJPEG库加速了JPEG编解码器。我们开发了JPEG算法的完整并行实现。JPEG编码器和解码器工作流的典型GPU加速部分如图2和3所示。
新的JPEG硬件解码器
推出了NVIDIA A100 GPU,它具有专用的硬件JPEG解码器。以前,数据中心GPU上没有这样的硬件单元,JPEG解码是同时使用CPU和GPU的纯软件CUDA解决方案。
现在,硬件解码器与其余GPU同时运行,后者可以执行各种计算任务,例如图像分类,目标检测和图像分割。与NVIDIA Tesla V100相比,它以不止一种方式提供了显着的吞吐量提高,JPEG解码速度提高了4-8倍。
它通过CUDA工具包的一部分nvJPEG库公开。
nvJPEG库概述
nvJPEG是用于JPEG编解码器的GPU加速库。结合数据扩展和图像加载库NVIDIA DALI,它可以通过加速数据的解码和扩展来加速对图像分类模型的深度学习训练。A100包含5核硬件JPEG解码引擎。nvJPEG利用硬件后端来批量处理JPEG图像。
图4. JPEG硬件解码过程采用了硬件解码器和GPU CUDA软件的并行利用。硬件解码器独立于CUDA SM,因此可以同时使用软件GPU解码器。
通过选择具有nvjpegCreateExinit功能的硬件解码器,nvJPEG可提供基线JPEG解码和各种颜色转换格式(例如YUV 420、422、444)的加速。如图4所示,与纯CPU处理相比,这将使图像解码速度提高20倍。DALI的用户可以直接受益于这种硬件加速,因为nvJPEG是抽象的。
nvJPEG库支持以下操作:
- nvJPEG编码
- nvJPEG转码
- nvJPEG解码(包括硬件(A100)支持)
该库支持以下JPEG选项:
- 基线和渐进JPEG编码和解码,仅适用于A100的基线解码
- 每像素8位
- 霍夫曼比特流解码
- 多达四个通道的JPEG位流
- 8位和16位量化表
- 以下三个色度通道Y,Cb,Cr(Y,U,V)的色度子采样:
- 4:4:4
- 4:2:2
- 4:2:0
- 4:4:0
- 4:1:1
- 4:1:0
该库具有以下功能:
- 同时使用CPU和GPU的混合解码。
- 库的输入在主机内存中,输出在GPU内存中。
- 单张图像和批量图像解码。
- 用户为设备提供的内存管理器和固定的主机内存分配。
绩效数字
对于本节中的性能图,使用了以下测试设置和GPU / CPU硬件:
- NVIDIA V100 GPU:CPU – E5-2698 v4 @ 2GHz 3.6GHz Turbo(Broadwell)HT on GPU – Tesla V100-SXM2-16GB(GV100)1 16160 MiB 1 80 SM GPU视频时钟1312 Batch 128和单线程
- NVIDIA A100 GPU CPU –铂金8168 @ 2GHz 3.7GHz Turbo(Skylake)HT on GPU – A100-SXM4-40GB(GA100)1 40557 MiB 1108 SM GPU视频时钟1095 Batch 128和单线程
- CPU:CPU –铂金8168 @ 2GHz 3.7GHz Turbo(Skylake)HT在TurboJPEG解码上进行CPU测试
- 图像数据集:2K FHD = 1920 x 1080 4K UHD = 3840 x 2160 CUDA Toolkit 11.0 CUDA驱动程序r450.24
接下来的两个图表显示了硬件JPEG解码器的解码速度。
图5.该图显示了A100上的硬件解码比V100上的CUDA混合解码所提高的速度。
图6. V100上的混合解码器所需的CPU线程数,以跟上A100上的硬件解码器吞吐量。
通过将解码工作转移到硬件上,可以释放宝贵的CPU周期,以便更好地利用它们。
图7显示了编码加速。
图7a. 对于1920×1080(2K FHD),3840×2160(4K UHD)图像尺寸的CPU,CUDA(V100,A100)之间的JPEG基线编码吞吐量比较。
图7b. 对于1920×1080(2K FHD),3840×2160(4K UHD)图像尺寸的CPU,CUDA(V100,A100)之间的JPEG渐进编码吞吐量比较。
图像解码示例
这是使用nvJPEG库的图像解码示例。此示例显示了A100 GPU上硬件解码器的使用以及其他NVIDIA GPU的后端回退。
//
// The following code example shows how to use the nvJPEG library for JPEG image decoding.
//
// Libraries used
// nvJPEG decoding
int main()
{
...
// create nvJPEG decoder and decoder state
nvjpegDevAllocator_t dev_allocator = {&dev_malloc, &dev_free};
nvjpegPinnedAllocator_t pinned_allocator ={&host_malloc, &host_free};
// Selecting A100 Hardware decoder
nvjpegStatus_t status = nvjpegCreateEx(NVJPEG_BACKEND_HARDWARE,
&dev_allocator,
&pinned_allocator,
NVJPEG_FLAGS_DEFAULT,
¶ms.nvjpeg_handle);
params.hw_decode_available = true;
if( status == NVJPEG_STATUS_ARCH_MISMATCH) {
std::cout<<"Hardware Decoder not supported. Falling back to default backend"<<std::endl;
// GPU SW decoder selected
nvjpegCreateEx(NVJPEG_BACKEND_DEFAULT, &dev_allocator,
&pinned_allocator, NVJPEG_FLAGS_DEFAULT,
¶ms.nvjpeg_handle);
params.hw_decode_available = false;
}
// create JPEG decoder state
nvjpegJpegStateCreate(params.nvjpeg_handle, ¶ms.nvjpeg_state)
// extract bitstream metadata to figure out whether a bitstream can be decoded
nvjpegJpegStreamParseHeader(params.nvjpeg_handle, (const unsigned char *)img_data[i].data(), img_len[i], params.jpeg_streams[0]);
// decode Batch images
nvjpegDecodeBatched(params.nvjpeg_handle, params.nvjpeg_state,
batched_bitstreams.data(),
batched_bitstreams_size.data(),
batched_output.data(), params.stream)
...
}
$ git clone https://github.com/NVIDIA/CUDALibrarySamples.git
$ cd nvJPEG/nvJPEG-Decoder/
$ mkdir build
$ cd build
$ cmake ..
$ make
// Running nvJPEG decoder
$ ./nvjpegDecoder -i ../input_images/ -o ~/tmp
Decoding images in directory: ../input_images/, total 12, batchsize 1
Processing: ../input_images/cat_baseline.jpg
Image is 3 channels.
Channel #0 size: 64 x 64
Channel #1 size: 64 x 64
Channel #2 size: 64 x 64
YUV 4:4:4 chroma subsampling
Done writing decoded image to file:/tmp/cat_baseline.bmp
Processing: ../input_images/img8.jpg
Image is 3 channels.
Channel #0 size: 480 x 640
Channel #1 size: 240 x 320
Channel #2 size: 240 x 320
YUV 4:2:0 chroma subsampling
Done writing decoded image to file:/tmp/img8.bmp
Processing: ../input_images/img5.jpg
Image is 3 channels.
Channel #0 size: 640 x 480
Channel #1 size: 320 x 240
Channel #2 size: 320 x 240
YUV 4:2:0 chroma subsampling
Done writing decoded image to file:/tmp/img5.bmp
Processing: ../input_images/img7.jpg
Image is 3 channels.
Channel #0 size: 480 x 640
Channel #1 size: 240 x 320
Channel #2 size: 240 x 320
YUV 4:2:0 chroma subsampling
Done writing decoded image to file:/tmp/img7.bmp
Processing: ../input_images/img2.jpg
Image is 3 channels.
Channel #0 size: 480 x 640
Channel #1 size: 240 x 320
Channel #2 size: 240 x 320
YUV 4:2:0 chroma subsampling
Done writing decoded image to file: /tmp/img2.bmp
Processing: ../input_images/img4.jpg
Image is 3 channels.
Channel #0 size: 640 x 426
Channel #1 size: 320 x 213
Channel #2 size: 320 x 213
YUV 4:2:0 chroma subsampling
Done writing decoded image to file:/tmp/img4.bmp
Processing: ../input_images/cat.jpg
Image is 3 channels.
Channel #0 size: 64 x 64
Channel #1 size: 64 x 64
Channel #2 size: 64 x 64
YUV 4:4:4 chroma subsampling
Done writing decoded image to file:/tmp/cat.bmp
Processing: ../input_images/cat_grayscale.jpg
Image is 1 channels.
Channel #0 size: 64 x 64
Grayscale JPEG
Done writing decoded image to file:/tmp/cat_grayscale.bmp
Processing: ../input_images/img1.jpg
Image is 3 channels.
Channel #0 size: 480 x 640
Channel #1 size: 240 x 320
Channel #2 size: 240 x 320
YUV 4:2:0 chroma subsampling
Done writing decoded image to file: /tmp/img1.bmp
Processing: ../input_images/img3.jpg
Image is 3 channels.
Channel #0 size: 640 x 426
Channel #1 size: 320 x 213
Channel #2 size: 320 x 213
YUV 4:2:0 chroma subsampling
Done writing decoded image to file:/tmp/img3.bmp
Processing: ../input_images/img9.jpg
Image is 3 channels.
Channel #0 size: 640 x 480
Channel #1 size: 320 x 240
Channel #2 size: 320 x 240
YUV 4:2:0 chroma subsampling
Done writing decoded image to file:/tmp/img9.bmp
Processing: ../input_images/img6.jpg
Image is 3 channels.
Channel #0 size: 640 x 480
Channel #1 size: 320 x 240
Channel #2 size: 320 x 240
YUV 4:2:0 chroma subsampling
Done writing decoded image to file:/tmp/img6.bmp
Total decoding time: 14.8286
Avg decoding time per image: 1.23571
Avg images per sec: 0.809248
Avg decoding time per batch: 1.23571
图像大小调整示例
此图像调整大小和加水印示例根据客户端请求生成图像的缩放版本。图8显示了图像调整大小和加水印的典型工作流程。
图8.并行使用GPU软件和CUDA的图像大小调整和水印流水线。
下面的代码示例演示如何调整图像大小并在徽标图像上添加水印。
//
// The following code example shows how to resize images and watermark them with a logo image.
//
// Libraries used
// nvJPEG decoding, NPP Resize, NPP watermarking, nvJPEG encoding
int main()
{
...
// nvJPEG decoder
nReturnCode = nvjpegDecode(nvjpeg_handle, nvjpeg_decoder_state, dpImage, nSize, oformat, &imgDesc, NULL);
// NPP image resize
st = nppiResize_8u_C3R_Ctx(imgDesc.channel[0], imgDesc.pitch[0], srcSize,
srcRoi, imgResize.channel[0], imgResize.pitch[0], dstSize, dstRoi,
NPPI_INTER_LANCZOS, nppStreamCtx);
st = nppiResize_8u_C3R_Ctx(imgDescW.channel[0], imgDescW.pitch[0], srcSizeW,
srcRoiW,imgResizeW.channel[0], imgResizeW.pitch[0], dstSize, dstRoi,
NPPI_INTER_LANCZOS, nppStreamCtx);
// Alpha Blending watermarking
st = nppiAlphaCompC_8u_C3R_Ctx(imgResize.channel[0], imgResize.pitch[0],
255, imgResizeW.channel[0], imgResizeW.pitch[0], ALPHA_BLEND,
imgResize.channel[0], imgResize.pitch[0], dstSize, NPPI_OP_ALPHA_PLUS,
nppStreamCtx);
// nvJPEG encoding
nvjpegEncodeImage(nvjpeg_handle, nvjpeg_encoder_state, nvjpeg_encode_params,
&imgResize, iformat, dstSize.width, dstSize.height,NULL));
...
}
$ git clone https://github.com/NVIDIA/CUDALibrarySamples.git
$ cd nvJPEG/Image-Resize-WaterMark/
$ mkdir build
$ cd build
$ cmake ..
$ make
// Running Image resizer and watermarking
$ ./imageResizeWatermark -i ../input_images/ -o resize_images -q 85 -rw 512 -rh 512