사전 컴퓨트 셰이더
개념

컴퓨트 셰이더

gabury1고친 사람 github-actions[bot]

컴퓨트 셰이더는 그래픽 카드에 그림과 상관없는 계산을 시키는 프로그램입니다. 사진 백만 화소의 밝기를 한꺼번에 올리는 일이 그런 계산입니다. 같은 계산을 수만 번 되풀이해야 할 때 그 수만 번을 한꺼번에 나눠 돌립니다. 결과는 화면이 아니라 메모리에 남습니다.

쉽고 빠른 이해

컴퓨트 셰이더는 그래픽 카드에 계산만 시키는 프로그램입니다. 사진 한 장의 화소 백만 개를 한꺼번에 밝게 만드는 일이 그런 계산입니다.

그래픽 카드 안에는 똑같은 셈을 하는 작은 연산기가 수천 개 들어 있습니다. 그림을 안 그리는 동안에도 그 수천 개를 쓰려고 계산만 하는 길을 따로 열어 둔 것입니다. 이 길이 없으면 계산을 시키려고 보이지도 않는 화면에 억지로 그림을 그려야 합니다.

어떻게 도는가:

  1. 계산할 값을 한 줄로 늘어놓아 그래픽 카드로 보냅니다
  2. 같은 프로그램을 몇 개나 돌릴지 정해 실행을 시킵니다
  3. 하나하나가 값 하나씩을 맡아 계산하고 제자리에 써 둡니다
  4. 다 끝나면 그 값들을 다시 읽어 옵니다

대가도 있습니다. 값을 보내고 받아 오는 데 시간이 듭니다. 계산이 작으면 오가는 시간이 계산 시간보다 큽니다. 나눠 준 몫마다 계산하는 방법이 달라지는 일도 잘 안 맞습니다.

상세

강당에 접어 치워야 할 의자가 천 개 있습니다. 혼자면 하나씩 차례로 천 번을 접어야 합니다. 의자마다 번호를 붙인 다음 번호표를 받은 천 명을 저마다 자기 번호 의자 앞에 세우면, 접으라는 말 한마디에 천 개가 한꺼번에 접힙니다.

GPU(Graphics Processing Unit, 그래픽 처리 장치)는 화면에 그림을 그리려고 만든 칩입니다. 화면의 점 수백만 개를 각각 무슨 색으로 칠할지 정해야 합니다. 그래서 똑같은 계산을 동시에 많이 하도록 만들어졌습니다.

그 안에는 작고 단순한 연산기가 수천 개 들어 있습니다. 앞선 비유의 천 명이 이 연산기들입니다. 이 칩을 얹은 판을 흔히 그래픽 카드라고 부릅니다.

셰이더는 그 칩에 끼워 넣는 작은 프로그램입니다. 그림을 그리는 절차 중 몇 칸은 비어 있고, 거기서 무엇을 계산할지 쓰는 쪽이 적어 넣습니다. 꼭짓점의 위치를 정하는 정점 셰이더와 점의 색을 정하는 프래그먼트 셰이더가 그렇게 들어갑니다.

컴퓨트 셰이더는 그 비어 있는 칸 밖에 따로 서는 셰이더입니다. 그려야 할 꼭짓점도 색칠할 점도 받지 않습니다. 메모리에서 값을 읽고, 계산하고, 메모리에 값을 씁니다.

그림으로 위장하던 시절

GPU 의 연산기 수천 개는 그림 말고 다른 계산에도 쓸 만합니다. 컴퓨트 셰이더가 없던 시절에는 그 힘을 쓰려고 계산할 값을 그림으로 위장했습니다. 값을 그림 파일에 색으로 적어 넣고, 보이지도 않는 화면에 그림을 그리게 한 뒤, 그려진 색을 다시 값으로 읽는 식입니다.

이 위장은 번거롭습니다. 계산 하나를 시키려고 그리기 절차 전체를 세워야 합니다. 쓸 수 있는 값의 모양도 그림이 담을 수 있는 것에 묶입니다.

