본문 바로가기
Infrastructure

GPU는 '코어 많은 CPU'가 아니다 - 웹 백엔드 개발자가 다시 배운 GPU

by Ramos 2026. 7. 11.

나는 Kotlin/Spring으로 웹 백엔드를 만든다. 우리 제품의 SRE 영역까지 담당한다. 코드 레벨을 넘어 신경 쓰는 자원은 CPU와 메모리다. 스레드풀 크기를 만지고, 커넥션 풀을 튜닝하고, GC 로그를 들여다본다. GPU는 "게임할 때 그래픽 카드" 정도였고, 솔직히 내 일과는 상관없다고 생각했다. 그런데 언젠가부터 업계 전체가 GPU 얘기만 한다. 클라우드는 GPU를 못 구해 안달이고, 인프라 팀은 "GPU를 어떻게 쪼개 쓰냐"로 회의를 한다. 나 같은 개발자도 이제 GPU가 뭔지는 알아야겠다 싶어서 파고들었다. 그리고 첫 번째로 깨진 착각이 바로 "GPU = 코어 많은 CPU" 였다. 이 글은 그 오해를 부순 기록이다.

Intro

  • CPU와 GPU는 코어 수만 다른 게 아니라 설계 철학이 반대다. CPU는 소수의 똑똑한 코어로 지연을 줄이고, GPU는 수천 개의 약한 코어로 처리량을 뽑는다.
  • OS 관점에서 GPU는 CPU 위에서 도는 게 아니라 PCIe 너머의 주변장치(코프로세서) 다. 내 JVM도 리눅스 커널도 전부 CPU에서 돈다. GPU는 일감을 "던져주는" 대상이다.
  • 그래서 CPU엔 당연한 프로세스, cgroups, 공정 스케줄러 같은 멀티테넌시 도구가 GPU엔 오랫동안 없었다.
  • 지금 업계가 GPU 가상화에 목매는 이유는 단순하다. 비싼 GPU를 워크로드가 10~30%밖에 못 쓴다. "CPU처럼 0.3개씩 쪼개 나눠 쓰자" 가 핵심이다.
  • 그 해법(MIG, time-slicing, PagedAttention)은 결국 내가 아는 OS 개념(파티셔닝, 스케줄링, 페이징, 배칭)을 GPU에 이식하는 일이었다. 백엔드 지식이 그대로 무기가 됐다.

출발점: "코어가 많으니까 빠른 CPU 아니야?"

처음 GPU 스펙을 봤을 때 든 생각은 이랬다. "CPU는 코어가 몇십 개인데 GPU는 수천 개네? 그럼 그냥 엄청 병렬화 잘 되는 CPU잖아." 이 직관이 편하긴 한데, 딱 여기서 막힌다. 그럼 왜 우리는 웹 서버를 GPU 위에서 안 돌릴까? 코어가 수천 개면 동시 요청을 수천 개 처리하면 될 텐데.

답은 GPU 코어가 내가 생각하는 그 "코어"가 아니기 때문이다. 내가 아는 CPU 코어는 이렇게 생겼다.

  • 코어 하나가 아주 강력하다. 깊은 파이프라인, 분기 예측(branch prediction), out-of-order 실행, 큼직한 L1/L2/L3 캐시를 갖고 있다.
  • 목표는 요청 하나를 최대한 빨리 끝내는 것이다. 지연(latency) 최적화다.
  • if/else가 잔뜩 있는 분기 로직, 트랜잭션 처리처럼 "생각을 많이 해야 하는 일"에 강하다.

GPU 코어는 정반대다.

  • 코어 하나는 약하다. 분기 예측도 거의 없고, 캐시도 작고, 혼자 두면 느리다.
  • 목표는 똑같은 연산을 방대한 데이터에 동시에 퍼붓는 것이다. 처리량(throughput) 최적화다.
  • 행렬 곱셈, 벡터 연산처럼 "같은 걸 엄청 많이" 하는 일에 강하다. 딥러닝 연산이 정확히 그런 모양이다.

비유하자면 이렇다. CPU 코어는 어떤 복잡한 주문이든 알아서 판단하고 처리하는 베테랑 웨이터 몇 명이다. GPU는 "전원 동시에 오른쪽으로 한 걸음"만 시킬 수 있는 신입 1만 명 군단이다. 동일한 단순 동작 1만 개를 시키면 압도적이지만, 각자 다른 판단을 시키는 순간 무너진다. 웹 요청은 요청마다 로직이 다르니 GPU엔 최악의 워크로드다.

