제목

  • Efficient Memory Management for Large Language Model Serving with PagedAttention
    • 즉, LLM을 서빙할 때 PagedAttention을 사용하여 효율적인 메모리 관리하는 방법론에 대해서 다루는 논문이다.

소개

  • LLM 서빙에서 throughput(단위 시간당 처리할 수 있는 업무량)을 늘려 request 당 비용을 낮추는 것이 점점 중요해지고 있다.
  • 왜 throughput을 늘리는 게 어렵나?
    • 토큰을 하나 새로 생성할 때, 이전에 생성한 모든 토큰을 알고 있어야 그걸 계산할 수 있다.
    • 이런 sequential 생성 방식이 CPU/GPU의 성능보다 메모리 대역폭이 병목이 되게끔 만든다. (makes the workload memory-bound)
  • 위 도표는 A100 40GB에 13B LLM을 올렸을 때, 전체의 몇 퍼센트가 어떤 부분을 차지한 것인지 보이는 도표다.
    • Parameters: 모델의 가중치. 전체에서 65%를 차지하나 상수이므로 조절하기 어렵다.
    • Others: activation과 같은 짜투리. 이 부분을 최적화해봤자 차지하는 크기가 작으므로 큰 의미가 없다.
    • KV Cache: key와 value 텐서들을 저장해놓는 캐싱. 최적화할 여지가 있다!
      • 어떻게 도움이 될까?
        • 원래 hidden state에 가중치 를 곱하여 key와 value 텐서를 얻을 수 있는데, 매번 계산을 하는 것보다 Next Token Prediction에 필요한 key와 value 텐서만 저장하고 있으면 더 빠르게 생성이 가능하다.

문제상황

  • tensor가 연속된 메모리 공간 상에 있어야, 대부분의 연산을 적용할 수 있다.
  • 근데 출력을 얼마나 해야할지 알 수 없기 때문에, 가능한 최대한의 길이(maximum possible sequence length)를 일단 allocate한다.
  • 그러다보니 아래와 같은 비효율이 발생한다.
    1. reserved: 앞으로 생성될 토큰을 위해 예약(reserved)해둔 메모리 공간
    2. internal fragmentation: 실제 입출력과 무관하게 가능한 최대로 메모리 공간을 예약하다보니, 내부(internal)에서 영원히 사용되지 않은 메모리 공간(fragmentation)이 존재하게 된다.
    3. external fragmentation: 위에 figure처럼 어떤 request도 할당받지 않은 slot이지만, 연속된 slot이 짧기 때문에 할당해줄 수 없는 메모리 공간이 존재하게 된다.
  • 아래는 LLM 서빙 시스템별로 얼마나 메모리 공간이 낭비되고 있는지 측정한 도표다.

해결방법

PagedAttention

  • Paging을 이용해서 KV Cache를 관리한다. 이름하여 PagedAttention.
  • 번째 블록에 저장된 key와 value가 각각 , 일 때 아래 식으로 다음 토큰을 예측할 수 있다. (는 블록 사이즈)
  • 즉, 비연속적인 메모리 공간에서도 어텐션 스코어를 계산할 수 있다.
    • 장점: physical memory를 미리 한꺼번에 예약할 필요없이, 추가적으로 블록을 할당해야 할 때만 할당하면 된다. → 당장 사용할 수 있는 메모리 공간이 늘어나므로 request를 더 많이 받을 수 있다. → throughput 상승
  • 블록 사이즈가 크면 클수록 좋을까?
    • parallel한 연산을 한꺼번에 할 수 있어서 좋지만, internal fragmentation을 감당해야 함.

자세한 구조

  • block engine이 GPU DRAM의 연속적인 청크를 할당하고 이를 물리적인 메모리 공간 상에서 KV Cache를 나눈다.
    • 블록 테이블을 이용해서 logical 메모리와 physical 메모리 사이를 mapping한다.

복잡한 디코딩 방식 구현

Sampling

  • copy-on-write를 이용한다.
    • 똑같이 sampling된다면 계속 같은 physical block을 공유하다가, 변화가 생기면 그 block만 다른 physical block에 복사한 다음은 mapping을 바꿔준다.

  • sampling이랑 비슷하게 사용한다.
  • 도중에 beam에서 벗어나게 된 예측의 경우, 해당 예측과 연결된 block을 모두 free해서 메모리를 아낄 수 있다.

Shared Prefix

  • system prompt과 같이 계속 쓰이는 prefix의 경우, 미리 계산해놓을 수 있다.

LLM “서빙” 시스템

  • 위 그림에서 확인할 수 있는 것처럼, 스케쥴러가 달려있다.
  • 그건 얘가 서빙 시스템이라 LLM으로 들어오는 요청을 받고 그에 따른 출력을 반환을 할 때 어떤 요청부터 처리해야할지 정해야하기 때문이다.
  • 그냥 queue처럼 먼저 들어온 요청부터 처리한다.
    • 일단 VRAM에 다 올려놓고 token을 생성하다가 자리가 부족하면 가장 늦게 들어온 요청을 내쫓는다.
      • 좀 더 구체적으로는 CPU DRAM으로 스와핑이 일어난다.
        • all-or-nothing 정책을 사용해서 같은 sequence group이라면 한꺼번에 스와핑을 한다.
      • 내쫓은 요청이 없을 때까지 새로운 요청을 더이상 받지 않는다.
    • 완료된 요청이 있으면 내쫓은 요청을 다시 VRAM으로 올려서 출력을 계속 만들게 한다.

구현

  • 8.5천 라인의 Python 코드 + 2천 라인의 C++/CUDA 코드
    • Python: 스케줄러, 블록 매니저와 같은 control-related component
      • PyTorch, Transformer: model executor
    • CUDA: PagedAttention가 같은 key operation을 커스텀 CUDA 커널
      • NCCL: GPU worker 간의 tensor communication
      • Fused reshape and block write / Fusing block read and attention / Fused block copy