서울대학교 컴퓨터공학과 이재진 교수님의 "확장형 고성능 컴퓨팅" 강의를 필기한 내용입니다.

Performance factors for HW

  • Key performance factors
    • Parallelism: 한번에 몇개의 연산을 같이 하느냐
    • Frequency: 1 clock 이 얼마나 빠른지 (speed)
    • Memory bandwidth: 한번에 데이터를 얼마나 가져올 수 있는지
    • Memory latency: Memory bandwidth 에서의 “한번” 이 얼마나 빠른지
  • 이 factor 들 중에서 집중하는 factor 들은 workload 에 따라 달라진다.
  • 각 factor 들을 향상시키는 방법은 architecture 에 따라 달라질 수 있다.
  • CPU 한테 저 factor 들을 적용해 보자.
    • Parallelism: CPU 에는 core 가 여러개고 (multicore) 각각 ILP 를 지원한다.
    • Frequency: 오래걸리는 Instruction 을 기준으로 frequency 가 설정되니까 이 오래걸리는 instruction 을 multi-step 으로 나눠서 frequency 를 올리는 방법을 해왔으나,
      • Power consumption (발열) 등의 문제로 이것을 올리는 데는 한계가 있다.
    • Memory bandwidth: DDR 은 bandwidth 가 한정되어 있다.
    • Memory latency: DDR 은 latency 가 다소 길다.
    • Memory 쪽이 좀 부실해 보이긴 하지만,
      • 어차피 CPU 의 freqency 를 올리는데는 한계가 있어서 CPU 의 속도 발전은 정체되어 있고 따라서 memory 와의 성능차이는 (비록 mem 발전속도가 느리긴 하지만) 점차 줄어들고 있다고 한다.
      • 그리고 CPU 에는 cache 가 있으니까 이 부실함이 그렇게까지 critical 하지는 않다.

GPU overview

  • 일단 Graphic Processing Unit (GPU) 는 단순 연산을 아주 빠르게 하는 core 가 엄청 많이 (많게는 몇천개까지) 들어있는 장치이다.
    • 생각해 보면 이미지는 하나의 큰 2차원 배열이고, 영상은 이것의 모음이기 때문에 이 배열을 계산하는 것이 graphic 처리의 시작과 끝일 것이다.
  • 특정 작업만 아주 빠르게 하는 HW 장치를 Accelerator 라고 하는데, 단순 연산이 아주 빠르다는 점에서 GPU 도 accelerator 에 속한다.
  • GPU 한테도 위에서의 performance factor 들을 적용해보면
    • Parallelism: 단순한 작업을 하는 core 가 매우 많게 구성된다.
    • Frequency: CPU 보다는 frequency 가 좀 낮긴 하다.
      • 근데 parallelism 이 매우 커서 그다지 흠이 아니다.
    • Memory bandwidth: HBM (High-bandwidth Memory) 를 사용해 아주 bandwidth 가 크다.
    • Memory latency: 이건 DDR 과 비슷하다고 하네
      • 근데 HW multithreading 으로 이러한 latency 를 가린다고 한다 (Latency hiding)
  • 여담으로 GPU 는 원래 게이밍 분야에서 발전해 왔지만, NVIDIA 를 필두로 복잡한 계산을 수행하는데 사용할 수 있도록 HPC 시장으로 확대되어 왔다고 한다.

Rendering

  • Rendering: 3D model 을 구성해 놓고, 그것을 2차원 이미지에 보는 방향에 따라 투사하는 것
  • 여기서 3D Model 은 3차원의 객체의 표면을 수학적으로 표현하는 것이다.
    • 그리고 이건 2차원 삼각형의 조합으로서 표현된다.

Shader

  • 아래는 Rendering 의 과정 (pipeline) 이다.

  • 3d rendering 하는 과정은 위와 같은데, 원래는 이 작업들이 각각의 accelerator 를 이용했다면
  • 지금은 많은 작업들이 programmable 하다는 것이 알려저 general purpose computation unit (CPU) 로 수행해도 비슷한 성능을 낸다.
  • 이때 Shader 라는 것은 GPU 에서 작동하며 위 pipeline 중 하나를 처리하는 프로그램을 일컫는다.

