AEGLOSLAB 엔비디아 기술 스택

제2부 CUDA 프로그래밍

2.5 메모리 모델 II — 전송 최적화

2.5.1 이 장에서 배우는 것 / 선수 지식

CUDA 커널이 빠르게 실행된다고 프로그램이 빠른 것은 아니다. 많은 경우 실제 병목은 호스트(CPU)와 디바이스(GPU) 사이 데이터 이동(PCIe)이다. 이 장은 그 전송을 최적화한다 — 전송 시간 산수, pinned 메모리, 통합 메모리 튜닝, zero-copy. 복사와 연산을 겹치는 비동기 오버랩은 개념까지만, 실제 패턴은 2.10.

배우는 것:

  • 호스트-디바이스 전송이 왜 PCIe에서 병목인지, PCIe 대역폭으로 전송 시간을 산수로 추정한다.
  • 핀드(pinned) 메모리가 왜 빠르고 왜 비동기 복사의 전제인지.
  • 통합 메모리를 cudaMemPrefetchAsync·cudaMemAdvise로 튜닝한다.
  • 제로카피(zero-copy) 가 언제 이득이고 언제 독인지.

선수 지식: 1.9~1.10(HBM 대역폭·메모리 바운드), 2.1~2.3(커널·실행 구성), 2.4(메모리 공간). 비동기 오버랩은 2.10.

정직성: PCIe 대역폭·통합 메모리 동작은 GPU 세대·PCIe·드라이버·OS 의존 — 시점·플랫폼 확인 필요.

2.5.2 왜 데이터 이동이 문제인가

GPU 내부 대역폭(HBM·온칩)은 TB/s 단위인데, GPU 밖에서 안으로 데이터를 넣는 경로(PCIe)는 한~두 자릿수 느리다.

PCIe / NVLink — 병목 수십~수백 GB/ s 시스템 DRAM (수십~100+ GB/s) CPU GPU SM 레지스터 / L1 / 공유 (수 TB/s) HBM (수 TB/s)
  • 데이터를 한 번 적재해 두고 GPU에서 여러 번 재사용하면 PCIe 비용이 분할상환된다.
  • 매 연산마다 호스트로 내렸다가 다시 적재하면 PCIe가 병목 — 초보 CUDA 코드가 CPU보다 느린 가장 흔한 이유다.

루프라인(1.10) 관점: 연산량 대비 PCIe 전송량이 낮으면, 커널을 아무리 최적화해도 PCIe가 천장이다.

2.5.2.1 전송 시간을 산수로

PCIe 세대별 이론 단방향 대역폭(x16, 시점 의존·확인 필요):

PCIe 세대x16 단방향(이론)
Gen3약 16 GB/s
Gen4약 32 GB/s
Gen5약 64 GB/s

예 1 — $4096\times4096$ FP32 = $4096\cdot4096\cdot4 \approx67.1 \,\mathrm{MB}$. Gen4(실측 ~25 GB/s 가정): $67.1 \mathrm{MB} / 25 \,\mathrm{GB/s} \approx2.7 \,\mathrm{ms}$.

예 2 — 가치 판단. 같은 행렬로 GEMM($2N^3\approx1.37\mathrm{e}11 \,\mathrm{FLOP}$)을 50 TFLOPS에 실행하면 $\approx2.7 \,\mathrm{ms}$. 입력 두 장 전송(~5.4 ms) 대비 연산도 비슷한 자릿수 — 데이터를 한 번 적재해 여러 연산을 묶지 않으면 전송이 연산만큼 비용이 크다.

예 3 — 비효율 패턴. 벡터 덧셈(N=10⁷, FP32). 전송 $3\cdot40 \mathrm{MB} = 120 \,\mathrm{MB} \to\sim4.8 \,\mathrm{ms}$. 연산 $10^7 \mathrm{FLOP} / 5\mathrm{e}13 \approx0.2 \,\mu\mathrm{s}$. 전송이 연산보다 2만 배 비용이 크다. 이런 연산은 GPU에 단독으로 이전할 가치가 없다 — 더 큰 파이프라인의 일부로 데이터가 이미 GPU에 있을 때만 의미.

2.5.3 호스트-디바이스 전송 — pageable vs pinned

2.5.3.1 pageable의 숨은 복사

malloc/new로 잡은 메모리는 pageable — OS가 언제든 디스크로 스왑할 수 있다. GPU DMA 엔진은 물리 주소가 고정된 메모리만 안전하게 읽으므로, 드라이버는 staging 복사를 삽입한다:

pageable H2D (cudaMemcpy) CPU가 staging 복사 DMA · PCIe pageable 버퍼 드라이버 pinned staging HBM

