深度学习(4):NCHW和NHWC
NHWC的话以此类推,代表的是[C W H N],第一个元素是000,第二个沿C方向,即020,040, 060..一直到300,之后沿W方向,001 021 041 061...301..到了303后,沿H方向,即004 024 .。在不同的硬件加速的情况下,选用的类型不同,在intel GPU加速的情况下,因为GPU对于图像的处理比较多,希望在访问同一个channel的像素是连续的,一般存储选
1. NCHW NHWC格式说明
N代表数量, C代表channel,H代表高度,W代表宽度.
NCHW其实代表的是[W H C N],第一个元素是000,第二个元素是沿着w方向的,即001,这样下去002 003,再接着呢就是沿着H方向,即004 005 006 007...这样到019后,沿C方向,轮到了020,之后021 022 ...一直到319,然后再沿N方向。
NHWC的话以此类推,代表的是[C W H N],第一个元素是000,第二个沿C方向,即020,040, 060..一直到300,之后沿W方向,001 021 041 061...301..到了303后,沿H方向,即004 024 .。。304.。最后到了319,变成N方向,320,340....
data_format 默认值为 "NHWC。其中 N 表示这批图像有几张,H 表示图像在竖直方向有多少像素,W 表示水平方向像素数,C 表示通道数(例如黑白图像的通道数 C = 1,而 RGB 彩色图像的通道数 C = 3)。为了便于演示,我们后面作图均使用 RGB 三通道图像。
NCHW 中,C 排列在外层,每个通道内像素紧挨在一起,即 'RRRRRRGGGGGGBBBBBB' 这种形式。
NHWC 格式,C 排列在最内层,多个通道对应空间位置的像素紧挨在一起,即 'RGBRGBRGBRGBRGBRGB' 这种形式。
如果我们需要对图像做彩色转灰度计算,NCHW 计算过程如下:
即 R 通道所有像素值乘以 0.299,G 通道所有像素值乘以 0.587,B 通道所有像素值乘以 0.114,最后将三个通道结果相加得到灰度值。
相应地,NHWC 数据格式的彩色转灰度计算过程如下:
输入数据分成多个(R, G, B) 像素组,每个像素组中 R 通道像素值乘以 0.299,G 通道像素值乘以 0.587,B 通道像素值乘以 0.114 后相加得到一个灰度输出像素。将多组结果拼接起来得到所有灰度输出像素。
以上使用两种数据格式进行 RGB -> 灰度计算的复杂度是相同的,区别在于访存特性。通过两张图对比可以发现,NHWC 的访存局部性更好(每三个输入像素即可得到一个输出像素),NCHW 则必须等所有通道输入准备好才能得到最终输出结果,需要占用较大的临时空间。
在 CNN 中常常见到 1x1 卷积(例如:用于移动和嵌入式视觉应用的 MobileNets),也是每个输入 channel 乘一个权值,然后将所有 channel 结果累加得到一个输出 channel。如果使用 NHWC 数据格式,可以将卷积计算简化为矩阵乘计算,即 1x1 卷积核实现了每个输入像素组到每个输出像素组的线性变换。
TensorFlow 为什么选择 NHWC 格式作为默认格式?因为早期开发都是基于 CPU,使用 NHWC 比 NCHW 稍快一些(不难理解,NHWC 局部性更好,cache(缓存) 利用率高)。
NCHW 则是 Nvidia cuDNN 默认格式,使用 GPU 加速时用 NCHW 格式速度会更快(也有个别情况例外)。
最佳实践:设计网络时充分考虑两种格式,最好能灵活切换,在 GPU 上训练时使用 NCHW 格式,在 CPU 上做预测时使用 NHWC 格式。
在不同的硬件加速的情况下,选用的类型不同,在intel GPU加速的情况下,因为GPU对于图像的处理比较多,希望在访问同一个channel的像素是连续的,一般存储选用NCHW,这样在做CNN的时候,在访问内存的时候就是连续的了,比较方便。
# NCHW [batch,in_channels,in_height,in_weight]
# NHWC [batch,in_height,in_weight,in_channels]
# CHWN [in_channels,in_height,in_weight,batch]
# 转换 NCHW---NHWC
import tensorflow as tf
x = tf.reshape(tf.range(24),[1,2,3,4])
out = tf.transpose(x,[0,2,3,1])
print (x.shape)
print (out.shape)
#转换NHWC--NCHW
import tensorflow as tf
x = tf.reshape(tf.range(24), [1, 3, 4, 2])
out = tf.transpose(x, [0, 3, 1, 2])
print (x.shape)
print (out.shape)
参考:
原文链接:https://blog.csdn.net/weixin_41847115/article/details/83794551
2. NHWC,NCHW,HWFC,HWCF转换c++实现
已经在路上了就努力修行吧
关注他
4 人赞同了该文章
一:背景
在python中,我们使用numpy.transpose实现多维数组的layout转换,但是在c++中,transpose转置函数并没有直接实现,tensorflow中调用第三方库Eigen实现多维数组的转换,在AI编译器开发过程中,涉及到输入和权重的layout转换,比如常见的conv算子,各种硬件支持的layout不一样,conv有输入activation和filter权重,对于输入activation,有的硬件支持NHWC,有的支持NCHW,还有其他的变种等等,前端框架比如tensorflow默认是NCHW,conv的权重,有的硬件支持HWFC,有的支持HWCF(H:卷积核的宽度,W:卷积核的宽,C:卷积核输入通道数,需要和input的通道channel一样;F:卷积核的个数等于卷积输出的通道数),这些框架训练的模型对接到各种NPU时,都需要转换tensor的layout,工作中经常用到,今天抽个时间总结记录一下。
二:tensor layout转换算法如何理解
先上画的图,下面再解释并加上代码。
图一:NHWC NCHW内存排布
上图中表示一张3hx2wx3c的小图片,我们为了简单只画了一个图片,默认n=1,图中1,2,3,4表示该像素点的值,所以如果是nchw布局,上图nchw的shape是{1,3,3,2},由于nchw布局时,图片上所有R通道数据在内存中紧挨着放,这里特意用图片的RGB来说是为了让大家理解起来更容易,实际上在cv模型中比如resnet除了第一个conv的输入是这种三通道(举例子不一定)的rgb(举例子不一定)数据,后面的feature tensor都是channel大部分高于3的,这里我说一个最简单又特别特别好用的办法:看最后一个维度是哪一个,数据就按这个方向的顺序排列,脑袋都不要去想空间布局,比如NCHW,最后一个维度是W,那么内存排布就是按W方向第一优先级排列,然后才是H,C,N,所以你看图中按NCHW排列的话:数据在内存中就是w方向:1,2没有了,然后h方向取一行,去取一行后最优先的维度又从h变成了w方向:3,4,然后h方向也取完了,去c方向取...依此顺序循环,直到数据取完。
理解好上面的加粗部分后所有转换就好办了,下面放上代码解释一下代码中如何体现上面的这种简单粗暴的思维
/* nchw to nhwc */
void TransposeNchw2Nhwc(float *input_data, float *output_data, int N, int C, int H, int W)
{
// [N,C,H,W] -> [N,H,W,C]
for (int n = 0; n < N; ++n)
{
for (int h = 0; h < H; ++h)
{
for (int w = 0; w < W; ++w)
{
for (int c = 0; c < C; ++c)
{
int old_index = n * C * H * W + c * H * W + h * W + w; // 原始索引值
int new_index = n * H * W * C + h * W * C + w * C + c; // 新索引值
output_data[new_index] = input_data[old_index];
}
}
}
}
for(int i = 0; i < N*C*H*W; i++){
std::cout << "output_data[" << i << "]: " << output_data[i] << std::endl;
}
}
上面代码中是实现conv的输入activation的layout由nchw转为nhwc,注意四个for循环的遍历顺序N,H,W,C(这种遍历最容易理解)和目标layout的nhwc一致,其中最重要的就是找到目标索引和原来索引的对应关系,这里的顺序非常关键,完全体现了上面的思维:int old_index = n C H W + c H W + h W + w; // 原始索引值,看顺序,从最后面开始,因为原始layout是nchw,所以:w:最里面的维度,只有一维,所以就是w;h*W:h方向是第二维,加一就会有W个h;c * H * W:第三维度,加一就有H * W个值,后面依此类推,同理int new_index = n * H * W * C + h * W * C + w * C + c; // 新索引值也是一样的,从最内维度开始,从右往左看,就会非常的简单,依据这个规律,我们可以写出nhwc,hchw,hwcf,hwfc之间的所有转换,下面放一个权重weight的图和代码,原理解释和上面一样,不再重复。
图二:HWFC HWCF内存排布
图二中表示一个shape为2x2x3x2的卷积核,图中的1,7。。。数字表示该位置的权重值,图中列出了hwfc和hwcf二种layout时卷积核在内存中的排列顺序。
void TransposeHwfc2Hwcf(float *input_data, float *output_data, int H, int W, int F, int C)
{
for (int h = 0; h < H; ++h)
{
for (int w = 0; w < W; ++w)
{
for (int c = 0; c < C; ++c)
{
for (int f = 0; f < F; ++f)
{
int old_index = h*W*F*C + w*F*C + f*C + c;
int new_index = h*W*C*F + w*C*F + c*F + f;
output_data[new_index] = input_data[old_index];
}
}
}
}
for(int i = 0; i < H*W*F*C; i++){
std::cout << "output_data[" << i << "]: " << output_data[i] << std::endl;
}
}
三:结果验证
对于结果的验证,我们直接用numpy的transpose函数进行对比验证,比如对于hwfc转hwcf,如图二中所示的权重:
import numpy as np
const_value = np.array([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24]).reshape(2,2,2,3)
>>> const_value_1 = np.transpose(const_value, (0,1,3,2))
>>> const_value_1
array([[[[ 1, 4],
[ 2, 5],
[ 3, 6]],
[[ 7, 10],
[ 8, 11],
[ 9, 12]]],
[[[13, 16],
[14, 17],
[15, 18]],
[[19, 22],
[20, 23],
[21, 24]]]])
>>>
我们用c++跑结果:
int main(){
float img_data[25] = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24};
float output_data[25] = {0};
TransposeHwfc2Hwcf(img_data, output_data, 2, 2, 2, 3);
return 0;
}
输出结果:
output_data[0]: 1
output_data[1]: 4
output_data[2]: 2
output_data[3]: 5
output_data[4]: 3
output_data[5]: 6
output_data[6]: 7
output_data[7]: 10
output_data[8]: 8
output_data[9]: 11
output_data[10]: 9
output_data[11]: 12
output_data[12]: 13
output_data[13]: 16
output_data[14]: 14
output_data[15]: 17
output_data[16]: 15
output_data[17]: 18
output_data[18]: 19
output_data[19]: 22
output_data[20]: 20
output_data[21]: 23
output_data[22]: 21
output_data[23]: 24
numpy的transpose结果和我们写的函数一样,我们可以选取实际模型中的权重进行测试,更加贴近实际开发中的layout转换。
3. 从GPU的内存访问视角对比NHWC和NCHW
链接:从GPU的内存访问视角对比NHWC和NCHW-腾讯云开发者社区-腾讯云
NHWC和NCHW是卷积神经网络(cnn)中广泛使用的数据格式。它们决定了多维数据,如图像、点云或特征图如何存储在内存中。
- NHWC(样本数,高度,宽度,通道):这种格式存储数据通道在最后,是TensorFlow的默认格式。
- NCHW(样本数,通道,高度,宽度):通道位于高度和宽度尺寸之前,经常与PyTorch一起使用。
NHWC和NCHW之间的选择会影响内存访问、计算效率吗?本文将从模型性能和硬件利用率来尝试说明这个问题。
卷积作为GEMM
GEneral Matrix to Matrix Multiplication (通用矩阵的矩阵乘法)
卷积可以使用基于变换的方法来实现,如快速傅立叶变换,它将卷积转换为频域的元素乘法,或者使用无变换的方法,如矩阵乘法,其中输入和滤波器(卷积核)被平面化并使用矩阵操作组合以计算输出特征映射。
但是:fft是内存密集型的,因为它们需要额外的内存来存储转换后的矩阵。并且fft的计算成本很高,特别是在时域和频域之间来回转换数据时,涉及操作开销。
而卷积运算的一般矩阵乘法是这样的。每个接受域按列堆叠,得到特征映射变换矩阵。同时还将滤波器矩阵逐行平摊和叠加,形成滤波器变换矩阵。滤波变换和特征映射变换矩阵经过矩阵乘法运算,形成扁平化的输出矩阵。这里的变换矩阵是一个中间矩阵,只是数值重排,与频域变换没有关系。
N -特征图的批量大小,C -输入通道,h -输入高度,W -输入宽度,
k -输出通道,r -滤波器高度,s -滤波器宽度,p -输出高度,q -输出宽度
特征映射变换矩阵和滤波变换矩阵被认为是中间矩阵,其维数大于特征映射本身。feature map的尺寸= C × H × W, (3x3x3) feature map transform的尺寸= CRS × NPQ (12x4)
GEMM的GPU实现:
GPU为了避免内存预感使用了隐式GEMM。在隐式GEMM中,不是形成Transform矩阵,而是对每个列和行进行动态索引。最终的输出直接存储在输出张量对应的索引中。
由SMs(流多处理器)组成的GPU主要用于执行并行计算。在上面的隐式GEMM中,每个矩阵乘法可以分成更小的矩阵乘法或块。然后每个块都由SMs同时处理,以加快过程。
有了上面的计算过程,还需要存储张量,下面我们看看张量是如何在GPU中存储的。
张量通常以跨行格式存储在GPU中,其中元素在内存布局中以非连续的方式存储。这种跨行存储方法提供了以各种模式(如NCHW或NHWC格式)排列张量的灵活性,优化了内存访问和计算效率。
下图中所示的给定张量,我们可以用NCHW和NHWC的行主格式表示它们,行主存储通过顺序存储每一行来安排内存中的张量元素。
NCHW
这里W是最动态的维度。同一通道中的元素存储在一起,然后是下一个通道中的元素。
NHWC
这里C是动态的维度。所有通道中来自相同空间位置的元素依次存储,然后是来自下一个空间位置的元素,从而优化对每个通道内空间数据的访问。
GPU上的内存吞吐量
GPU是高度并行的处理器,当数据访问以合并方式完成时,它们工作得最好,这意味着它们喜欢以连续的、有组织的方式读取数据。当每个线程在二级缓存中查找数据时,如果是缓存命中(请求内存的内容在缓存中可用),则内存访问速度很快。如果是缓存丢失(缓存命中的否定),那么GPU接近DRAM来获取请求的内存地址的内容,这是一个耗时的操作。
当GPU需要访问存储在内存中的数据时,它会在“事务”中这样做。根据GPU配置,每个事务访问32/128字节的信息。访问的信息保留在缓存中。当另一个GPU线程请求内存访问时,它首先检查缓存。如果数据在缓存中不可用,那么请求将被转发到DRAM。
GPU工作原理十分复杂,我们不想也没有时间在这里详细解释,所以将其简单概括为:
合并内存事务发生在GPU访问连续块中的内存时。如果GPU需要读取连续存储在内存中的32字节数据,它将执行单个合并内存事务来一次检索所有32字节。非合并内存事务发生在GPU需要访问未连续存储在内存中的数据时。在这种情况下,GPU将需要执行多个事务来检索所有必要的数据
在GEMM的情况下,无论滤波器的高度和宽度如何,我们都可以确保读取给定空间位置的所有通道信息。例如,如果我们的输入特征是128 x 128 x 32。无论使用1x1还是3x3内核,我们都可以读取位置(1,1)的所有通道。
如果使用NCHW,它将属于单个通道的所有元素存储在一起,我们将不得不跨到位置a[0], a[16384], a[32,768]……直到位置a[16384x31]进行1x1卷积。这些位置不是连续的,并且肯定会导致缓存丢失,从而导致内存读取期间的额外开销。在每个事务期间读取的其余数据也不被使用,也称为非合并内存事务。
当使用NHWC格式表示张量时,访问位置是a[0],a[1]…,a[127],它们是连续的,并且肯定是缓存命中。第一次访问a[0]会导致缓存丢失和从DRAM获取32/128字节数据的事务。当访问a[1]时,这将是保存事务的缓存命中。即使在一定数量的位置之后缓存丢失导致来自DRAM的事务,事务本身将携带连续内存位置的连续数据,可以在访问进一步位置时缓存命中,称为合并内存事务。
NHWC减少了张核gpu的内存访问瓶颈,从而优化了性能,与NCHW相比,这似乎是一个更好的选择。
以下是NVIDIA A100-SXM4-80GB, CUDA 11.2, cuDNN 8.1下NCHW和NHCW的TFLOPS的性能条款。我们看到NHWC在两种设置下的TFLOPS方面表现更好。为了简单起见,在这里没有进入NC/xHWx布局,这是NHWC的一个变体,为NVIDIA张量核心操作准备。
那么为什么Pytorch还要使用NCHW呢?
官方论坛的一个帖子可以作为参考:
https://discuss.pytorch.org/t/why-does-pytorch-prefer-using-nchw/83637
另外就是TensorFlow 的官网也说过这么一段话,也可以作为参考
Most TensorFlow operations used by a CNN support both NHWC and NCHW data format. On GPU, NCHW is faster. But on CPU, NHWC is sometimes faster.
参考资料
- https://docs.nvidia.com/deeplearning/performance/dl-performance-convolutional/index.html#imp-gemm-dim
- https://docs.nvidia.com/deeplearning/cudnn/developer-guide/index.html
- https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html
- https://leimao.github.io/blog/CUDA-Convolution-Tensor-Layouts/
- https://www.microway.com/hpc-tech-tips/avoiding-gpu-memory-performance-bottlenecks/
- https://stackoverflow.com/questions/44280335/how-much-faster-is-nchw-compared-to-nhwc-in-tensorflow-cudnn
更多推荐
所有评论(0)