본문 바로가기

03. prefill과 decode의 서로 다른 병목

핵심 질문
  • "prefill은 compute-bound, decode는 memory-bound"라는 유명한 문장은 언제 참이고, 언제 뒤집히는가?
  • 병목(bottleneck)이 어디인지를 무엇으로 판별하는가?

목표
  • compute-bound와 memory-bound를 정의하고, 두 phase에 조건부로 연결한다.
  • "경향"과 "법칙"을 구분하는 습관을 기른다.

먼저: compute-bound와 memory-bound

어떤 계산이 느릴 때, 병목은 대개 둘 중 하나입니다.

  • compute-bound(연산 병목): 가속기의 연산 능력(초당 부동소수점 연산 수)이 한계여서 느린 상태. 데이터는 충분히 빨리 오는데, 곱셈·덧셈 자체가 밀린다.
  • memory-bound(메모리 병목): 가속기의 메모리 대역폭(초당 옮길 수 있는 데이터 양)이 한계여서 느린 상태. 연산 유닛은 놀고 있는데, 데이터가 제때 도착하지 못한다.
🔎 약어·개념 풀이
  • 대역폭(bandwidth): 단위 시간에 옮길 수 있는 데이터 양(예: GB/s). "수도관 굵기"에 비유.
  • VRAM(Video RAM): GPU에 붙은 메모리. 가중치와 KV 캐시가 여기 산다.
  • 판별의 직관: 같은 데이터를 얼마나 많이 재사용해 연산하는가(arithmetic intensity, 연산 강도)가 핵심. 데이터 한 번 읽어 연산을 많이 하면 compute 쪽으로, 데이터를 많이 읽는데 연산은 적으면 memory 쪽으로 기웁니다. (깊은 roofline 모델은 선수 과정 참조.)

왜 prefill과 decode가 다른 쪽으로 기우는 '경향'이 있나

prefill의 경향 — compute 쪽

prefill은 프롬프트의 많은 토큰을 한꺼번에 처리합니다. 가중치를 한 번 VRAM에서 읽어오면, 그 가중치로 여러 토큰을 동시에 곱합니다. 즉 "데이터(가중치)를 읽는 비용" 대비 "그 데이터로 하는 연산량"이 큽니다 → 연산 강도가 높아 compute-bound로 기우는 경향.

decode의 경향 — memory 쪽

decode는 한 스텝에 토큰 하나만 만듭니다. 그 한 토큰을 위해 모델의 방대한 가중치와 커지는 KV 캐시를 VRAM에서 읽어와야 합니다. "읽어온 데이터"에 비해 "그걸로 하는 연산"이 적습니다 → 연산 강도가 낮아 memory-bound로 기우는 경향.

flowchart TB
    subgraph prefill경향
        P1[많은 토큰 동시 처리]:::comp --> P2[가중치 1회 로드로<br/>연산 다수]:::comp --> P3[연산 강도 높음<br/>→ compute 쪽 경향]:::result
    end
    subgraph decode경향
        D1[토큰 1개 생성]:::comp --> D2[가중치·KV 대량 로드로<br/>연산 소수]:::mem --> D3[연산 강도 낮음<br/>→ memory 쪽 경향]:::danger
    end

    classDef comp fill:#d0ebff,stroke:#1971c2,color:#000;
    classDef mem fill:#e5dbff,stroke:#7048e8,color:#000;
    classDef result fill:#d3f9d8,stroke:#2f9e44,color:#000;
    classDef danger fill:#ffc9c9,stroke:#e03131,color:#000;

⚠️ 여기가 함정: '경향'을 '법칙'으로 굳히지 말 것

"prefill = compute-bound, decode = memory-bound"는 기본 경향이지 불변의 법칙이 아닙니다. 조건이 바뀌면 뒤집히거나 흐려집니다.

경향이 뒤집히거나 흐려지는 조건들

