Notice
Recent Posts
Recent Comments
Link
반응형
«   2026/02   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
Archives
Today
Total
관리 메뉴

RenderLog

[번역]Understanding numerical precision 본문

Graphics/참고자료

[번역]Understanding numerical precision

scahp 2020. 6. 30. 18:10

개인 공부용으로 번역한 거라 잘못 번역된 내용이 있을 수 있습니다.

또한 원작자의 동의 없이 올려서 언제든 글이 내려갈 수 있습니다.

출처 : https://developer.arm.com/solutions/graphics-and-gaming/developer-guides/learn-the-basics/understanding-numerical-precision/single-page

 

Overview

이 안내서는 GPU에 사용가능한 서로다른 수치 정밀도의 레벨을 알아봅니다. 여기서 더 높은 정밀도 타입 대신 좁은 데이터 타입을 사용하는 것의 이점에 대해서 설명합니다.

이 안내서는 쉐이더 개발과 그들의 지식을 더 높이고 그들의 쉐이더 성능을 더 최적화 하려는 응용프로그램 개발자들을 위해 만들어졌습니다.

당신이 이 안내서를 다 봤을때, 16 float-point 값과 32 float-point 값, 쉐이더 정밀도, 반올림 값, 저 정밀도의 장단점, 텍스쳐 좌표 그리고 어떻게 정밀도 손실을 완화하는가를 이해할 것입니다.

Numerical Precision

Data-plane 프로세싱는 큰 양의 데이터와 계산을 가능하면 효율적이게 처리하는 것입니다. 프로세싱 경로 내내 좁은 데이터 타입을 알고리즘에 사용하는 것은 효율을 증가시키는데 사용된 주요 트릭중 하나 입니다.

그래픽스 알고리즘에서, 렌더링의 최종 결과 색상은 컬러 채널당 8 bit 만 사용하는 RGBA8 서피스일 가능성이 높습니다. 그러므로, FP32 부동소수점 값을 사용할 필요는 없습니다.

FP16 값은 컬러 계산에서 가능한 대안입니다. 많은 계산 문제 영역에서, 더 작은 데이터 타입이 되는 것이 가능합니다.

Shader precision

OpenGL ES와 Vulkan 그래픽스 표준은 그들 자신의 데이터 타입 정의가 있습니다. 변수들은 구현에서 사용할 수 있는 최소한의 정밀도를 정의하는 정밀도 한정자(precision qualifier)를 사용하여 정의되어 집니다.

그러나, 구현에서는 필요하면 더 큰 정밀도 변수로 바꿀 수 있습니다. 아래의 테이블에서 보는것 처럼, 부동소수점 값 상태에 대한 OpenGL ES 쉐이더 언어의 사양은 다음과 같습니다:

 DeclarationRangeMagnitudePrecision
 lowp [-2,      2] [2^-8,  2] Absolute 2^-8
 mediump [-2^14, 2^14] [2^-14, 2^14] Relative 2^-10
 highp [-2^62, 2^62] [2^-62, 2^62]Relative 2^-16

lowp float 값은 10-bit 의 고정소수점를 사용하여 저장됩니다, mediump float 값은 16-bit 부동소수점를 사용하여 저장됩니다, 그리고 highp float 값은 32-bit 부동소수점를 사용하여 저장됩니다. 그러나 출력 결과는 기반 GPU에 따라 다릅니다.
Mali GPU는 lowp와 mediump 변수를 구분하지 않습니다. 이것은 둘 모두가 16-bit 데이터 타입에 대응된다는 의미입니다, 그리고 highp 변수는 32-bit 데이터 타입에 대응됩니다.
주목할점: 오래된 Mali-400 GPU 시리즈는, Utgard 아키텍쳐에 기반합니다, 프래그먼트 쉐이더에서 highp를 지원하지 않습니다. 그러므로, Mali-400 시리즈 GPU는 모든 변수는 16-bit 변수로 다루어 집니다.

Rounding modes