warp: GPU의 진짜 실행 단위는 스레드가 아니다

GPU를 이해하려면 warp라는 단어를 먼저 넘어야 한다. GPU의 실제 실행 단위는 스레드 하나가 아니라 32개 스레드 묶음(warp) 이다. 이 32개는 완전히 같은 명령을 lockstep으로, 그러니까 발맞춰 동시에 실행한다. 이걸 SIMT(Single Instruction, Multiple Threads)라고 부른다.

여기서 백엔드 개발자가 제일 놀라는 함정이 나온다. 이런 코드를 GPU에서 돌린다고 해보자.

// warp 안 32개 스레드가 이 코드를 만나면...
if (data[tid] > 0) {
    heavyA();   // 절반은 이쪽
} else {
    heavyB();   // 절반은 저쪽
}

CPU라면 각 스레드가 제 갈 길을 간다. 그런데 GPU의 warp는 한 몸이라 두 갈래를 동시에 못 간다. 먼저 heavyA()를 실행하는 동안 else 쪽 스레드를 놀리고(masking), 다음에 heavyB()를 실행하는 동안 if 쪽 스레드를 놀린다. 결과적으로 분기가 갈리면 실행 시간이 두 배가 된다. 이 현상을 branch divergence라고 한다.

스레드풀에 비유하면, 배치(warp) 안에서 한 명이라도 다른 길로 가면 배치 전체가 두 번 도는 셈이다. 백엔드에서 조건 분기는 공짜에 가깝지만, GPU에서 if는 비싸다. "당연한 코드"가 여기선 안티패턴이 된다는 게 첫 번째 문화 충격이었다.

실행 계층 전체를 그림으로 보면 이렇다.

메모리 계층도 CPU와 다르다. 특히 shared memory가 흥미로웠다. CPU 캐시는 하드웨어가 알아서 채워주지만, GPU의 shared memory는 개발자가 명시적으로 "이 데이터를 여기 올려라"고 손으로 관리한다. L1 캐시를 코드로 직접 배치하는 셈이다. GPU 성능 튜닝의 절반이 이 메모리 배치 싸움이라고 한다.

진짜 핵심: OS 레벨에서 GPU는 "남의 동네"다

여기가 이 글에서 가장 하고 싶은 얘기다. 백엔드 개발자가 GPU를 오해하는 근본 원인은 GPU를 CPU와 같은 무대에 올려놓고 생각하기 때문이다. OS 입장에서 보면 둘은 완전히 다른 위치에 있다.

내가 GPU를 공부하며 정리한 다섯 가지다.

  1. GPU는 커널이 도는 곳이 아니다. 리눅스 커널, 스케줄러, 내 JVM은 전부 CPU에서 돈다. GPU는 PCIe에 꽂힌 가속기이고, CPU가 드라이버를 통해 "이 함수(커널)를 실행하라"고 일감을 던지는 대상이다. GPU는 "부하 노동자"이지 "관리자"가 아니다.
  2. GPU엔 전통적으로 '프로세스'가 없다. CPU는 OS가 프로세스를 선점형으로 스케줄링한다(리눅스 CFS). GPU는 오랫동안 한 번에 하나의 컨텍스트만 실행하거나 아주 거친 단위로 시분할했다. 커널을 커맨드 큐에 밀어 넣으면 순서대로 처리하는 방식이라, "여러 테넌트가 공평하게 나눠 쓴다"는 개념 자체가 최근까지 빈약했다.
  3. 메모리가 물리적으로 분리돼 있다. 데이터를 GPU에 쓰려면 시스템 RAM에서 VRAM으로 PCIe를 통해 복사해야 한다. 이 복사가 진짜 병목인 경우가 많다. 그래서 GPU 프로그래밍은 "연산을 빠르게"보다 "데이터를 언제 넘기고 언제 도로 가져올까"의 싸움에 가깝다. 백엔드의 N+1 쿼리 왕복 비용과 똑같은 고민이다.
  4. 드라이버가 어마어마하게 두껍다. CPU에서 syscall 한 번 하는 것과 달리, GPU는 명령 스케줄링, 메모리 관리, 컨텍스트 전환을 유저스페이스와 커널 드라이버 조합이 담당한다. 이 두꺼운 드라이버가 벤더 종속(CUDA)의 뿌리이기도 하다.
  5. 컨텍스트 스위칭이 원래 비쌌다. CPU는 스레드 전환이 마이크로초 단위다. 그런데 GPU는 수천 스레드의 레지스터 상태가 전부 칩 위에 상주(resident)해 있어서, warp 간 전환은 사실상 0비용으로 하는 대신(뒤에 나올 latency hiding), 서로 다른 프로세스나 테넌트 간 전환은 매우 무거웠다. 바로 이 비대칭이 GPU 가상화가 어려운 근본 이유다.

