基准测试移动 GPU 中的浮点精度 - 第 1 部分

原文: Benchmarking floating-point precision in mobile GPUs

 

作者Tom Olson / 2013  5  29 日下午 2:39 /

谈到 GPU 性能时(在Mali官方网站有很多讨论),我们通常会谈论速度。在之前的博文中,我们谈论了每秒可在屏幕中显示的像素数,每秒可以宣称绘制的三角形数(不要问),最近我们还谈论了每秒可以运行的浮点数操作。谈论速度非常有趣,我们热衷于这一点,但这并非人们感兴趣的唯一 GPU 性能指标;质量也算一个。毕竟,如果获得错误的结果,计算速度有多快就不重要了。在我接下来几篇博文中,我将(暂时)放下对速度的迷恋,谈谈对 GPU 浮点运算质量的基准测试。要谈论的内容有许多,篇幅肯定会很长。所以请保存下来,有时间再细看。

但首先要大喊一声
使用浮点运算比较棘手。许多程序员都不能真正地了解它,即便是一些非常优秀的也不例外。一些看起来正确的代码可能得不到正确的结果,而且常常会难以找到的原因。即使在你为 IEEE-754 兼容良好的 CPU 编写代码时,也会存在该问题。当你面对具有更多特性(其实是 GPU)的设备时,很容易推断你看到的不正常现象是因为在运算上出现了错误。但并不一定是这样;可能只是因为你对正常结果的直观概念是错误的。

如果你准备利用浮点运算做些比较前卫的事情 – 当然质量基准测试就属于这一类别 – 你最好习惯于准确思考浮点单元中发生的一切,也就是说要更加细致地理解浮点运算的原理,可能要超过你真正想要达到的水平。搞定它,黑客!

比你想的更多的细节
如果已了解浮点的原理,你可以跳过这一部分;如果没有,这里有几篇有关 IEEE-754 单精度浮点的维基百科文章,你真的应该读一下这些优秀文章。不过,对本文而言,你真正需要了解的只是下文:

基本的浮点数包含一个符号 n,几个指数位,还有几个有效数位。(有些人说尾数而不是有效数;我有点喜欢它的发音 – 尾数尾数,但如今已成为一种怀旧的说法。)如果使用典型的 FP32 浮点,将有 8 位指数和 23 位有效数。指数(逻辑上)范围为 -126  +127;此处我将逻辑值写为E。有效数是二进制定点数,我写为 1.sss…,值就是:

最后,整个浮点数的值按如下方法算出:

form.jpg

my_value = (-1)n × 2E × 1.sssssssssssssssssssssss

由于有效数的位数是有限的,数字表示的精度就存在限制。

假设我们要将两个数字相加,如 1600 万和 11.3125

16000000 = (-1)0 × 223 × 1.11101000010010000000000

  1. 11.31250 = (-1)0 × 20× 1.01101010000000000000000

要将它们相加,我们首先要右移位(也叫非规范化)较小数字的有效数,使得指数相等。在本例中,我们得移位 20 位:

16000000 = (-1)0 × 223 × 1.11101000010010000000000

  1. 11.31250 = (-1)0 × 223 × 0.00000000000000000001011(010100…00)

… 然后,将有效数相加获得结果:

16000011 = (-1)0 × 223 × 1.11101000010010000001011

… 最后,根据需要进行重新规范化,但本例中不需要。

请注意,较小数的一些位(上文红色表示)移到了有效数的末尾之外而被丢弃,所以我们的结果偏差了 0.3125;这就是你在进行浮点运算时丢失精度的常见方式。你要相加的两个数字指数差距越大,你丢失的位越多。

GPU 中的浮点精度
现在,我们已准备好开始讨论 GPU 中的浮点。我谈论这一主题的灵感源自 Stuart Russell 的文章,该文章发布在 Youi Labs 网站上。他比较了 6款移动 GPU,以及一款桌面显卡,得到一些有趣的发现。我先回顾一下他的结果。我早前说过浮点是棘手的,正确的行为可能会产生不直观的结果... 这得到了证实。