부동소수점 정밀도 외에도, 결과 값이 정확히 표현되지 않는 경우 어떻게 부동소수점 값이 반올림되어지는지 고려해봅시다.
대략 3개의 큰 부동소수점 반올림 가능한 카테고리가 있습니다:

  • 가까운 수로 반올림
  • 0 방향으로 반올림
  • 0에서 멀어지는 방향으로 반올림

가까운 쪽으로 반올림하는 것을 , 0.5 ULP 까지, 0으로 혹은 0에서 부터 먼곳으로 반올림 하는 경우, 1ULP 까지, 와 비교했을 때 가장 오차가 적습니다. 그러나, 가까운 쪽으로 반올림하는 경우가 하드웨어에서 지원하기에 조금 더 비쌉니다. 수의 정밀도에서, OpenGL ES는 구현을 빡빡하게 하지 않습니다.
Mali GPU는 가까운 쪽으로 반올림이 기본입니다, 비록 몇몇 OpenCL (역주 : 오타 인거 같은데 OpenGL 일듯)버젼이 이 구현을 오버라이드 할 수 있지만요.

The pros and cons of lower precision

저 정밀도 데이터 타입은 다양한 효율적 이점을 제공합니다:

  • Narrower 산술 단위 대한 하드웨어는 더 적습니다, 그리고 더 적은 트랜지스터 전환만 필요합니다. 이것은 연산에서 적은 에너지를 사용한다는 의미입니다.
  • Narrower 연산의 벡터를 함께 묶어서 전체 성능을 향상시킬 수 있습니다. 예를들어, 한개의 FP32 연산 대신에 2개의 FP16 을 제출하는 것이 가능합니다.
  • 메모리에서 Narrow 데이터는 더 적은 저장공간을 요구하며 비싼 외부 DDR 메모리의 필요를 줄여줍니다, 동시에 더 많은 데이터를 데이터 캐쉬와 레지스터 저장공간에 동시에 저장할 수 있게 해주며 성능을 향상시킵니다.

그러나, Narrower 데이터 타입은 오직 작은 범위의 숫자만 표현할 수 있다는 것은 트레이드 오프 입니다.  다음과 같은:

  • 부동소수점 타입과 정수 타입은 줄어든 동적 범위에 의해 고통받습니다. 예를들어, FP32 float는 2^62 범위 까지 값을 저장할 수 있습니다. 이것과 비교해서 FP16 float 는 2^14 까지만 저장할 수 있습니다.
  • 부동소수점 타입은 주어진 동적 범위에서 줄어든 정밀도로 부터 역시 고통받습니다. FP32 값은 24개의 fractional bits를 제공합니다, 그리고 FP16 floqt는 11개의 fractional bits를 제공합니다.

그러므로, 우리는 그들이 불완전한 정밀도를 제공해서 렌더링 에러를 만들지 않는한 narrow 타입을 사용할 것은 권장합니다.

일반적으로, 특정 그래픽스 어플리케이션은 FP 타입을 혼합해서 사용해야만 합니다. FP16 으로 충분한 경우가 많습니다, 그러나 그것이 충분하지 않는 경우도 많습니다, 그래서 FP32 를 대신 사용해만 합니다.

FP16 값은 에너지효율과 성능이 FP32에 비해서 2배이기 때문에, FP16 값은 어플리케이션에서 사용가능한 경우 써야합니다. 이것은 mediump가 불완전한 경우를 고려하는 것이 더 쉽다는 의미입니다.

Vertex positions

출력 position의 정확성을 보장하고 Vertex Shader에서 Vertex position의 안정성을 보장하기 위해서, 우리는 highp를 사용할 추천합니다. 항상 highp를 positions의 입력, 매트릭스의 변환, 그리고 라이팅에 대한 거리 기반 계산 으로 사용하세요.

Texture coordinates

텍스쳐는 0~1 사이의 UV 좌표로 다뤄집니다. FP16 좌표는 2048 중 1개의 부분의 정확도와 함께 11 fractional bits를 줍니다. 이것은 1440p (2560x1440 픽셀) 렌더링 같은 일반 텍스쳐 크기는 GL_NEAREST 필터링이라도 정확하게 다룰 수 없다는 뜻입니다.

