| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 | 29 | 30 | 31 |
- ShadowMap
- SIMD
- 번역
- Nanite
- Study
- RayTracing
- unrealengine
- optimization
- Graphics
- atmospheric
- DX12
- DirectX12
- Shadow
- Wavefront
- scattering
- GPU
- hzb
- scalar
- SGPR
- texture
- VGPR
- UE5
- deferred
- shader
- vulkan
- wave
- rendering
- GPU Driven Rendering
- forward
- ue4
- Today
- Total
RenderLog
[번역]WHAT IS SHADER OCCUPANCY AND WHY DO WE CARE ABOUT IT? 본문
개인 공부용으로 번역한 거라 잘못 번역된 내용이 있을 수 있습니다.
또한 원작자의 동의 없이 올려서 언제든 글이 내려갈 수 있습니다.
출처 : interplayoflight.wordpress.com/2020/11/11/what-is-shader-occupancy-and-why-do-we-care-about-it/
WHAT IS SHADER OCCUPANCY AND WHY DO WE CARE ABOUT IT?
November 11, 2020 | Kostas Anagnostou
트위터 DM을 통해서 occupancy가 무엇인지? 왜 그것이 쉐이더 성능에 중요한지? 에 관한 좋은 질문을 받았습니다, 블로그 포스트로 나의 답변을 더 자세히 쓰고 있습니다.
먼저 몇 가지 문맥에서, GPUs, 쉐이더 프로그램이 동작하는 동안, 64와 32 픽셀 혹은 버택스 묶음이 함께 처리됩니다(AMD에서는 wavefront NVidia에서는 warps 라 부름). 그리고 그 묶음(Batch) 모두에 단일 명령어가 실행됩니다. 보통은, 메모리로 부터 가져오는 명령어는 많은 지연이 있습니다(즉, 명령어를 발행하는 것과 결과를 얻는 시간이 깁니다), 데이터를 가져오기 위해 캐시와 아마도 램에 접근해야 하기 때문입니다. 이 지연은 데이터를 기다리는 동안 잠재적으로 GPU를 stall 시킬 수 있습니다.
메모리 명령어를 마주치면 GPU는 그것을 발행하고(즉, 데이터를 요청하고) 쉐이더 프로그램의 다음 명령어를 계속해서 실행합니다. 데이터를 사용해야 할때 그것이 사용 가능 한지 멈춰서 기다립니다. 만약 데이터가 준비되어 있다면 사용하고, 그렇지 않다면 사용 가능할 때까지 실행을 멈추고 기다려야 합니다. 예제로 아래의 가상의 쉐이더를 봐주세요. 거기서는 몇몇 수학과 텍스쳐로부터 uvScale을 읽는 것을 합니다:
Texture1D Materials : register(t0);
PSInput VSMain(VSInput input)
{
PSInput result = (PSInput)0;
result.position = mul(WorldViewProjection, float4(input.position.xyz, 1));
float uvScale = Materials[MeshIndex].x;
result.uv = uvScale * input.uv;
return result;
}
이것은 아래의 ISA로 컴파일됩니다.
s_swappc_b64 s[0:1], s[0:1]
s_buffer_load_dword s0, s[20:23], 0x00
s_waitcnt lgkmcnt(0)
v_mov_b32 v0, s0
v_mov_b32 v1, 0
image_load_mip v0, v[0:3], s[8:15] unorm
s_buffer_load_dwordx8 s[0:7], s[16:19], 0x40
s_buffer_load_dwordx8 s[8:15], s[16:19], 0x60
s_waitcnt lgkmcnt(0)
v_mul_f32 v1, s4, v5
v_mul_f32 v2, s5, v5
v_mul_f32 v3, s6, v5
v_mul_f32 v5, s7, v5
v_mac_f32 v1, s0, v4
v_mac_f32 v2, s1, v4
v_mac_f32 v3, s2, v4
v_mac_f32 v5, s3, v4
v_mac_f32 v1, s8, v6
v_mac_f32 v2, s9, v6
v_mac_f32 v3, s10, v6
v_mac_f32 v5, s11, v6
v_add_f32 v1, s12, v1
v_add_f32 v2, s13, v2
v_add_f32 v3, s14, v3
v_add_f32 v4, s15, v5
exp pos0, v1, v2, v3, v4 done
v_mov_b32 v5, 0
exp param0, v5, v5, v5, off
s_waitcnt vmcnt(0)
v_mul_f32 v6, v8, v0
v_mul_f32 v0, v9, v0
exp param1, v5, v5, v5, off
exp param2, v6, v0, off, off
exp param3, v5, v5, v5, off
s_endpgm
쉐이더에서 동일 색상의 명령어로 그에 대응하는 ISA 코드를 표시하였습니다. GPU는 텍스처 로드를 프로그램의 시작에서 제출할 것이고 명령어를 계속해서 실행할 것입니다. 몇몇 지점에서, 프로그램의 끝 부분에서, uvScale을 사용해야 할 것입니다. 그 바로 전에 데이터가 도착했는지 확실히 하기 위해서 대기 명령어를 넣습니다. 이것은 데이터가 사용 가능해질 때까지 GPU가 대기해야만 하는 Stalling 명령어입니다(요약하자면, GPU는 데이터를 사용할 때 Stall 되지 요청할 때가 아님)
기다리며 낭비되는 시간을 피하기 위해서, GPU는 다른 명령을 실행하기 위해서 현재 stall 된 묶음(warp/wavefront)에서 다른 작업으로 전환하여 일할 수 있는 다른 작업을 찾으려 할 것입니다. Stall은 일반적이기 때문에, GPU는 묶음 버퍼를 사전에 준비하고, 이것을 사용 가능한 Compute Units(실행을 수행하는)에 묶음들을 할당합니다. GPU가 사전에 할당 가능한 묶음의 수는 쉐이더가 실행에 필요하다고 선언한 벡터 레지스터(VGPRs)에 종속됩니다. 이런 것들은 컴파일 시간에 결정됩니다. 디자인에 따라 각 Compute Unit의 사용 가능한 VGPRs의 수는 제한되어 있기 때문에 GPU가 스케줄 가능한 묶음의 수는 쉐이더에서 사용되어지는 VGPRs의 수에 종속됩니다. GPU 아키텍쳐로 너무 깊게 들어가는 것을 피하기 위해서, 이것을 명확하게 해주는 짧은 예제가 있습니다: AMD의 GCN 아키텍쳐에서, 실제로 실행을 수행하는 unit을, SIMD라고 불림, 256개의 벡터 레지스터의 공간만 가지고 있습니다. 더 많은 레지스터를 쉐이더 프로그램에서 사용하면, GPU는 Stall의 상황에서 전환하기 위해서 더 적은 개수의 묶음 버퍼를 SIMD를 위해 스케쥴 할 수 있습니다. (이미지는 이 GCN 프리젠테이션에서 가져옴).