Stuart 利用精心设计的 OpenGL ES 2.0 片段(像素)着色器进行了对比。我的版本如下所示;它稍有不同,但这些修改不会影响结果。他的博文包含着色器在每款设备上生成的内容的图片,我强烈建议花点时间看看这些图片。结果中存在显著的差异。它们全部都是 OpenGL ES 2.0 兼容设备,但OpenGL ES 对浮点运算的定义非常松散。这对于一般的图形应用来说不是问题,但测试着色器经过了精心设计,它对浮点运算精度比较敏感。

// Youi Labs GPU precision shader (slightly modified)

precision highp float;

uniform vec2 resolution;

void main( void )

{

float y = ( gl_FragCoord.y / resolution.y ) * 26.0;

float x = 1.0 – ( gl_FragCoord.x / resolution.x );

float b = fract( pow( 2.0, floor(y) ) + x );

if(fract(y) >= 0.9)

b = 0.0;

gl_FragColor = vec4(b, b, b, 1.0 );

}

 

看得出移动 GPU 中存在许多差异;告诉我一些以前不知道的。例如,什么类型的差异?事实证明,Stuart 的结果中有多个不同(基本上互不相关的)的地方。我将从最简单的开始:所有图像都将屏幕画面划分多个水平条,但条纹的数量从最少 10 个到最多 23 个不等。为什么?

为了回答这个问题,我们需要仔细看看测试着色器。

测试着色器的行为
上述着色器在图像的每个像素上运行。内置的输入变量 gl_FragCoord 提供 x  y 像素坐标。函数的第一行(变量 y)将图像划分为 26 个水平条纹,其中 y 的整数部分告诉你当前像素位于哪一个条纹( 25),小数部分告诉你它在条纹上方多远处。第二行(变量 x)计算亮度值,从图像最左侧的接近 1.0(白色)到最右侧的接近 0.0(黑色)呈线性方式改变。第 4 和第 5 行将每个条纹的上部 10% 像素变为黑色,让你容易看清条纹数量。

有趣的地方在第 3 行:

float b = fract( pow( 2.0, floor(y) ) + x );

内置的 pow() 函数返回整数:20(第一条)、21(第二条)、22(第三条),以此类推,直到达到 225(最后一条)为止。该整数值与亮度 x 相加,两者之和的整数部分被 fract() 函数丢弃。

我们已知道在将两个不同大小的浮点数相加时会发生什么:较小数的低阶位将被丢弃。所以,当着色器丢弃整数部分时,留下的部分为原始的亮度x,除了一些低阶位被丢弃之外;我们在第一条少了 0 位,第二条少了 1 位,以此类推。结果导致亮度在灰度的数量级上越来越小,光滑的斜坡变得越来越凹凸不平。当指数的差异等于有效数的位数时,所有位都被丢弃,我们就看不到条纹了。由于 x 始终小于 1,其浮点指数最多为 -1;所以,如果你进行简单的三年级运算(是介绍负数的时间吗?),你会让自己相信图像中非黑色条纹的数量正好等于着色器引擎的浮点有效数的小数部分的位数。太棒了!

尾数、尾数
所以,这些图像告诉我们的第一点是不同的 GPU 在有效数的位数上有所不同。似乎分成两个大类:极简派,仅提供 OpenGL ES 2.0 要求的位数;以及奢侈派模型,提供接近于 FP32 的位数。我们来分开考虑。

小即是美
比较中的两款 GPU 采取了极简派方式:ARM  MaliTM-400 拥有 10 位有效数,NVIDIA  Tegra 3 拥有 13 位,两者都大约是其他四款 GPU提供位数的一半。区别很大 – 怎么回事呢?