많은 게임들이 이것 보다 더 작은 텍스쳐를 사용합니다, 512x512 혹은 1024x1024 픽셀과 같은, 그러나 많은 게임은 또한 GL_LINEAR 필터링을 사용합니다.

필터링 중에 부드러운 선형 보간과 sub-texel 정확도를 안정되게 다루기 위해서, 우리는 당신이 적어도 16개의 sub-texel 인덱스를 사용하는 것을 추천합니다. 이것은 몇몇 더 작은 텍스쳐에는 FP16 이 충분하지 않다는 의미입니다.

두가지 이유로, 우리는 텍스쳐 좌표에 대해서 FP32 보간 정밀도를 보장하기 위해 프레그먼트 쉐이더에서 입력변수를 highp로 사용하기를 추천합니다. 그러나 만약 텍스쳐의 각 차원의 너비가 2048 텍셀이거나 혹은 같다면, Vertex 쉐이더에서 텍스쳐의 입력과 출력 좌표를 저장하기 위해서 더 높은 정밀도가 필요하진 않습니다.

입력 Attribute 버퍼를 GL_HALF_FLOAT로 저장하는 것과 Vertex 쉐이더로 부터 mediump 출력을 쓰는것은 좌표를 메모리에 저장하는데 필요한 메모리 대역폭을 최소화 합니다, 그리고 highp 입력으로 프래그먼트 쉐이더에서 입력을 받는 것은 높은 정밀도 보간을 제공합니다.

주목할점: Mali-400 시리즈 GPU는 프래그먼트 쉐이더 math 유닛에서 highp 연산을 지원하지 않습니다. 그러나 400-series GPU는 varying interpolator와 텍스쳐 샘플 유닛 사이에 더 높은 정밀도 패스를 포함합니다. 텍스쳐 좌표에 대해 추가 정밀도를 잃는 것을 피하려면, 보간되어진 varying 값은 직접 texture2D 함수 호출에 전달되어야만 합니다. 우리가 사용하기전에 좌표에서 특정 산술 계산을 하면 FP16 정밀도로 떨어집니다, 그리고 이후에 샘플 위치의 정확도가 떨어집니다.

Depth samplers

대부분의 최신 컨텐츠는 24-bit unsigned normalized integers나 32-bit float 깊이 버퍼를 사용할 것입니다. 텍스쳐로 데이터 정밀도를 잃지 않으면서 데이터를 샘플하기 위해서, 텍스쳐 샘플러는 highp 샘플러여야 합니다.

32-bit per channel texture formats

OpenGL ES 3.0는 부동소수점과 정수 데이터 타입에 대해  텍스쳐 채널당 32-bit 를 도입합니다. 더 넓은 데이터 폭이 주어진 경우, highp 샘플러 외에 다른 것을 사용할 때 데이터 잘림이 발생할 것입니다.

반면에 32-bit 텍스쳐 채널이 사용가능 하지만 우리는 높은 메모리 대역폭과 에너지 효율 비용 때문에 사용을 추천하지 않습니다.

32-bit render targets

OpenGL ES 3.0 과 OpenGL ES 3.2 는 채널당 32-bit의 출력 framebuffer attachments를 도입합니다, integer 데이터는 ES 3.0에서, 그리고 부동소수점 데이터는 ES 3.2 데이터에서. 데이터 너비가 큰것이 주어질 때, 쉐이더에서 계산과 출력에 highp 외에 다른것을 사용하면 데이터가 잘립니다.

32-bit 채널 출력 framebuffer는 사용가능하지만, 우리는 높은 메모리 대역폭과 에너지 효율 비용 때문에 사용을 추천하지 않습니다.

Debugging

정밀도 이슈를 디버깅 하는 것은 어려울 수 있습니다, 왜냐하면 어떤 계산이 오버플로우 되었는지 정확히 알기 어렵기 때문입니다. 몇몇 trial-and-error 디버깅 기술이 범인을 찾기 위해서 필요합니다.