컴퓨트 셰이더는 그 위장을 없앱니다. 값을 값 그대로 보내고, 계산만 시키고, 값 그대로 받아 옵니다.

그래픽스 파이프라인 밖의 실행

그림을 그리는 절차는 정해진 순서로 돕니다. 꼭짓점을 받아 위치를 정하고, 그 사이를 채워 화면의 점으로 바꾸고, 점마다 색을 칠합니다. 이 순서를 그래픽스 파이프라인이라고 부릅니다.

컴퓨트 셰이더는 이 순서에 끼지 않습니다. 앞 단계에서 무엇을 받지도 않고 뒤 단계로 무엇을 넘기지도 않습니다. 실행을 시키면 혼자 돌고 끝납니다.

flowchart TD
    subgraph pipe["그래픽스 파이프라인"]
        V["꼭짓점"] --> VS["정점 셰이더"]
        VS --> RA["점으로 바꾸기"]
        RA --> FS["프래그먼트 셰이더"]
        FS --> SC["화면"]
    end
    CS["컴퓨트 셰이더"] --> MEM["메모리"]
    MEM --> CS

묶어 놓은 사슬 안에서는 앞 칸이 만든 것을 뒤 칸이 받습니다. 컴퓨트 셰이더에는 그렇게 이어진 앞뒤가 없습니다. 값을 메모리에서 읽고 메모리에 쓰는 고리 하나가 전부입니다.

그래서 그리기와 섞어 쓸 때는 순서를 쓰는 쪽이 정합니다. 먼저 컴퓨트 셰이더로 값을 계산해 두고, 그 값을 그리기에 쓰는 식입니다. 풀잎 수만 개가 바람에 흔들린 위치를 미리 계산해 두는 일이 그렇습니다.

셰이더 호출과 자기 번호

셰이더가 한 번 도는 것 하나를 셰이더 호출이라고 부릅니다. GPU 쪽에서는 스레드라고도 합니다. 실행을 시킬 때 몇 번을 돌릴지 쓰는 쪽이 정합니다. 계산할 값이 백만 개면 백만 호출을 시키는 식입니다.

호출마다 도는 프로그램은 똑같습니다. 다른 것은 자기 번호 하나뿐입니다. 호출은 그 번호를 보고 자기가 맡을 값을 골라냅니다.

main() {                // 한 호출의 몸통
  i = 내 번호            // 0 · 1 · 2 …
  값[i] = 값[i] * 2      // 자기 칸 하나만
}

세 줄이 전부입니다. 반복문이 하나도 없는데 값 백만 개가 모두 두 배가 됩니다. 컴퓨트 셰이더를 쓴다는 것은 반복문을 호출의 수로 옮겨 적는 일입니다.

flowchart TD
    subgraph calls["셰이더 호출"]
        T0["0번 호출"]
        T1["1번 호출"]
        T2["2번 호출"]
        TN["나머지 호출"]
    end
    subgraph vals["메모리에 늘어선 값"]
        V0["0번 칸"]
        V1["1번 칸"]
        V2["2번 칸"]
        VN["나머지 칸"]
    end
    T0 --> V0
    T1 --> V1
    T2 --> V2
    TN --> VN

호출의 번호가 곧 맡을 칸의 번호입니다. 짝이 겹치지 않으니 호출끼리 서로의 값을 건드릴 일이 없습니다.

작업 그룹

호출은 낱개로 흩어져 돌지 않습니다. 정해진 수만큼 묶여서 함께 돕니다. 이 묶음을 작업 그룹이라고 부릅니다. 묶음 하나에 호출을 몇 개 담을지도 셰이더를 쓸 때 적어 둡니다.

묶음에는 그 안의 호출들만 같이 쓰는 작은 메모리가 딸려 있습니다. 이 메모리는 바깥 메모리보다 가깝습니다. 옆 호출이 이미 읽어 온 값을 다시 읽지 않고 나눠 쓸 때 씁니다.