추가 호스트 복사 한 번이 개입한다. 그래서 pageable 전송은 (1) 느리고, (2) staging이 끝나야 DMA가 시작되므로 실질적 비동기가 불가능 — cudaMemcpyAsync를 pageable 버퍼에 써도 동기적으로 동작한다.

2.5.3.2 pinned(page-locked) 메모리

cudaMallocHost(또는 cudaHostAlloc)로 잡은 메모리는 OS가 스왑하지 못하게 고정된다. DMA 엔진이 직접 읽으니 staging 복사가 사라진다:

pinned H2D (cudaMemcpyAsync) DMA · PCIe · 실질 비동기 pinned 버퍼 HBM

효과: 더 높은 PCIe 대역폭(staging 제거 → 이론치에 근접), 실질적 비동기 가능(복사와 커널·다른 복사를 겹침, 2.10). 단 물리 메모리를 고정하므로 과하게 잡으면 시스템 전체 저하, 할당이 malloc보다 느리니 재사용한다.

float* h_pinned;
cudaMallocHost(&h_pinned, bytes);   // page-locked
// ... 사용 ...
cudaFreeHost(h_pinned);

2.5.4 통합 메모리 튜닝 — prefetch와 advise

cudaMallocManaged 포인터는 호스트·디바이스가 같은 주소로 접근하고, 런타임이 페이지를 자동 마이그레이션한다(2.2). 장점은 생산성, 단점은 페이지 폴트 마이그레이션이 보이지 않는 비용 — 잘못 쓰면 PCIe를 오가며 스래싱한다. 기본 설정으로 쓰면 편하지만 빠르게 쓰려면 힌트를 줘야 한다.

  • cudaMemPrefetchAsync(ptr, bytes, dstDevice, stream) — 미리 옮긴다. 커널이 폴트로 한 장씩 끌어오는 대신 큰 단위를 한 번에 마이그레이션 → 폴트 제거. dstDevice에 GPU id 또는 cudaCpuDeviceId.
  • cudaMemAdvise(ptr, bytes, advice, device) — 접근 패턴 힌트(데이터 자체를 옮기지는 않음).
    • SetReadMostly — 여러 프로세서가 읽기만 → 마이그레이션 대신 복제.
    • SetPreferredLocation — 선호 위치 지정 → 다른 쪽이 접근해도 가급적 거기 둠.
    • SetAccessedBy — 자주 접근하는 프로세서에 매핑을 만들어 폴트 회피.

통합 메모리 동작·성능은 GPU 아키텍처·OS·드라이버에 강하게 의존(페이지 폴트 마이그레이션 지원 여부, Grace-Hopper 같은 NVLink-C2C 일관성 시스템에서의 동작). 시점·플랫폼 의존, 확인 필요.

2.5.5 제로카피(zero-copy / mapped pinned)

cudaHostAlloc(&p, bytes, cudaHostAllocMapped)로 잡은 pinned 메모리를 디바이스 포인터로 매핑하면, 커널이 호스트 메모리를 PCIe 너머로 직접 읽고 쓴다(복사 없음).

  • 이득: 정확히 한 번 접근하고 마는 데이터, 또는 데이터의 극히 일부만 GPU가 보는 경우.
  • 독: 커널이 반복 접근하면 매 접근이 PCIe를 타므로 글로벌(HBM)보다 자릿수 느리다. "복사를 없앴다"가 곧 "빠르다"가 아니다.
  • 적합처: 통합 메모리가 없거나 부적합한 환경, 통합 칩(Jetson, 9.1)처럼 CPU·GPU가 물리 메모리를 공유해 PCIe가 없는 경우.

2.5.6 워크스루 — pageable vs pinned 대역폭

// bw.cu — 빌드: nvcc -O2 -o bw bw.cu
#include <cstdio>
#include <cuda_runtime.h>
#define CK(x) do{cudaError_t e=(x); if(e){printf("err %s:%d: %s\n",__FILE__,__LINE__,cudaGetErrorString(e));return 1;}}while(0)
int main(){
  const size_t N = 256*1024*1024; const int iters = 20;
  void *d; CK(cudaMalloc(&d, N));
  void *h_page = malloc(N);                 // pageable
  void *h_pin;  CK(cudaMallocHost(&h_pin, N));  // pinned
  cudaEvent_t a,b; CK(cudaEventCreate(&a)); CK(cudaEventCreate(&b));
  auto bench = [&](void* src, const char* tag){
    CK(cudaMemcpy(d,src,N,cudaMemcpyHostToDevice));         // warmup
    CK(cudaEventRecord(a));
    for(int i=0;i<iters;i++) CK(cudaMemcpy(d,src,N,cudaMemcpyHostToDevice));
    CK(cudaEventRecord(b)); CK(cudaEventSynchronize(b));
    float ms=0; CK(cudaEventElapsedTime(&ms,a,b));
    printf("%-8s  %.2f ms/iter   %.1f GB/s\n", tag, ms/iters, (double)N*iters/(ms/1e3)/1e9);
    return 0;   // CK는 오류 시 1을 반환 → 람다 반환형(int)과 일치시킴
  };
  bench(h_page,"pageable"); bench(h_pin,"pinned");
  cudaFree(d); free(h_page); cudaFreeHost(h_pin);
  return 0;
}