조건어떻게 바뀌나
decode의 배치 크기(batch)가 커짐여러 요청의 토큰을 한 스텝에 묶으면, 같은 가중치 로드로 더 많은 연산을 하게 되어 decode도 compute 쪽으로 이동할 수 있음 (배칭의 핵심 동기 — 07장).
prefill 프롬프트가 매우 짧음토큰이 적으면 병렬성이 작아 compute 이점이 줄고, 상대적으로 오버헤드·메모리 접근이 부각될 수 있음.
컨텍스트가 매우 길어 KV가 거대함decode에서 KV 캐시 읽기가 지배적이 되어 memory 병목이 더 심해질 수 있음.
양자화로 데이터 폭을 줄임읽어야 할 바이트가 줄어 memory 병목이 완화될 수 있으나, 커널·품질 절충이 따라옴(11장).
하드웨어의 연산:대역폭 비율가속기마다 이 비율이 달라, 같은 워크로드가 어떤 칩에선 compute, 다른 칩에선 memory 병목이 될 수 있음.
🔬 증거 읽기 (주장 유형 구분)
  • "decode는 memory-bound 경향이 있다"는 inferred(구조에서 유도한 해석)에 가깝고, 널리 재현되는 관찰이지만 조건부입니다.
  • "이 모델을 이 GPU에서 배치 N으로 돌리니 decode가 compute-bound가 됐다"는 measured여야 합니다 — 모델·하드웨어·배치·정밀도가 명시돼야 의미가 있습니다.
  • 조건 없이 "decode는 항상 memory-bound"라고 옮기는 자료는 신뢰도를 낮춰 읽으세요.

이것이 여정에서 왜 중요한가

01장의 여정 지도로 돌아가 봅시다. prefill과 decode가 다른 병목을 갖는다는 사실은 이후 거의 모든 최적화의 동기가 됩니다.

  • decode가 memory 쪽으로 기울므로 → 배칭으로 가중치 로드를 여러 요청이 나눠 쓰게 만들자(07~08장).
  • KV 캐시가 memory 병목의 큰 축이므로 → KV를 아껴 쓰자(PagedAttention 09장, prefix caching 10장, KV 양자화 11장).
  • prefill과 decode의 성격이 다르므로 → 둘을 분리하거나 섞어 스케줄링하자(chunked prefill, disaggregation — 08장에서 언급).

즉 이 장의 "조건부 병목"은 뒤 장들을 꿰는 왜(why)입니다.

📌 핵심

  • compute-bound는 연산 능력이, memory-bound는 메모리 대역폭이 한계인 상태.
  • prefill은 연산 강도가 높아 compute 쪽, decode는 낮아 memory 쪽으로 기우는 경향.
  • 이 경향은 배치 크기·컨텍스트 길이·정밀도·하드웨어에 따라 뒤집히거나 흐려진다.
  • 이 조건부 병목이 이후 모든 최적화(배칭·KV 절약·스케줄링)의 동기다.

🔎 이 장의 '지지하지 않는 결론'

  • "decode는 언제나 memory-bound"라고 말할 수 없습니다 — 배치가 커지면 이동합니다.
  • 특정 수치(예: "decode가 prefill보다 몇 배 느리다")를 이 장 근거만으로 주장할 수 없습니다 — 그건 모델·하드웨어·배치가 명시된 measured 근거가 필요합니다.

✍️ 확인 문제

  1. compute-bound와 memory-bound를 각각 한 문장으로 정의하고, "연산 강도"가 둘을 가르는 데 왜 핵심인지 설명하세요.
  2. decode가 memory 쪽으로 기우는 이유를 "한 토큰을 위해 무엇을 얼마나 읽는가"로 설명하세요.
  3. (조건 유형) "decode는 memory-bound다"라는 주장을 신뢰하려면 자료가 어떤 조건들을 함께 밝혀야 합니까? 최소 세 가지를 드세요. 그리고 그 경향이 뒤집힐 수 있는 조건 하나를 제시하세요.
다음 묶음(2부): 04 · weight와 runtime memory → 05 · KV 캐시 → 06 · attention 변형. 메모리의 세계로 들어갑니다.