Forcing highp in shaders

highp 정밀도를 사용하는 드로우콜에서, 쉐이더의 모든 변수들을 강제하고 그 이슈가 해결되는 것을 보는 것이 그들의 정밀도를 확인하는 가장 빠른 방법입니다. 만약 이슈가 해결되었다면, 당신은 문제가 있는 계산을 찾을 때 까지 낮은 정밀도를 도입해볼 수 있습니다. 그런 후 완화 전략을 구현 할 수 있습니다.

Increasing depth precision

특정 지오메트리의 Z-fighting 문제에서, depth 버퍼의 정밀도는 , 동일 평면상에서 quantization 이슈를 피하기 위해서 충분한 depth 정밀도를 갖도록 보장하기 위해 항상 검토할 가치가 있습니다

당신은 32-bit 부동소수점 depth 버퍼(DEPTH_COMPONENT_32F)로 교체함으로써 depth 정밀도가 렌더링 이슈를 유발할 가능성 여부를 결정할 수 있습니다.

주목할점 : 우리는 16-bit depth 버퍼가 정밀도 문제를 유발하므로 사용하지 않는 것을 추천합니다.

Migrating loss of precision

기본 부동소수점 숫자의 아이디어는 저장된 fractional bit 위치가 변경 혹은 떠다니는 것입니다. 이것은 당신이 표현하려는 숫자의 크기에 기반합니다. 당신이 저장할 수 있는 정확도의 수준은 저장된 숫자의 크기 증가에 따라 감소합니다.

많은 쉐이더 산술 종류에 대해, 작은 숫자의 정확도는 중요합니다. 예를들면 다음과 같습니다: unorm color 출력들, UV 텍스쳐 좌표들 그리고 모든 값이 0.0~1.0 사이인 unit length vectors의 컴포넌트들.

이런 출력 범위의 숫자의 정밀도를 유지하는 것은 중요합니다. 그러므로, 우리는 이제 당신이 어떻게 정밀도 제한에 의해 발생하는 에러를 줄이기 위해 수학적 구성을 사용할 수 있을지 논의 해볼 것입니다.

Avoid large magnitudes

수학 연산으로 큰 규모의 숫자에서 작은 숫자로 변경될 숫자 만들기를 피하세요.

예를들어, 다음 표현식을 고려해봅시다:

glsl
float opA = 100.00;
float opB =   0.01;
float tmp = (a + b)
float result = tmp - a;

FP32 정밀도에서 실행되었을 때, 이 표현식은 예상했던 100.01 결과를 줍니다. 그러나, FP16로 수행되면, 이 표현식은 99.989를 줍니다.

원본 입력들의 규모가 서로 차이가 크기 때문에 발생합니다. 이것은 중간 tmp의 값이 100.01의 fractional part를 저장할 충분한 정밀도가 없기 때문입니다. 그래서 100의 값만 포함합니다.

그러나, 더 작은 tmp 값(저장가능 한)은 에러가 상쇄될 수 없다는 의미입니다.

정확도를 잃는 것을 피하기위해, 중간값을 보존할 방정식을 구성합니다, 그래서 그들을 최대한 최종 규모에 근접하게 합니다. 예를들어, 만약 어플리케이션에서 rotation을 sin() or cos()으로 전달한다면, 우리는 함수의 유용한 부분을 [0, 2(PI)]인 것을 알 수 있습니다. 이것 보다 더 높은 특정 값은 360도 보다 더 큰 것은 반복되어지는 회전입니다 (역주 : 360도 보다 더 커도 [0~2(PI)] 범위로 표현가능하다는 의미), 그래서 이것은 더 작은 회전(역주 : [0~2(PI)]를 뜻하는 것 같음)과 시각적으로 차이가 없습니다.