예상(개략 — 절대 수치는 GPU·PCIe 세대·메인보드 의존):

pageable  ~14.x ms/iter   ~18 GB/s
pinned    ~10.x ms/iter   ~25 GB/s

읽는 법: pinned가 pageable보다 빠른 비율(여기선 ~1.4×)이 핵심이고, 절대값은 PCIe 세대 이론 대역폭에 얼마나 근접하는지로 본다(2.5.2.1과 대조). pinned가 pageable과 거의 같다면 PCIe가 이미 다른 이유로 막혔거나 통합 칩(staging 경로 없음)을 의심.

통합 메모리 + prefetch도 같은 방식으로 측정한다(Nsight Systems, 2.13): prefetch 없는 커널은 타임라인에 다수의 page fault/migration이 보이고, prefetch 있으면 마이그레이션이 커널 전 하나로 빠진다. 단 PCIe를 건너는 총량 자체는 prefetch로 줄지 않는다 — 줄이려면 데이터를 GPU에 두고 재사용(2.5.2).

2.5.7 트레이드오프 — 무엇을 어떻게 옮길까

방식코드 복잡도성능 제어적합
① pageable + cudaMemcpy가장 단순낮음(staging·동기)프로토타입, 일회성, 작은 데이터
② pinned + cudaMemcpyAsync중간높음(대역폭·오버랩)성능 경로, 반복 전송, 2.10 오버랩
③ 통합 메모리(+prefetch/advise)낮음중간(힌트로 보정)복잡 자료구조, 빠른 개발, 일관성 시스템
④ 제로카피(mapped pinned)중간상황 의존1회·부분 접근, 통합 칩(PCIe 없음)

견해: 기본은 ②. 성능 경로는 pinned + async로 가고 2.10의 스트림 오버랩으로 복사를 연산 뒤에 숨긴다. 개발 속도·복잡 자료구조가 우선이면 ③으로 시작해 프로파일러가 폴트를 보여주면 prefetch/advise로 튜닝. ①은 프로토타입까지, ④는 "왜 복사를 없애도 되는가"를 명확히 답할 수 있을 때만.

비엔비디아 정직: 통합·핀드 메모리의 대응물은 다른 가속기에도 있다(host-pinned/DMA, SoC 공유 메모리, 통합 가상 주소 공간). API는 다르지만 "PCIe(또는 그에 준하는 인터커넥트)를 건너는 비용을 분할상환하고, 고정 메모리로 DMA를 가능케 한다" 원리는 공통. NVLink-C2C로 CPU·GPU를 일관성 있게 묶는 시스템(4.8)에서는 이 장의 PCIe 병목 전제 자체가 약해진다 — 플랫폼을 먼저 확인.

2.5.8 중간 점검 (체크포인트)

  • [ ] 버퍼 크기·PCIe 세대로 H2D 전송 시간 하한을 산수하고, 같은 데이터의 연산 시간과 비교해 "이전할 가치"를 판단한다.
  • [ ] malloc→cudaMallocHost가 왜 빨라지는지(staging 제거)와 왜 비동기의 전제인지 설명한다.
  • [ ] 통합 메모리의 페이지 폴트 스래싱 원인을 짚고 prefetch/advise로 고친다.
  • [ ] 제로카피가 1회·부분 접근에서만 이득임을 안다.

증상: pinned가 pageable과 차이 없음 → PCIe가 다른 곳에서 막혔거나 통합 칩. prefetch를 넣어도 느림 → 매 반복 CPU가 데이터를 다시 만져 마이그레이션 반복(스래싱), 소유권을 한쪽으로 고정했는지 확인.

