展开循环是否会影响其中计算的准确性



摘要问题展开循环是否会影响循环中执行的计算的准确性?如果是,为什么?

精化和背景我正在使用HLSL编写一个计算着色器,用于Unity项目(2021.2.9f1)。我的部分代码包括数值过程和高度振荡函数,这意味着高计算精度至关重要。

当将我的结果与Python中的等效过程进行比较时,我注意到一些1e-5量级的偏差。这令人担忧,因为我没想到如此大的误差是精度差异的结果,例如HLSL中三角函数或幂函数的浮点精度。

最后,经过多次调试,我现在认为展开或不展开循环的选择是导致偏差的原因。然而,我确实觉得这很奇怪,因为我似乎找不到任何来源表明展开一个循环会影响准确性;空间-时间权衡";。

为了澄清,如果认为我的Python结果是正确的解决方案,那么在HLSL中展开循环会得到比不展开更好的结果

最小工作示例下面是一个MWE,它由Unity的C#脚本、执行计算的相应计算着色器以及在Unity中运行时我的控制台的屏幕截图(2021.2.9f1)组成。请原谅我对Newtons方法的实现有点混乱,但我选择保留它,因为我认为这可能是导致这种偏差的原因。也就是说,如果简单地计算cos(x),则展开和未展开之间没有区别。尽管如此,我仍然不明白在测试内核中简单添加[unroll(N)]是如何改变结果的。。。

// C# for Unity
using UnityEngine;
public class UnrollTest : MonoBehaviour
{
[SerializeField] ComputeShader CS;
ComputeBuffer CBUnrolled, CBNotUnrolled;
readonly int N = 3;
private void Start()
{
CBUnrolled = new ComputeBuffer(N, sizeof(double));
CBNotUnrolled = new ComputeBuffer(N, sizeof(double));
CS.SetBuffer(0, "_CBUnrolled", CBUnrolled);
CS.SetBuffer(0, "_CBNotUnrolled", CBNotUnrolled);
CS.Dispatch(0, (int)((N + (64 - 1)) / 64), 1, 1);
double[] ansUnrolled = new double[N];
double[] ansNotUnrolled = new double[N];
CBUnrolled.GetData(ansUnrolled);
CBNotUnrolled.GetData(ansNotUnrolled);
for (int i = 0; i < N; i++)
{
Debug.Log("Unrolled ans = " + ansUnrolled[i] + 
"  -  Not Unrolled ans = " + ansNotUnrolled[i] +  
"  --  Difference is: " + (ansUnrolled[i] - ansNotUnrolled[i]));
}
CBUnrolled.Release();
CBNotUnrolled.Release();
}
}
#pragma kernel CSMain
RWStructuredBuffer<double> _CBUnrolled, _CBNotUnrolled;
// Dummy function for Newtons method
double fDummy(double k, double fnh, double h, double theta)
{
return fnh * fnh * k * h * cos(theta) * cos(theta) - (double) tanh(k * h);
}
// Derivative of Dummy function above using a central finite difference scheme.
double dfDummy(double k, double fnh, double h, double theta)
{
return (fDummy(k + (double) 1e-3, fnh, h, theta) - fDummy(k - (double) 1e-3, fnh, h, theta)) / (double) 2e-3;
}
// Function to solve.
double f(double fnh, double h, double theta)
{
// Solved using Newton's method.
int max_iter = 50;
double epsilon = 1e-8;
double fxn, dfxn;
// Define initial guess for k, herby denoted as x.
double xn = 10.0;
for (int n = 0; n < max_iter; n++)
{
fxn = fDummy(xn, fnh, h, theta);

if (abs(fxn) < epsilon)     // A solution is found.
return xn;

dfxn = dfDummy(xn, fnh, h, theta);
if (dfxn == 0.0)    // No solution found.
return xn;
xn = xn - fxn / dfxn;
}
// No solution found.
return xn;
}
[numthreads(64,1,1)]
void CSMain(uint3 threadID : SV_DispatchThreadID)
{
int N = 3;

// ---------------
double fnh = 0.9, h = 4.53052, theta = -0.161, dtheta = 0.01;   // Example values.

for (int i = 0; i < N; i++)                 // Not being unrolled
{   
_CBNotUnrolled[i] = f(fnh, h, theta);
theta += dtheta;
}

// ---------------
fnh = 0.9, h = 4.53052, theta = -0.161, dtheta = 0.01;          // Example values.
[unroll(N)] for (int j = 0; j < N; j++)     // Being unrolled.
{
_CBUnrolled[j] = f(fnh, h, theta);
theta += dtheta;
}
}