그래서 어플리케이션에서 계속 증가하는 값을 전달하는 것 보다, CPU의 회전을 [0~2(PI)] 범위로 Wrap (역주 : 딱히 뭐라고 해야할지 번역이 마땅치 않네요... 설명하면 2.1 이면 0.1로 -0.1 이면 1.9로 되는 것 처럼 0~2 범위내의 숫자로 영역을 제한하면서 초과 되는 부분은 반대쪽 숫자위치로 매핑) 시켜서 유용한 범위내에서 정밀도를 최대한 보존합니다.

이 예제에서, 만약 CPU에서 회전이 작은 범위로 wrap 되지 않았다면, 오브젝트는 결국 회전을 중단합니다. 숫자 규모를 너무 커져서 작은 회전 증가 값을 계속 더하는 것이 아무런 영향을 주지 못합니다. 왜냐하면 저장된 숫자의 정확도 문턱값 보다 낮은 증가 값이기 때문입니다.

이것은 FP16에서 빠르게 발생하지만 FP32도 결국 발생합니다.

Exploit symmetrical functions

Sign-bit 는 항상 부동소수점에서 저장됩니다. 많은 주기적 수학 함수들의 종류에서, 이것은 저장되어야 하는 숫자의 크기를 줄일 수 있기 때문에, 정확도를 증가시키는데 사용되어질 수 있습니다.

예를들어, +270 도 회전은 -90 도와 같습니다. 그래서, sin() 과 cos()으로 입력에 대해, [0, 2(PI)] 범위 대신 [-(PI), +(PI)]을 사용하는 것더 더 좋습니다. 왜냐하면 -PI 에서 +PI 범위는 최대 규모의 절반 범위이기 때문입니다, 그러므로 후자의 값이 1 비트의 정확도를 잃은 것을 보존하는 것입니다.

 

Exploit built-in functions

쉐이더 라이브러리의 빌트인 함수들은 종종 쉐이더 코드의 연산들보다 더 많은 정밀도를 보존을 하드웨어에 의해 지원받습니다.
이것의 한 예가 Fused Multiply Accumulate 연산입니다. 이 연산은 컴퓨터 어플리케이션에 일반적입니다:

glsl
float r = (a * b) + c;


만약 이 연산이 분리된 곱셈과 덧셈으로 구현되어졌다면, (a * b)는 tmp float 에 맞게 반올림 됩니다. tmp + c의 결과는 다시 반올림 됩니다, 그 결과 2 세트의 반올림 오류가 적용됩니다. 우리가 hardware fused multiply accumulate 연산을 하면, 출력값을 위한 최종 결과에 반올림만이 필요합니다. 이것은 중간 반올림 결과와 오류를 제거합니다.

Minimize memory size

Double Data Rate (DDR) 메모리 대역폭은 많은 파워를 요구합니다, 그래서 쉐이더를 리뷰하고 정밀도를 좁힐때, 역시 메모리에 저장된 vertex 속성들에 연관된 것 또한 정밀도로 좁히는 것을 기억하세요.
GL_HALF_FLOAT 속성의 지원은 OpenGL ES 3.0의 핵심 기능입니다. 만약 당신이 OpenGL ES 2.0을 사용한다면, 모든 Mali GPU는 [OES_vertex_half_float][VHF] 확장을 지원한다는 것을 기억하세요.

OpenGL ES OES Vertex Half-Float Information (역주 : 원본에 링크가 있었는데 깨진것 같네요.)

낮은 수 정밀도를 사용하는 것의 주의점은, 보통, 저 정밀도는 더 좋습니다. 그러나 타입의 변환에 비용이 들 것입니다. 그러므로, 적합한 정밀도 수준의 데이터를 로드하여서 쉐이더 코드에서 수의 변환을 최소화 하는 것이 필요합니다.

Next steps

쉐이더 사용의 효율성을 개선할 때, 언제 16fp나 32fp를 쓰게 가장 좋은가, 쉐이더 정밀도, 반올림 값, 저 정밀도의 장단점 그리고 정밀도 손실을 완화할 방법을 아는 것은 Mali GPU에서 당신의 쉐이더 성능의 최적화 노력의 핵심 요소입니다.

반응형