ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • GPU에서 LLM까지, 추론 스택 완전 해부
    IT 2026. 4. 13. 21:00
    GPU에서 LLM까지, 추론 스택 완전 해부

    왜 이 글을 쓰게 됐나

    로컬 환경에서 LLM을 직접 돌려보면서 vLLM, FlashAttention, NGC 같은 이름을 처음 접했다. PyTorch로 모델 돌리면 되는 거 아니야? 했는데, 알고 보니 GPU 하드웨어와 LLM 사이에는 각자 다른 병목을 해결하는 소프트웨어 레이어가 겹겹이 쌓여 있었다.

    이 글에서는 전체 스택을 관통하는 하나의 질문 — "이 레이어는 무슨 문제를 풀기 위해 등장했는가?" — 을 축으로 정리한다. 각 레이어에서 가장 핵심적인 기술 딱 하나만 골라서 집중적으로 설명한다.

    전체 레이어 다이어그램

    diagram

    L1. GPU 하드웨어 — 왜 CPU가 아니라 GPU인가

    LLM 추론의 핵심 연산은 거대한 행렬 곱셈이다. "A 행렬 × B 행렬"을 수십억 번 반복하는 것. CPU는 코어가 수십 개라서 복잡한 로직에는 강하지만, 이런 단순 연산의 대량 반복에는 맞지 않는다.

    GPU는 다르다. 수천 개의 작은 코어가 동시에 돌아간다. 비유하자면 CPU는 박사 10명이 어려운 문제를 푸는 것이고, GPU는 고등학생 10,000명이 단순 계산을 나눠 푸는 것이다. LLM 추론은 후자에 가깝다.

    NVIDIA H100 기준으로 보면:

    • Tensor Cores: 행렬 곱셈 전용 유닛. FP8 기준 3,958 TFLOPS — CPU 대비 수백 배
    • HBM3 메모리: 80GB, 대역폭 3.35TB/s. 모델 가중치를 빠르게 읽어오는 게 핵심인데, 일반 DDR5의 10배 이상

    하지만 GPU가 아무리 빨라도, 이걸 프로그래밍할 방법이 없으면 무용지물이다. 그래서 다음 레이어가 등장한다.

    L2. CUDA — GPU를 범용 연산에 쓸 수 있게 한 열쇠

    GPU는 원래 게임 그래픽용이었다. 삼각형을 그리고 텍스처를 입히는 전용 하드웨어. 여기에 행렬 곱셈을 시키려면?

    2007년 NVIDIA가 CUDA(Compute Unified Device Architecture)를 발표하기 전에는, GPU에서 범용 연산을 하려면 "그래픽 렌더링인 척" 해야 했다. 데이터를 텍스처로 위장하고, 셰이더 프로그램으로 연산을 돌리는 식이다. 당연히 프로그래밍이 지옥이었다.

    CUDA는 이 문제를 해결했다. GPU에서 일반적인 C/C++ 코드를 실행할 수 있게 해준 것이다. 핵심은:

    • 커널(kernel): GPU에서 실행되는 함수. 수천 개의 스레드가 동시에 같은 커널을 실행한다
    • cuBLAS: 행렬 곱셈(GEMM)을 GPU에서 최적화하는 라이브러리. LLM 연산의 80% 이상이 여기를 통과한다
    • 메모리 계층 관리: GPU의 HBM(느리지만 큼) ↔ SRAM(빠르지만 작음) 사이의 데이터 이동을 제어

    CUDA 덕분에 GPU가 "그래픽 카드"에서 "AI 연산 엔진"으로 탈바꿈했다. 하지만 CUDA 프로그래밍은 여전히 수백 줄의 C++ 코드가 필요한 고난이도 작업이다.

    L3. PyTorch & TensorRT-LLM — 같은 레벨, 다른 역할

    CUDA 위에 올라가는 프레임워크는 하나가 아니다. 학습(Training)추론(Inference)이라는 서로 다른 단계를 위해 서로 다른 프레임워크가 존재한다. 이 둘은 상하 관계가 아니라 같은 레벨에서 나란히 서 있다.

    PyTorch — 학습의 표준

    CUDA를 직접 쓰면 행렬 곱셈 하나에 수백 줄의 C++ 코드가 필요하다. 연구자가 원하는 건 "커널 최적화"가 아니라 "내 아이디어를 빠르게 실험하는 것"이다.

    PyTorch는 이 간극을 메웠다. Python 몇 줄이면 모델을 정의하고 학습시킬 수 있다. 비결은 Dynamic Computation Graph다:

    • 코드를 실행하면서 연산 그래프를 동적으로 만든다
    • Python의 if/for 문을 자연스럽게 쓸 수 있어 디버깅이 쉽다
    • 내부적으로는 cuBLAS, cuDNN 같은 CUDA 라이브러리를 자동 호출한다

    LLM 연구와 학습의 사실상 표준이다. 하지만 이 편의성에는 성능 비용이 따른다. Python 인터프리터 오버헤드, 매번 새로 만드는 동적 그래프, 최적화 안 된 메모리 할당... 학습 중에는 괜찮지만, 추론 서비스에서는 이 오버헤드가 치명적이다.

    TensorRT-LLM — 추론의 표준

    그래서 추론 단계에서는 별도의 프레임워크를 쓴다. PyTorch로 학습된 모델을 받아서 GPU 전용 최적화 엔진으로 변환하는 것이 TensorRT-LLM이다.

    PyTorch가 추론에서 느린 이유를 구체적으로 보면:

    1. 연산 A를 GPU에 보내고 → 결과를 메모리에 쓰고 → 다시 읽어서 → 연산 B를 GPU에 보내고 → ... 이 "보내고-쓰고-읽고"가 매번 반복된다
    2. Python이 매 연산마다 "이 텐서의 크기는? 타입은? 어디에 있지?" 하고 검사한다. 이 디스패치 오버헤드가 쌓인다
    3. 연산 하나하나를 개별 커널로 실행해서, GPU가 놀고 있는 시간(idle)이 길다

    TensorRT-LLM은 이걸 한 방에 해결한다:

    • 커널 퓨전(Kernel Fusion): "A 하고 B 하고 C 해" 대신 "ABC를 한 번에 해" 하나의 커널로 합친다. 중간 메모리 읽기/쓰기가 사라진다
    • 그래프 최적화: 전체 연산 그래프를 분석해서 불필요한 연산을 제거하고, 순서를 재배치한다
    • GPU 전용 코드 생성: 특정 GPU 아키텍처(H100 등)에 맞춘 최적화 바이너리를 만든다

    비유하면 PyTorch가 "통역사를 끼고 대화"하는 것이라면, TensorRT-LLM은 "상대방 언어로 직접 대화"하는 것이다.

    성과: vanilla PyTorch 대비 최대 4x throughput 향상.

    둘의 관계: 상하가 아니라 전후

    정리하면 이렇다:

      PyTorch TensorRT-LLM
    단계 학습(Training) 추론(Inference)
    장점 유연성, 빠른 실험 극한의 추론 성능
    위에서 쓰는 것 CUDA (cuBLAS, cuDNN) CUDA (cuBLAS, cuDNN)
    관계 PyTorch로 학습한 모델 → TensorRT-LLM으로 변환하여 서빙

    둘 다 CUDA 위에서 동작하는 같은 레벨의 프레임워크다. PyTorch가 TensorRT 아래에 깔려 있는 게 아니라, 학습이 끝난 모델을 TensorRT에 넘기는 전후 관계인 것이다. 그리고 이 다음부터 나오는 L4~L6의 최적화 기술들은 모두 추론 경로(TensorRT-LLM 쪽) 위에서만 동작한다.

    L4. FlashAttention — LLM 최대 병목 "어텐션"을 정조준

    여기서부터는 추론 경로만의 이야기다. Transformer 아키텍처의 핵심인 Self-Attention이 왜 문제인지부터 보자.

    어텐션은 입력의 모든 토큰이 다른 모든 토큰과의 관계를 계산한다. 시퀀스 길이가 N이면 N×N 크기의 어텐션 행렬이 만들어진다. N=4,096이면 약 1,600만 개의 값이다. 이걸 통째로 GPU 메모리(HBM)에 올려야 하니까:

    • 메모리 사용량이 O(N²)로 폭발한다. 시퀀스가 길어지면 메모리가 모자란다
    • 문제는 메모리 크기만이 아니다. 이 거대한 행렬을 HBM에서 읽고-쓰는 시간이 실제 연산 시간보다 훨씬 길다. GPU 연산은 빠른데 데이터를 실어나르는 게 병목인 것이다 (memory-bound)

    FlashAttention의 아이디어는 단순하면서도 강력하다: "N×N 행렬을 한 번에 만들지 말고, 작은 조각(타일)으로 나눠서 처리하자."

    GPU 안에는 HBM(크지만 느림) 외에 SRAM(작지만 매우 빠름)이라는 메모리가 있다. FlashAttention은:

    1. 어텐션 행렬을 SRAM에 들어가는 작은 타일로 쪼갠다
    2. 각 타일을 SRAM에서 계산한다 (HBM 접근 최소화)
    3. 타일 결과를 수학적 트릭(online softmax)으로 합산한다
    4. 최종 결과만 HBM에 쓴다

    N×N 행렬 전체가 메모리에 존재할 필요가 없으니 메모리 O(N²) → O(N)으로 줄고, HBM 읽기/쓰기가 대폭 감소하니 속도도 2-4x 빨라진다. 시퀀스 길이 4K에서는 메모리가 20배 절감된다.

    L5. vLLM — 100명이 동시에 질문해도 버티는 서빙

    FlashAttention까지 적용하면 "한 번의 추론"은 빨라진다. 하지만 실제 서비스에서는 수십~수백 명이 동시에 질문한다. 여기서 새로운 병목이 드러난다: KV Cache 메모리 관리.

    LLM은 토큰을 하나씩 생성할 때마다 이전 토큰들의 Key/Value 벡터를 저장해둬야 한다 (같은 계산을 반복하지 않으려고). 이게 KV Cache다. 문제는 기존 방식이 각 요청마다 최대 시퀀스 길이만큼 메모리를 미리 할당했다는 것이다.

    예를 들어 최대 4,096 토큰을 지원하는 모델이면, 실제로 "안녕" 두 글자만 물어봐도 4,096 토큰 분량의 메모리를 잡아먹는다. 실제 사용량은 절반도 안 되는데 나머지는 빈 방인 셈이다. 동시 요청이 늘어날수록 이 낭비가 치명적이다.

    vLLM은 운영체제의 가상 메모리(Virtual Memory) 개념을 가져왔다. PagedAttention이라는 이름이다:

    • KV Cache를 작은 페이지(블록) 단위로 나눈다
    • 토큰이 생성될 때마다 필요한 만큼만 페이지를 할당한다
    • 연속된 메모리 공간이 필요 없으니 단편화(fragmentation)가 사라진다

    마치 호텔에서 "4,096호실을 통째로 예약"하는 대신 "지금 당장 필요한 방만 하나씩 배정"하는 것과 같다.

    성과:

    • HuggingFace Transformers 대비 14-24x throughput 향상
    • GPU utilization 85-92%까지 끌어올림 (기존 68-74%)
    • 동시 요청 100-150개까지 linear scaling

    L6. Quantization — 모델을 다이어트시켜서 GPU에 태우기

    위의 모든 최적화를 적용해도, 모델 자체가 GPU 메모리에 안 올라가면 소용없다. LLaMA-70B 모델은 FP16 기준 140GB — H100 한 장(80GB)으로는 불가능하다.

    Quantization(양자화)은 모델 가중치의 숫자 표현 정밀도를 낮추는 것이다. 사진을 JPEG로 압축하면 파일 크기는 줄지만 눈으로 보면 거의 차이가 없는 것과 비슷하다:

    • FP16 → INT8: 메모리 50% 절감. 품질 거의 유지
    • FP16 → INT4: 메모리 75% 절감. 약간의 품질 하락 있지만 대부분의 태스크에서 무시할 수준

    특히 AWQ(Activation-aware Weight Quantization)가 주목받는 이유는, 모든 가중치를 균등하게 압축하는 게 아니라 "중요한 채널은 정밀하게, 덜 중요한 채널은 과감하게" 차등 압축하기 때문이다. 덕분에 INT4까지 내려도 품질 유지율 ~95%를 달성한다.

    실제 수치: AWQ + Marlin 커널 조합으로 68 → 741 tok/s (10.9x throughput 향상). 메모리도 줄고 속도도 빨라지는 일석이조다.

    NGC — 이 모든 걸 묶는 컨테이너 레지스트리

    여기까지 읽으면 드는 생각: "이걸 다 직접 설치하고 버전 맞추라고?"

    맞다. CUDA 12.2에 cuBLAS 12.x에 PyTorch 2.3에 TensorRT-LLM 0.12에 FlashAttention 2.6에 vLLM 0.6... 한 버전만 어긋나면 세그폴트가 터진다. 실제로 "모델은 5분이면 다운받는데, 환경 설정에 3일 걸렸다"는 이야기가 흔하다.

    NGC(NVIDIA GPU Cloud)는 이 문제를 해결하는 컨테이너 레지스트리다. L2(CUDA)부터 L5(vLLM)까지의 소프트웨어를 사전 검증된 Docker 이미지로 패키징해서 제공한다:

    • CUDA + cuBLAS + cuDNN + PyTorch + TensorRT-LLM + vLLM이 호환성 검증이 끝난 상태로 하나의 컨테이너에 들어 있다
    • 신규 GPU(Blackwell 등) 출시일에 Day-1 최적화 이미지 배포
    • docker pull nvcr.io/nvidia/pytorch:24.03-py3 한 줄이면 끝

    성능을 올려주는 게 아니라, "성능 올리는 도구들을 설치하는 삽질"을 제거하는 것이 NGC의 가치다. 다이어그램에서 L2~L5를 세로로 감싸는 형태로 그린 이유다.

    정량 지표 한눈에 보기

    레이어 핵심 기술 해결하는 문제 개선 수준
    L1 GPU (H100) 대규모 병렬 연산 3,958 TFLOPS (FP8)
    L2 CUDA GPU 범용 프로그래밍 GPU 연산의 토대
    L3 학습 PyTorch 모델 학습 생산성 Python으로 모델 정의
    L3 추론 TensorRT-LLM 추론 오버헤드 최대 4x ↑
    L4 FlashAttention Attention O(N²) 메모리 2-4x 속도 ↑, 메모리 20x ↓
    L5 vLLM KV Cache 메모리 낭비 14-24x throughput ↑
    L6 AWQ Quantization 모델이 메모리에 안 올라감 메모리 75% ↓, 10.9x 속도 ↑
    횡단 NGC 환경 설정 지옥 docker pull 한 줄

    마무리: 레이어를 알면 병목이 보인다

    이 스택을 처음 봤을 때 "왜 이렇게 복잡해?"라는 생각이 먼저 들었다. 하지만 한 레이어씩 뜯어보니 패턴이 명확하다. 공통 기반(L1~L2) 위에서 학습과 추론이 갈라지고, 추론 경로 위에 최적화가 쌓인다:

    • GPU가 빨라도 프로그래밍이 어려우니까 → CUDA
    • CUDA가 있어도 직접 쓰기 힘드니까 → PyTorch(학습) / TensorRT-LLM(추론)
    • 추론 엔진이 있어도 Attention이 O(N²)이니까 → FlashAttention
    • Attention이 빨라져도 서빙 시 메모리가 낭비되니까 → vLLM
    • 서빙이 빨라져도 모델이 안 올라가니까 → Quantization

    결국 이 모든 최적화의 목표는 하나다 — 같은 GPU로 더 많은 토큰을 더 빠르게 뽑아내는 것.

    참고 자료


    이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.

Designed by Tistory.