2.5.9 흔한 함정

  • 과도한 H2D/D2H 전송. 루프 안 매 반복 복사 → PCIe가 전부 점유한다. 데이터를 한 번 적재하고 GPU에서 끝까지 처리하도록 재구성. "CPU보다 느린 GPU 코드"의 1순위.
  • cudaMemcpyAsync를 pageable 버퍼에. async라 써도 pageable이면 동기로 떨어진다. 실질적 비동기는 pinned + non-default 스트림(2.10).
  • 통합 메모리 오남용. "복사가 사라졌다"를 "비용이 사라졌다"로 착각. 폴트 마이그레이션은 보이지 않을 뿐 PCIe를 탄다. prefetch/advise로 소유권을 한쪽으로 고정.
  • pinned 과다 할당. page-lock은 물리 메모리를 묶는다. 거대하게 잡으면 시스템 전체가 느려진다. 버퍼 재사용.
  • 측정 함정. 비동기 호출 뒤 동기화 없이 시간을 재면 전송이 안 끝난 채 측정. 대역폭 측정은 반드시 동기화 후.

2.5.10 연습문제

연습 1 (전송 시간 산수). $8192\times8192$ FP16 두 장을 Gen5(실측 ~50 GB/s) PCIe로. (a) 총 바이트·전송 시간 하한. (b) 같은 두 행렬로 GEMM($2N^3$)을 200 TFLOPS에 실행할 때 연산 시간과 비교, "이전할 가치"를.

정답

한 장 = $8192^2\cdot2 = 128 \,\mathrm{MB}$, 두 장 256 MB. (a) $0.256/50 \approx5.1 \,\mathrm{ms}$. (b) $2\cdot8192^3\approx1.10\mathrm{e}12 /2\mathrm{e}14 \approx5.5 \,\mathrm{ms}$. 전송·연산이 비슷한 자릿수 → 데이터를 여러 번 재사용하거나 더 큰 파이프라인에 둘 때만 효율적. 단발 GEMM 한 번이면 전송이 거의 절반.

연습 2 (pageable→pinned). malloc만 cudaMallocHost로 바꾸면 대역폭이 오르는 이유를 staging 관점에서. 왜 pinned가 "비동기의 전제"인가?

정답

pageable은 OS가 스왑할 수 있어 DMA가 직접 못 읽는다 → 드라이버가 pinned staging으로 CPU 복사 후 DMA. pinned는 page-lock되어 DMA 직접 → staging 제거로 이론 대역폭에 근접. staging이 없으니 CPU가 기다릴 필요가 없어 DMA를 백그라운드로 진행 → 실질적 비동기·오버랩 가능(2.10).

연습 3 (통합 메모리 스래싱). 통합 메모리 반복 커널이 매 iteration마다 CPU에서 결과를 읽어 다음 입력으로 쓴다. 왜 느린가? 어떻게 완화?

정답

매 반복 GPU가 쓰고 CPU가 읽으니 같은 페이지가 GPU↔CPU를 왕복(스래싱). 해법: 가능하면 루프를 GPU 안에서 끝내 CPU 접근을 없앤다. 꼭 필요하면 SetPreferredLocation으로 소유권 고정, CPU가 읽기만 하면 SetReadMostly로 복제, 진입 전 cudaMemPrefetchAsync로 한 번에 옮겨 폴트 제거.

연습 4 (제로카피 판단). 4 GB 호스트 배열 중 커널이 무작위 100개만 1회 읽는다. 전체 복사 vs 제로카피? 그 100개를 1000번 반복 읽으면?

정답

1회 읽기: 제로카피 유리 — 4 GB 전체 복사를 피하고 필요한 캐시라인만 PCIe로. 1000번 반복: 매 접근이 PCIe라 먼저 필요한 영역을 디바이스로 복사해 HBM 재사용이 압도적으로 빠르다. 핵심은 "접근 횟수 × PCIe 지연" 대 "1회 복사 비용".

2.5.11 요약 · 다음 장

  • 실제 병목은 흔히 커널이 아니라 PCIe를 건너는 데이터 이동이다. 전송 시간을 산수하고 연산 시간과 비교해 "이전할 가치"를 먼저 판단하라.
  • pinned 메모리는 staging을 없애 대역폭을 높이고 비동기 복사의 전제다. 통합 메모리는 코드를 단순화하되 폴트 마이그레이션을 prefetch·advise로 다스려야 한다. 제로카피는 1회·부분 접근이나 통합 칩에서만 이득.
  • 절대 수치는 GPU 세대·PCIe·드라이버·OS 의존 — 시점·플랫폼 확인 필요.

다음 장: 2.6 공유 메모리 I — 뱅크·충돌·동기화. 블록 범위 온칩 스크래치패드를 직접 채우고 비우는 법, __syncthreads()의 정확한 위치, 그리고 뱅크 충돌을 타일링 맥락에서 다룬다.