运行上述时Unity控制台的图像

编辑经过更多的测试,偏差已缩小到以下代码,在完全相同的展开代码与未展开代码之间相差约1e-17。尽管差距很小,但我仍然认为这是一个有效的例子,因为我认为他们应该平等。

[numthreads(64, 1, 1)]
void CSMain(uint3 threadID : SV_DispatchThreadID)
{
if ((int) threadID.x != 1)
return;

int N = 3;
double k = 1.0;

// ---------------
double fnh = 0.9, h = 4.53052, theta = -0.161, dtheta = 0.01; // Example values.

for (int i = 0; i < N; i++)                 // Not being unrolled
{
_CBNotUnrolled[i] = (k + (double) 1e-3) * theta - (k - (double) 1e-3) * theta;
theta += dtheta;
}

// ---------------
fnh = 0.9, h = 4.53052, theta = -0.161, dtheta = 0.01; // Example values.

[unroll(N)]
for (int j = 0; j < N; j++)     // Being unrolled.
{
_CBUnrolled[j] = (k + (double) 1e-3) * theta - (k - (double) 1e-3) * theta;
theta += dtheta;
}
}

在上运行编辑后的脚本时Unity控制台的图像

Edit 2以下是Edit 1中给出的内核的编译代码。不幸的是,我在汇编语言方面的经验有限,我无法发现这个脚本是否显示了任何错误,或者它是否对手头的问题有用。

**** Platform Direct3D 11:
Compiled code for kernel CSMain
keywords: <none>
binary blob size 648:
//
// Generated by Microsoft (R) D3D Shader Disassembler
//
//
// Note: shader requires additional functionality:
//       Double-precision floating point
//
//
// Input signature:
//
// Name                 Index   Mask Register SysValue  Format   Used
// -------------------- ----- ------ -------- -------- ------- ------
// no Input
//
// Output signature:
//
// Name                 Index   Mask Register SysValue  Format   Used
// -------------------- ----- ------ -------- -------- ------- ------
// no Output
cs_5_0
dcl_globalFlags refactoringAllowed | enableDoublePrecisionFloatOps
dcl_uav_structured u0, 8
dcl_uav_structured u1, 8
dcl_input vThreadID.x
dcl_temps 2
dcl_thread_group 64, 1, 1
0: ine r0.x, vThreadID.x, l(1)
1: if_nz r0.x
2:   ret 
3: endif 
4: dmov r0.xy, d(-0.161000l, 0.000000l)
5: mov r0.z, l(0)
6: loop 
7:   ige r0.w, r0.z, l(3)
8:   breakc_nz r0.w
9:   dmul r1.xyzw, r0.xyxy, d(1.001000l, 0.999000l)
10:   dadd r1.xy, -r1.zwzw, r1.xyxy
11:   store_structured u1.xy, r0.z, l(0), r1.xyxx
12:   dadd r0.xy, r0.xyxy, d(0.010000l, 0.000000l)
13:   iadd r0.z, r0.z, l(1)
14: endloop 
15: store_structured u0.xy, l(0), l(0), l(-0.000000,-0.707432,0,0)
16: store_structured u0.xy, l(1), l(0), l(0.000000,-0.702312,0,0)
17: store_structured u0.xy, l(2), l(0), l(-918250586112.000000,-0.697192,0,0)
18: ret 
// Approximately 0 instruction slots used