Architecture for general purpose CPU

  • CPU 의 구조를 간단하게 보면 위와 같다.
    • IFID 를 담당하는 Fetch, Decode logic
    • ALU
    • Context 는 CPU 에서 사용할 저장공간 register (Multithreading 라면 이것도 여러개)
    • OOO controller 는 OoO execution 를 위한 reservation station, reorder buffer 등
    • 그리고 나머지 Branch predictor 나 Prefetch logic, Big data cache 는 뭐 이름이 시사하는 바 그대로임
  • 참고로 어차피 transistor 밀도는 정해져 있기 때문에 (몇 나노 공정 등) CPU 크기가 크면 그만큼 복잡도도 커진다고 한다.

Shader core

  • 근데 shader 를 생각해 보면 triangle 의 pixel 을 계산하는데 이 계산이 복잡한 것도 아니고 단순히 많은 것 뿐이다.
    • Triangle 간의 data dependency 도 없다고 한다.

  • 그래서 위처럼 (1) Fetch, Decode logic (2) ALU, (3) Execution context 만 남기고 다 쳐내게 된다.

  • 여기서 share 할 수 있는 것들을 보아보면 위처럼 되고, 이것이 GPU 의 기본적인 구조
    • 하나의 instruction stream 을 빠르게 해주는 요사스러운 것은 다 빼고
    • 무적권 parallel processing 으로만 승부보는 것.
  • 여기서 Fetch, Decode logic 이 1개라는 것은 PC 가 1개고, 이 instruction 를 모든 ALU 가 실행한다는 것이다.
    • 즉, 동일한 instruction 에 데이터만 바꿔서 (각각 ALU 에서 처리될 데이터는 Execution context 에 있겠지) 한번에 저만큼을 쫙쫙 계산하는 것.
    • 따라서 SIMD 라고 할 수 있는데, NVIDIA 에서는 이 용어 말고 (아무래도 SIMD 는 보통 CPU ISA 를 의미하니깐) Single Instruction, Multiple Threads (SIMT) 라고 한다.
    • 여기서 thread 는 너가 생각하는 Thread 와 비슷하면서 좀 다르다. 이건 바로 뒤에 설명함
  • 여기에서 rendering pipeline 을 하나하나 처리하게 되는데, 따라서 pipeline 마다 GPU 를 방문하게 된다.
    • 이런 것을 Logical graphics pipeline 이라고 한다.

Thread, Warp

  • 약간 NVIDIA-specific 용어인거같은데
  • Thread 는 하나의 IF-ID-EX 단위로 GPU 내의 ALU 하나하나라고 생각하면 된다.
    • IF-ID-EX 가 하나의 “실행 흐름” 이기 때문에 Thread 라고 이름붙여진 것.
  • 그리고 이놈을 묶어서 하나의 Fetch-Decode logic 에 따라 움직이는 Thread 들을 Warp 라고 한다.

Predicated execution

  • Predicated execution 은 GPU 에서 조건문 처리하는 방법인데, 무식하게 한다: 모든 조건들을 다 계산하고, 맞는 조건만 살리는 것.
    • Branch divergence 라고도 부른다.
  • 즉, if-else 에서 if 인 경우와 else 인 경우를 모두 parallel 하게 계산하고, 나중에 if branch 가 맞다는 것이 밣혀지만 else 인 경우를 버린다
  • 근데 딱 봐도 overhead 가 커보인다: 그래서 GPU 를 쓸 때는 branch 를 줄이는 것이 좋다
    • 다만 무조건 한쪽으로만 branch 되는 경우에는 예외.