묶음 안에서는 서로 기다릴 수도 있습니다. 호출들이 자기 몫을 다 쓸 때까지 멈춰 섰다가 함께 다음 줄로 갑니다. 이렇게 시점을 맞추는 장치를 배리어라고 합니다.

flowchart TD
    D["실행 요청 한 번"]
    D --> G1["작업 그룹"]
    D --> G2["작업 그룹"]
    D --> G3["나머지 작업 그룹"]
    subgraph one["작업 그룹 하나를 열어 본 모습"]
        I1["셰이더 호출"]
        I2["셰이더 호출"]
        I3["셰이더 호출"]
        S["그룹 안에서만 함께 쓰는 메모리"]
        B["함께 멈춰 서는 배리어"]
        I1 --- S
        I2 --- S
        I3 --- S
        I1 --- B
        I2 --- B
        I3 --- B
    end

묶음을 나누는 까닭은 이 안쪽 메모리와 배리어 때문입니다. 묶음이 다르면 둘 다 안 통합니다.

버퍼와 메모리 왕복

컴퓨트 셰이더에는 화면 같은 정해진 출구가 없습니다. 대신 버퍼를 읽고 씁니다. 버퍼는 값을 한 줄로 늘어놓은 메모리 덩어리입니다.

버퍼는 GPU 쪽 메모리에 있습니다. 프로그램이 쥔 값은 CPU(Central Processing Unit, 중앙처리장치) 쪽 메모리에 있습니다. 계산을 시키기 전에 건너편으로 옮겨야 합니다. 결과도 같은 길을 거꾸로 건너옵니다.

sequenceDiagram
    participant 프로그램
    participant 버퍼
    participant GPU
    프로그램->>버퍼: 계산할 값을 채운다
    프로그램->>GPU: 몇 호출 돌릴지 정해 실행을 시킨다
    GPU->>버퍼: 호출마다 자기 칸을 읽고 쓴다
    Note over GPU: 수천 호출이 같은 프로그램을 함께 돈다
    GPU-->>프로그램: 다 끝났다고 알린다
    프로그램->>버퍼: 결과를 읽어 온다

그림의 첫 줄과 마지막 줄이 값을 옮기는 구간입니다. 가운데만 계산입니다. 옮기는 구간이 계산보다 오래 걸리는 일이 흔합니다. 컴퓨트 셰이더를 쓸지 말지는 대개 이 구간이 정합니다.

잘 맞는 계산과 안 맞는 계산

같은 계산이 수만 건 있어도 다 잘 도는 것은 아닙니다. 갈림길은 두 가지입니다. 호출마다 가는 길이 갈리는가, 값을 어떤 순서로 읽는가입니다.

연산기 수천 개는 명령을 하나씩 따로 받지 않습니다. 여러 개가 한 명령을 같이 받습니다. 그래서 한데 묶인 호출들은 같은 줄을 같은 때에 밟습니다.

조건문에서 길이 갈리면 한쪽 길을 밟는 동안 다른 쪽 호출들이 멈춰 서서 기다립니다. 그다음 반대쪽이 돌 때는 이쪽이 기다립니다. 이렇게 갈려서 손해 보는 것을 분기 발산이라고 합니다.

flowchart TD
    A["같은 줄을 함께 밟는다"]
    A --> C["조건문에서 길이 갈린다"]
    C --> T["참인 호출만 돈다 · 거짓인 호출은 멈춰 기다린다"]
    T --> F["거짓인 호출만 돈다 · 이번엔 참인 호출이 기다린다"]
    F --> M["다시 같은 줄을 함께 밟는다"]

두 갈래가 나란히 돌지 않고 차례로 도는 것이 그림에서 보입니다. 갈린 구간에서는 한쪽이 도는 동안 다른 쪽이 놉니다. 그래서 걸리는 시간이 두 갈래를 더한 만큼이 됩니다.