原因在于 OpenGL ES 2.0(或者说 GLSL ES 1.0 着色语言)定义了三种不同的浮点数:highpmediump  lowp。第一种 (highp有至少 7 位指数和 16 位有效数,而第二种 (mediump则拥有至少 5 位指数和 10 位有效数。(第三种 (lowp实际上根本算不上浮点数;最小的实施是 10 位定点数,小数精度为 8 位。)务必要认识到这些是最小值;需要的话,可以完全自由地实现 64 位浮点的 lowp

更为重要的是要认识到,在 OpenGL ES 2.0 中,片段着色器对 highp 精度的支持是可选的Mali-400  Tegra 3 不支持 highp,其他四种 GPU 则是支持的。为何不同?其他四款 GPU 统一着色器架构;它们对顶点和片段着色使用相同的计算引擎。OpenGL ES 2.0 要求在顶点着色器中支持highp;由于必须为顶点着色提供这一支持,同时提供给片段着色在这些结构上不会增加硅面积成本。Mali-400  Tegra 3 使用非统一着色器,意味着它们对顶点着色和片段着色使用不同的计算引擎。这允许它们为必须执行的任务优化各个引擎。支持 highp 在硅面积和功耗上成本昂贵,而且并不是标准要求,所以放弃它似乎是这些架构理所当然的选择。编写良好的 OpenGL ES 2.0 内容并不需要它,丢弃它可以实现非常、非常高效的核心。

对于如何为不支持 highp  GPU 编写代码还有许多要了解的地方;如果需要更加全面的讨论,请参阅 Sean Ellis 针对该主题撰写的博文

Puttin’ on the Bitz
(对不起,我控制不住自己。)

现在,我们来看看奢侈派模型。在 Stuart 的结果中,如果你放大图像并且仔细数的话,你会看到 Qualcomm  Adreno 225  21 条,ARM Mali-T604  22 条,Vivante  Imagination 核心有 23 条。这是否表示 GC2000  SGX544 的精度比 Mali  Adreno 高?

 Stuart 的博文发表出来时,这个问题让我辗转反侧。最后,我注意到除了屏幕底部有标准的 Android 导航栏之外,Mali-T604 图像在屏幕顶部还有一个状态栏。Adreno 225 图像中的要厚一点,GC2000  SGX544 图像中则没有。嗯... 都来看看吧,我们的 Android 黑客。事实证明,如果你不够仔细,Android 状态栏可以混合到你所称的全屏应用的上方;或许,它们覆盖了其中一些条纹?好吧,我承认,这是我重新实现 Stuart 的着色器的真正原因。我必须搞清楚!

 1 显示了在配备 Mali-T604  Nexus 10 上运行该着色器的结果,以及在使用 Qualcomm Adreno 225  Samsung Galaxy SIII(美国版)上运行的结果(我们在实施中使用 GL 坐标,而不是 DX 坐标,因此我们的图像与 Stuart 的上下颠倒;如果这对你有干扰,那就倒立着看它们吧)。如果你不喜欢数条纹的话,这些图像表明,这两款 GPU 实际上在其有效数中有 23 个小数位,与 Imagination  Vivante 核心相同。也就是说:所有这些 GPU 提供完全相同的原始精度。

pic f 1.jpg.png

 1:测试着色器在 Mali-T604Nexus 10,左侧)和 Adreno 225Samsung Galaxy SIII,右侧)上运行

显而易见的事实
我们解答了 Stuart 图像中的条纹数告诉你的问题:它是片段着色器有效数中小数位的数量。Mali-400  10 位,正如你从使用 IEEE-754 半精度(binary16) 作为其浮点类型的设备上所预期的。Adreno 225GC4000Mali-T604  SGX544 都提供 23 位,表示它们提供的位数与 IEEE-754 单精度 (binary32) 接近。Tegra 3 有效数拥有 13 个小数位,从我的了解来看是 NVIDIA 所独一无二的。

不过,如果你看看 Stuart 的图像,条纹数并不是你首先注意到的地方。首先跳入你眼帘的是这些条纹排列成形状各不相同的图案。其中一些(如上图 1 中的 Mali-T604)组成了对称的碗或蜂巢状;而 Adreno 225 等另一些则紧靠在图像左侧并且逐渐以曲线方式向右分散;Imagination SGX544的图形则完全别具一格。怎么回事呢?答案很是有趣,但这篇博文已经够长了,今天到此为止吧。在下一篇中,我们将探索那些不同之处,看看它们会告诉我们有关这些 GPU 的什么信息。

到那时候 - 认为我上面所说的有错吗?认为我在胡说八道吗?那就留言吧

posted on 2014-03-29 09:55  JonnyLulu  阅读(915)  评论(0编辑  收藏  举报

导航