정리하면, GPU는 "코어 많은 CPU"가 아니라 OS 바깥에 있는, 나눠 쓰기가 어려운 초고가 주변장치다. 이 한 문장이 뒤의 모든 걸 설명한다.

느린 메모리를 숨기는 방식이 이벤트 루프와 똑같다

GPU가 약한 코어 수천 개로 어떻게 빠른 걸까? 비밀은 latency hiding에 있다. 그리고 이 원리가 백엔드 개발자에겐 놀랍도록 익숙하다.

CPU는 큰 캐시와 out-of-order 실행으로 메모리 지연을 숨긴다. GPU는 전략이 다르다. "놀고 있는 warp가 항상 넘쳐나게" 만들어 두고, 한 warp가 메모리를 기다리면 즉시 다른 warp로 갈아탄다. 그래서 GPU는 일부러 자원 대비 스레드를 초과 배치(oversubscription)한다.

이거, 이벤트 루프가 IO 대기 중에 다른 요청을 처리하는 것과 정확히 같은 원리다. 논블로킹 IO, 코루틴으로 스레드를 놀리지 않는 그 패턴을, GPU는 실리콘 레벨에서 나노초 단위로 한다. 내가 백엔드에서 배운 "블로킹되면 다른 일 시켜라"가 하드웨어 설계 철학으로 굳어져 있는 걸 보고 반가웠다.

여기서 실무적으로 제일 중요한 성능 개념 하나를 짚고 넘어가자. 대부분의 LLM 추론은 연산이 아니라 메모리 대역폭에 묶여 있다(memory-bound). 모델 가중치 수십에서 수백 GB를 VRAM에서 계속 읽어와야 하는데, 요청 하나만 처리하면 그 방대한 가중치를 읽는 비용을 요청 1개가 다 뒤집어쓴다. 그래서 여러 요청을 묶어서(batching) 가중치를 한 번 읽을 때 여러 요청을 같이 처리하면 효율이 폭발한다. "왜 GPU 활용률이 낮은가"라는 질문의 답이 바로 여기 있다.

왜 업계가 GPU 가상화에 목매는가

이제 처음의 질문으로 돌아온다. 왜 다들 GPU를 "쪼개 쓰는" 얘기만 할까? 세 가지가 겹쳤다.

첫째, 경제성이다. 고성능 GPU 한 장이 수천만 원인데 그마저도 공급이 부족하다. 그런데 현실 워크로드, 특히 추론은 GPU 한 장을 10~30%밖에 못 채운다. 수천만 원짜리 자산을 70% 놀리는 셈이다. 활용률 1%가 대규모 함대에서는 수백만 달러 단위로 벌어진다.

둘째, 성숙도 격차다. 이 표가 내 머릿속에서 GPU 문제를 확 정리해줬다.

CPU/메모리 (성숙함) GPU (최근까지 빈약)
cpu: 300m (0.3코어) nvidia.com/gpu: 1 (정수 1장 단위, 쪼개기 불가)
cgroups로 CPU/메모리 격리 GPU 메모리 limit 강제가 어려움
CFS 공정 스케줄링 테넌트 간 공정 분배 개념 미약
OOMKill로 격리된 종료 한 테넌트가 VRAM을 다 쓰면 옆 테넌트도 죽음 (noisy neighbor)

나는 K8s에서 CPU를 300m, 즉 0.3개씩 아무렇지 않게 나눠 쓴다. 그런데 GPU는 오랫동안 "한 장 통째로 아니면 못 씀"이었다. 그래서 업계가 지금 하는 일을 한마디로 요약하면 "GPU를 위한 cgroups와 K8s를 만드는 것" 이다.

셋째, AI 붐이다. LLM이 터지면서 GPU가 모든 회사의 최희소 자원이 됐다. "쪼개 쓰기(fractioning)"와 "채워 쓰기(utilization)"가 곧 돈이 됐다.