Context switch

  • CPU 에서는 context switch 를 할 때는 register-memory 간의 저장-복구 작업이 수행되는데
  • 근데 gpu 에서는 warp 가 사용할 execution context 를 여러벌 만들어서 이 overhead 를 줄인다.
    • 즉, context + shared context 를 여러벌 만들고 (= context pool) context switch 할 때 그냥 다른 context 를 사용하기만 하면 되는 방식
    • 위 경우에는 이 context 가 4벌있는 것
    • Warp 가 실행되다가 stall 이 되면 다른 warp 로 switch 되고, 이때는 data 이동 없이 그냥 contex pool 의 다른 context 를 사용하기만 하면 된다.
  • GPU 에서는 이런 구조로 인해 stall 이 되면 그냥 다른 context 로 갈아껴서 계속 연산을 진행한다.
    • 따라서 context switch 를 해줄 OS 가 따로 필요 없고, time shared 도 아니다.
  • 따라서 지원하는 warp 의 개수는 HW 적으로 fixed 되어 있다.
    • 참고로 필요한 register 사이즈에 따라 올리는 warp 는 달라질 수 있다고 한다.

Streaming Multiprocessor, SM

  • 저런 (Fetch-Decode logic, ALU 들, context 들) 을 묶어서 (NVIDIA 에서는) Streaming Multiprocessor (SM) 라고 하고, 위 그림처럼 이 SM 을 여러개 사용하는 방식을 사용
  • 참고) cosine, sine 계산은 수학적으로는 Taylor series 으로 할 수 있고, GPU 에서는 이것들을 미리 pre-calculation 해놓고 그냥 table lookup 을 한다고 한다.

  • 그래서 현대의 GPU 들은 위와 같은 형상을 띈다고 한다.
    • 저기서 SP 는 Scalar Processor 로, 그냥 ALU 의 묶음이라고 생각하면 된다.
    • 그리고 LD-ST 는 Load-Store unit 이다. 말 그대로 memory 접근하는 놈
    • 마지막으로 SFU 는 Special Function Unit 이다. 뭐 특별한 일 하나봄.

GPU Summary

  • 하나의 instruction 을 많은 ALU 가 동시에 실행하고 있다는 점에서 SIMD 와 유사하다.
  • Thread 간 sync 를 맞추는 기능은 (거의) 제공하지 않음: 왜냐면 shading 연산에서는 그럴 일이 별로 없었기 떄문에: 따라서 이런 sync 를 맞춰야 하는 코드를 작성하지 않는 것이 좋다.

Direct Memory Access (DMA)

  • DMA 가 뭔지는 알제? Memory access 해주는 HW
  • 뭐 IO 이후에 메모리에 올리는 것을 cpu 가 하는게 아니고 dma 가 하며 이것이 끝난 다음에 interrupt 거는 시스템
  • 대충 작동 과정은 다음과 같다:
    1. 일단 CPU 가 memory 의 한 곳에 DMA command 를 적는다.
      • 여기에는 뭐 memory 의 어느 주소부터 몇 byte 를 적어라 그런게 담긴다.
    2. 그리고 DMA 에다가 적은 DMA command 의 memory 주소를 보낸다.
    3. 그럼 DMA controller 가 여기로 가 command 를 읽고, 처리한다.
    4. 처리가 완료되면 DMA controller 는 CPU 에 다 됐다고 interrupt 를 건다.

DMA cache coherence

  • 위에서 알 수 있다 시피, memory 에 적힌 내용은 DMA 가 볼 수 있다. 근데 문제는 CPU 는 memory 에 바로 적지 않을 수도 있다는 것.
  • 즉, processor 가 cache 로 가져간 값이랑 dma 가 올린 값이랑 타이밍이 안맞으면 이들이 차이가 생기게 된다.
    • 가령 cache 로 올린 다음에 memory 에 dma 한 경우
    • 이러한 문제는 memory (DMA) 와 cache 간에 sync 가 맞지 않는 경우이므로 DMA cache coherence 라고 부른다.
  • 이걸 해결하는 것은
    • cache flush 를 하거나
    • caching 을 비활성화하던가 (C 에서 volatile)