마지막으로: 최대 Waves/SIMD의 양은 우리가 occupancy라 부르는 것이고, 또한 SIMD가 Stall 된 것을 위해 전환하기 위해 할당할 수 있는 묶음(Wavefronts) 버퍼의 크기입니다. GCN 아키텍처에서는 SIMD당 최대 10 묶음(Wavefronts)입니다.
실제 용어로, 예로 쉐이더에서 32 VGPRs를 할당한다면, SIMD는 GPU가 메모리 읽기를 기다리기 위해 Stall 된 상태에서 전환되기 위한 8개의 wavefront 버퍼를 가집니다. 이것은 전혀 대기하지 않을 것이기 때문에 좋습니다. 만약 다르게, 128 VGPRs 이상 할당하게 되면, 전환할 것이 없고 GPU는 쉐이더 실행을 계속하기 전에 실제로 메모리 읽기가 완료되기를 기다려야 할 것입니다, 이것은 좋지 않고 완벽하게 좋은 GPU를 낭비합니다.
이 방식으로부터, 고려해볼 2개의 질문이 더 있습니다:
- 낮은 occupancy는 항상 나쁜가?
- 높은 occupancy가 항상 좋은가?
첫 번째 질문의 답은 아니다 입니다. 만약 위의 코드에서 알아챘다면, 쉐이더 컴파일러는 메모리를 요청하는 명령어와 사용하는 명령어 사이에 가능한 많은 명령어가 들어가도록 명령어를 재배치하여 쉐이더가 길어집니다. 이것은 메모리를 사용하려고 할 때 이미 로드되어 있을 만큼 명령어를 충분히 채우도록 관리되는 경우 일 것입니다. 그래서 stall이 전혀 필요 없을 것입니다(그리고 다른 묶음으로 전환도). 요약하면 쉐이더 최적화를 시작하기 위해서 낮은 occupancy 지표에만 의존하지는 않습니다. 메모리 읽기(텍스처 읽기)로 발생하는 stall 같은 병목을 먼저 확인하세요. 만약 그것이 높다면 당신의 쉐이더 프로그램은 occupancy를 높여서 이득을 얻을 수 있을 것입니다(보통 사용하는 VGPRs의 수를 감소시켜서)
두 번째 질문의 답은 역시 아니다 입니다.만약 쉐이더 프로그램이 많은 메모리 읽기 명령과 많은 메모리 트래픽이 필요하다면, 큰 활성 묶음(high occupancy) 버퍼는 제한된 캐시 리소스와 싸우게 될 것입니다, 각 묶음은 잠재적으로 다른 묶음/명령어에 속한 데이터의 캐시라인을 무효화합니다. 당신의 어플리케이션과 플랫폼 내에서 몇 가지 프로파일링을 통해서 좋은 균형을 얻는 방법을 결정해줍니다.
'Graphics > 참고자료' 카테고리의 다른 글
| [번역+설명추가] Spherical Mapping (0) | 2020.11.29 |
|---|---|
| [번역]Real-Time Computation of Dynamic Irradiance Environment Maps (0) | 2020.11.26 |
| [번역]A Better Way to Scalarize a Shader (0) | 2020.10.31 |
| [번역] Effective Water Simulation from Physical Models (0) | 2020.09.22 |
| [번역]Real-Time Atmospheric Scattering (0) | 2020.08.22 |