编辑3联系Microsoft后,(请参阅https://learn.microsoft.com/en-us/an...nrolling-a-loop-affect-the-accuracy-of-t.html),他们表示,问题更多的是关于团结。这是因为

"pragmaunroll〔(n)〕是Unity使用主题"的keil编译器;

这与驱动程序、硬件、编译器和单位相关。

本质上,与常规IEEE-754浮点相比,HLSL规范对数学运算的舍入行为有一些宽松的保证。

首先,操作是向上取整还是向下取整取决于实现。

IEEE-754需要浮点运算才能产生是与无限精确结果最接近的可表示值,称为从圆到最近的偶数。然而,Direct3D10定义了要求:32位浮点运算产生的结果在无限精确结果的最后一位(1 ULP)的一个单位内。这意味着,例如,允许硬件截断结果到32位,而不是像那样执行四舍五入到最接近的偶数导致最多一个ULP的错误。

  • 请参阅https://learn.microsoft.com/en-us/windows/win32/direct3d10/d3d10-graphics-programming-guide-resources-float-rules#32-位浮点规则

更进一步,HLSL编译器本身具有许多快速数学优化,这些优化可能违反IEEE-754浮点一致性;例如,请参见:

D3DCOMPILE_IEEE_STRICTNESS-强制严格编译,这可能不允许使用遗留语法。默认情况下,编译器会禁用不推荐使用的语法的严格性
D3DCOMPILE_OPTIMIZATION_LEVEL3-指示编译器使用最高优化级别。如果设置此常量,编译器会生成尽可能好的代码,但可能需要更长的时间。当性能是最重要的因素时,为应用程序的最终构建设置此常量。D3DCOMPILE_PARTIAL_PRECISION-指示编译器以部分精度执行所有计算。如果设置此常量,编译后的代码可能会在某些硬件上运行得更快。

  • 来源:https://learn.microsoft.com/en-us/windows/win32/direct3dhlsl/d3dcompile-constants

这对您的场景特别重要,因为如果启用了优化,循环展开的存在可以触发不断的折叠优化,从而降低代码的计算成本并更改结果的精度(甚至可能提高结果)。请注意,当发生常量折叠时,编译器必须决定如何执行舍入,这可能与硬件FPU的做法不一致。

哦,请注意,IEEE-754并没有对";附加操作";(例如sin、cos、tanh、atan、ln等);它纯粹是推荐他们。

  • 看,这是一个非常常见的情况,在英特尔集成图形上,sin被量化为4个不同的值,但在其他硬件上具有合理的精度:sin(x)在GLSL碎片着色器英特尔HD4000上仅为中等大小的输入返回4个不同值

此外,请注意,Unity并不保证着色器中的float实际上是32位浮点;在某些硬件(例如移动设备)上,它甚至可以由16位half或11位fixed支持。

高精度:浮点最高精度浮点值;通常是32位(就像来自常规编程语言的float一样)。

浮点/半浮点/固定数据类型使用的一个复杂性是PC GPU总是高精度的。也就是说,对于所有PC(Windows/Mac/Linux)GPU,在着色器中写入浮点、半浮点还是固定数据类型都无关紧要。他们总是以32位浮点精度计算所有内容。

一半和固定类型只有在以移动设备为目标时才具有相关性GPU,这些类型主要用于电源(有时性能)约束。请记住,您需要测试移动设备上的着色器,以查看是否正在运行精度/数值问题。

即使在移动GPU上,不同的精度支持也有所不同GPU系列。

  • 来源:https://docs.unity3d.com/Manual/SL-DataTypesAndPrecision.html

我不相信Unity会向开发人员公开编译器标志;对于它传递给dxc/fxc的优化,您可以随心所欲。考虑到它主要用于游戏,你可以打赌它们可以实现优化。

  • 来源:https://forum.unity.com/threads/possible-to-set-directx-compiler-flags-in-shaders.453790/

最后,检查";浮点决定论";作者Bruce Dawson,如果你想深入探讨这个话题;我要补充的是,如果你想在语言之间获得一致的结果(因为语言本身可以实现数学函数,而不是使用硬件内部函数,例如为了获得更好的精度),在交叉编译时(因为不同的编译器/后端可以以不同的方式优化或使用不同的系统库),或者当在不同的运行时运行托管代码时(例如,因为JIT可以进行不同的优化)。

最新更新