그래서 나온 해법들을 분류해보면 이렇다.

  • Time-slicing은 GPU를 시간으로 쪼개 여러 컨테이너에 돌려준다. CPU 시분할과 비슷하지만 메모리 격리가 없어서, 옆 테넌트가 VRAM을 터뜨리면 같이 죽는다.
  • MPS는 여러 프로세스를 공간적으로 합쳐 동시에 실행한다. 처리량은 좋아지지만 격리는 여전히 약하다.
  • MIG(Multi-Instance GPU) 는 GPU 하드웨어를 물리적으로 여러 조각으로 파티셔닝한다. 각 조각이 자기 SM, 메모리, 캐시를 따로 가진다. 진짜 격리이고, 내가 원하던 "GPU 0.3개"에 가장 가깝다. 실제로 금융권처럼 격리가 중요한 곳에서 MIG로 GPU 한 장을 여러 워크로드에 안전하게 나눠주는 사례가 나오고 있다(아래 References의 Toss 글).
  • vGPU 는 하이퍼바이저 레벨에서 VM에 GPU를 나눠주는 방식이다.

그리고 소프트웨어 레이어로 내려오면, 내 OS 지식이 그대로 재현되는 걸 보고 소름이 돋았다. 추론 서버(vLLM 등)의 PagedAttentionOS의 가상 메모리 페이징을 GPU 메모리에 그대로 이식한 것이다. KV 캐시를 고정 크기 페이지로 관리하고 페이지 테이블로 매핑해 단편화를 없앤다. VRAM을 빽빽하게 채워 같은 GPU로 더 많은 동시 요청을 처리한다. 여기에 continuous batching까지 더하면 memory-bound였던 추론의 활용률이 크게 오른다.

정리하면 GPU 최적화는 하드웨어(MIG) 싸움만이 아니라, OS 교과서 개념(파티셔닝, 스케줄링, 페이징, 배칭)을 GPU 자원에 이식하는 소프트웨어 레이어 싸움이기도 하다.

백엔드 개발자에게 남는 것?

GPU를 파고들며 얻은 가장 큰 소득은, 낯선 하드웨어를 내가 아는 언어로 번역할 수 있게 됐다는 점이다.

내가 아는 것 GPU 세계의 대응
DB 커넥션 풀 (비싸고 유한, 반드시 공유) GPU 그 자체. 통째로 점유하면 낭비
커넥션 풀 튜닝 (max, 대기 큐) 배치 크기, 동시 요청 수 튜닝
스레드풀 / 이벤트 루프 warp 스케줄러의 latency hiding
N+1 쿼리 = 왕복 비용 PCIe RAM과 VRAM 사이 복사 = 왕복 비용
OOMKill / 메모리 limit VRAM 고갈 (격리가 약함)
K8s cpu: 300m MIG 슬라이스 / 시분할 (여기가 미성숙)
인프라 팀이 관리하는 인프라 vLLM, Triton 같은 추론 서버 = GPU용 앱서버와 커넥션 풀

웹 백엔드에 ML 추론 티어(임베딩 생성, LLM 응답, 이미지 처리)가 붙는 순간 GPU는 더 이상 남의 얘기가 아니다. 그때 GPU를 "코어 많은 CPU"로 오해하고 있으면 활용률도, 비용도, 장애 격리도 전부 헛다리를 짚게 된다. 반대로 GPU를 "아주 비싼 공유 자원 풀" 로 보면, 커넥션 풀을 튜닝하던 그 감각이 그대로 이어진다.

SRE 관점에서 한 문장으로 요약하면 이렇다. GPU는 활용률이 곧 비용이다. 봐야 할 지표는 GPU utilization, VRAM 사용량, SM occupancy, ECC error, 온도와 전력(보통 DCGM exporter로 수집한다)이다. memory-bound 워크로드는 배칭으로 활용률을 끌어올리고, noisy neighbor가 걱정되면 MIG 같은 하드 격리를 택한다. "비싼 GPU를 절대 놀리지 않는다"가 제1원칙이다.

나는 여전히 CPU와 메모리를 만지는 웹 백엔드 개발자다. 하지만 이제 GPU가 왜 다른 종류의 자원인지, 업계가 왜 그걸 쪼개는 데 그렇게 진심인지 설명할 수 있다. 그리고 그 답의 절반이 내가 이미 아는 OS 개념이라는 사실이, 이 학습에서 가장 든든한 부분이었다.

References

'Infrastructure' 카테고리의 다른 글

Gradle × GitHub Actions 캐시 전략 완전 가이드  (2) 2026.04.01