Remote Direct Memory Access (RDMA)

  • RDMA Network 를 통해 remote node 의 memory 에 바로 접근하겠다는 아이디어이다.
    • Host A 의 NIC 는 요청을 host B 의 NIC 로 보내고
    • Host B 의 NIC 는 host B 의 NIC 를 통해 host B 의 DMA 에 접근해 그쪽의 memory 에 있는 데이터를 읽거나 쓴 후
    • 그 결과를 host B 의 NIC 에서 host A 의 NIC 로 받아, host A 의 DMA 에게 알려 host A 의 memory 에 쓰게 한다.
  • 이렇게 하면 remote 의 OS 개입을 최소화하게 된다.
    • 그리고 data copy 도 줄일 수 있다.
  • 이게 뭔소리냐: 원래 remote node 의 memory 에 접근하려면 다음과 같은 stack 을 탔어야 했는데

  • 이제는 이렇게 할 수 있는 것.

  • 이것은 single side communication 이다: 즉, local 에서 remote 로 일방적으로 보내는 거고 remote 의 OS 는 이것을 인지하지 못한다.
  • 이 기술은 보통 infiniband 와 같은 아주 빠른 네트워크 환경에서 사용할 수 있다고 한다.

GPU Direct

  • GPU 가 누군가와 통신하려면 원래는 CPU 의 memory 를 통해야 했는데 (즉, main memory 를 bounce buffer 로 삼는 것) 이것을 GPU 에서 바로 접근하게 해주는 것이 GPU Direct 이다.
    • 즉, DMA 를 통해서 주변장치끼리 통신하는 것을 통칭하는 것.
  • 가령 GPU 끼리 서로의 메모리에 접근한다던가

  • 위 그림처럼 DMA 가 storage 가 보낸 데이터를 main memory 가 아닌 GPU memory 로 보내도록 한다던가 (GPU Direct Storage)

  • 아니면 다른 node 의 main memory 및 GPU memory 에 RDMA 로 접근한다던가 (GPU Direct RDMA)
  • … 할 수 있다.

Collective Communication Library (CCL)

여기부터는 2024-10-09 강의임

  • 통신을 할 때, 딱 누구를 지정해서 통신 (point-to-point) 할 수도 있지만
  • Broadcast 를 해야 할 일도 있을 것이다.
  • GPU 에서 이런 기능을 제공하는 library 가 Collective Communication Library (CCL) 이다.
    • 물론 point-to-point 로도 구현이 가능하지만, 너무 비효율적이어서 새로 만들어 library 로 만들었다고 한다.
  • 기존에 CPU 간의 통신을 위해 만든 것이 MPI (Message Passing Interface) CCL 이었고, 이것을 GPU 에서 사용하기 위해 바꾼 것이 NVCCL (NVIDIA), MSCCL (Microsoft), RCCL (AMD) 등이다.
  • 여기에는 Broadcast, Scatter, Gather, All-Gather, All-to-All, Reduce, All-Reduce 가 있다고 한다.
  • GPU Direct 를 사용하여 구현되어 있고, remote node 일 시에는 RDMA 를 사용한다고 한다.

깨알 홍보

  • 이재진 교수님 랩이 만든 Thunder CCL (TCCL) 도 있다고 한다.
    • NVCCL 에서는 NVIDIA 에 특화된 NVLink 라는 통신을 사용하는데,
    • 보통은 PCIe 를 사용하니까 이것을 위해 커스터마이징한 것이 TCCL 이랜다.

Broadcast, Scatter, Gather

  • Broadcast 는 동일한 값을 다 뿌리는 것
  • Scatter 는 여러곳에서 데이터를 받아 한곳으로 모으는 것
  • Gather 는 반대로 데이터를 쪼개 여러곳으로 뿌리는 것

All-Gather

  • 는 Gather 를 하되 그 결과를 모든 놈이 동일한 copy 를 갖게 하는 것

All-to-All

  • 는 transpose 와 같이 행렬의 X, Y 축을 반전시키는 것

Reduce, All-Reduce

  • Reduce 는 여러 놈의 데이터를 Reduce 해서 한 놈한테 모으는 것

  • All-Reduce 는 Reduce 한 것을 모든 놈이 동일한 copy 를 갖게 하는 것이다.