값을 어떤 순서로 읽느냐도 걸리는 시간을 바꿉니다. 이웃한 호출이 이웃한 칸을 읽으면 메모리에서 한 덩어리로 가져옵니다. 흩어진 칸을 제각기 읽으면 그만큼 여러 번 가져와야 합니다.

그래서 잘 맞는 계산은 모양이 비슷합니다. 값이 많고, 값마다 하는 일이 같고, 값끼리 서로 기다릴 일이 적은 계산입니다. 사진 보정과 행렬 곱, 입자 수만 개의 위치 갱신이 그렇습니다.

쓸 때와 안 쓸 때

백엔드 코드에서 컴퓨트 셰이더를 직접 적을 일은 드뭅니다. 그래도 PyTorch·TensorFlow 같은 라이브러리 안쪽에서는 늘 돌고 있습니다.

그래픽 카드에 그림 아닌 계산을 시키는 일을 GPGPU(General-Purpose computing on GPU, 범용 GPU 연산)라고 부릅니다. 기계 학습 라이브러리가 수천 개의 곱셈과 덧셈을 GPU 로 내려보낼 때 그 밑에서 도는 것이 컴퓨트 셰이더입니다.

따져 볼 만한 때는 정해져 있습니다. 같은 계산이 수만 건 넘게 있고, 건마다 남을 기다릴 일이 없고, 그 계산이 전체 걸리는 시간의 큰 몫을 차지할 때입니다.

안 맞는 때도 분명합니다. 건수가 적으면 값을 옮기는 시간이 계산 시간을 넘습니다. 건마다 조건이 갈리거나 앞 건의 결과를 뒤 건이 써야 하면 수천 호출을 모아도 같이 못 돕니다.

요청 하나가 데이터베이스를 기다리는 서비스라면 기다리는 쪽이 훨씬 큽니다. 이 일을 따져도 줄어드는 시간이 눈에 안 띕니다.

관련 항목

같은 칩 위에서 함께 도는 다른 셰이더

셰이더 · 정점 셰이더 · 프래그먼트 셰이더 · 지오메트리 셰이더 · 테셀레이션 제어 셰이더 · 테셀레이션 평가 셰이더 · 메시 셰이더 · 태스크 셰이더 · 레이 생성 셰이더

컴퓨트 셰이더를 돌리는 장치와 그 속의 구성 요소

GPU · CPU · 코어 · 스트리밍 멀티프로세서 · 워프 · VRAM · 메모리 대역폭 · SIMD

컴퓨트 셰이더가 값을 읽고 쓰는 저장소

버퍼 · 스토리지 버퍼 · 유니폼 버퍼 · 텍스처 · 공유 메모리 · 스테이징 버퍼 · 메모리 매핑

컴퓨트 셰이더 한 번의 실행을 이루는 구성 요소

컴퓨트 파이프라인 · 디스패치 · 작업 그룹 · 배리어 · 메모리 배리어 · 커널 · 커맨드 버퍼 · 큐

컴퓨트 셰이더를 실행시키는 그래픽스 API

Vulkan · WebGPU · OpenGL · Direct3D 11 · Metal · OpenCL · CUDA

컴퓨트 셰이더를 적는 셰이더 언어

GLSL · HLSL · WGSL · MSL · SPIR-V

컴퓨트 셰이더로 푸는 계산 종류

GPGPU · 병렬 계산 · 기계 학습 · 행렬 곱 · 입자 시뮬레이션 · 이미지 필터 · 프리픽스 합 · 리덕션 · 정렬 · 컬링

컴퓨트 셰이더에서 자주 나는 성능 문제

분기 발산 · 점유율 · 경쟁 상태 · 데이터 레이스 · 캐시 미스 · 동기화

다른 이름: compute shader · 컴퓨트 쉐이더 · 계산 셰이더