1조 개의 파라미터를 가진 모델을 어떻게 Kubernetes 클러스터에 맞출 수 있을까?
요약
1조 개의 파라미터를 가진 거대 모델을 Kubernetes 환경에서 서빙할 때 발생하는 기술적 도전 과제를 다룹니다. 모델은 단순한 컨테이너 단위가 아닌 분산 시스템으로 취급되어야 하며, 메모리 요구량과 병렬 처리 전략이 핵심임을 설명합니다.
핵심 포인트
- 거대 모델은 단일 포드가 아닌 분산 시스템으로 설계되어야 함
- LLM 서빙의 실제 스케일 단위는 요청이 아닌 토큰임
- 모델 가중치 외에도 KV 캐시, 통신 버퍼 등 추가 메모리 고려 필수
- 모델 크기에 따라 복제본의 단위가 GPU 클러스터 슬라이스로 확장됨
시리즈 링크
거대한 모델은 "포드(Pod) 안에서 실행"되지 않습니다.
만약 당신이 수년간 Kubernetes 객체(Objects)를 중심으로 생각하며 시간을 보냈다면, 이 문장은 틀린 것처럼 들릴 것입니다. 우리는 소프트웨어를 컨테이너(Containers)로 패키징합니다. 우리는 컨테이너를 포드(Pods)에서 실행합니다. 우리는 포드를 노드(Nodes)에 스케줄링합니다. 우리는 그 앞에 서비스(Services)를 배치합니다. 컨테이너 내부의 대상이 웹 서버(Web server), 워커(Worker), 큐 소비자(Queue consumer) 또는 API 프로세스(API process)일 때는 이 모델이 잘 작동합니다.
그런 다음 누군가 말합니다. "Kubernetes에 1조 개의 파라미터를 가진 모델을 호스팅할 수 있을까요?"
솔직한 답변은 이렇습니다: 네, 하지만 당신의 뇌가 처음 상상하는 방식으로는 아닙니다. 1조 개의 파라미터를 가진 모델은 kubelet이 충분한 CPU와 메모리를 할당해주기를 기다리며 하나의 깔끔한 포드 안에 앉아 있는 하나의 깔끔한 프로세스가 아닙니다. 그것은 가중치(Weights)의 더미, 통신 패턴(Communication patterns), 병렬 워커(Parallel workers), GPU 메모리 제한(GPU memory limits), 상호 연결 가정(Interconnect assumptions), 그리고 서빙 엔진(Serving-engine) 결정들의 집합체입니다. Kubernetes는 외부 형태를 조정할 수 있지만, 모델 자체는 분할되어야 합니다.
이 시리즈의 Part 1에서는 LLM 서빙(Serving)이 일반적인 웹 서빙이 아니라고 주장했습니다. Part 2에서는 토큰(Tokens)이 실제 작업 단위이기 때문에 요청(Requests)은 잘못된 스케일 단위라고 주장했습니다. Part 3는 다음의 불편한 단계에 대해 다룹니다: 모델이 충분히 커지면, 복제본(Replica)은 더 이상 포드가 아닙니다. 복제본은 GPU 그룹, 하나의 노드, 여러 개의 노드, 또는 함께 작동하는 훨씬 더 큰 GPU 클러스터의 슬라이스(Slice)가 될 수 있습니다.
포드는 단지 봉투일 뿐입니다. 모델은 분산 시스템(Distributed system)입니다.
지루한 수학부터 시작하기
텐서 병렬성 (Tensor parallelism), 파이프라인 병렬성 (Pipeline parallelism), 전문가 병렬성 (Expert parallelism), Ray, vLLM, KServe, MPI, NCCL 또는 그 어떤 Kubernetes YAML을 논하기 전에, 단순한 메모리 질문이 하나 있습니다:
가중치에는 몇 바이트가 필요할까?
대략적인 공식은 간단합니다:
가중치 메모리 = 파라미터 수 x 파라미터당 바이트
그것은 단지 모델 가중치(weights)일 뿐입니다. KV 캐시 (KV cache), 런타임 오버헤드 (runtime overhead), CUDA 그래프 (CUDA graphs), 학습 중 활성화 값 (activations during training), 옵티마이저 상태 (optimizer states), 통신 버퍼 (communication buffers), 단편화 (fragmentation), 또는 서빙 엔진 (serving engine) 자체의 메모리 예약분은 포함되지 않았습니다. 하지만 첫 번째 단계로 잘못된 가정을 바로잡기에는 이 정도면 충분합니다.
FP16 또는 BF16으로 저장된 1조 개의 파라미터를 가진 밀집 모델 (dense model)은 다음과 같은 메모리가 필요합니다:
1,000,000,000,000 파라미터 x 2 바이트 = 2,000,000,000,000 바이트
이는 대략 2 TB의 가공되지 않은 가중치 메모리 (raw weight memory)입니다.
디스크가 아닙니다. 오브젝트 스토리지 (object storage)도 아닙니다. GPU에서 주소 지정이 가능한 메모리 (GPU-addressable memory)입니다.
만약 80 GB GPU를 사용한다면, 2 TB의 가공되지 않은 가중치는 오버헤드를 고려하기 전에도 이미 25개의 GPU가 필요합니다. 만약 141 GB H200급 GPU를 사용한다면, 가중치만으로도 여전히 약 15개의 GPU가 필요합니다. 이것이 정확히 그만큼의 GPU가 있어야 모델을 사용할 수 있다는 뜻은 아닙니다. 이는 실제 서빙 문제가 시작되기 전의 최저 한계치 (floor)라는 의미입니다.
여기서부터 일반적인 Kubernetes 방식의 사고가 사람들을 오도하기 시작합니다. 포드 (pod)는 메모리를 요청할 수 있습니다. 포드는 nvidia.com/gpu: 8을 요청할 수 있습니다. Kubernetes는 해당 포드를 충분한 GPU 장치가 광고된 노드 (node)에 배치할 수 있습니다. 하지만 Kubernetes가 마법처럼 하나의 프로세스가 25개의 개별 GPU를 하나의 거대한 GPU처럼 취급하게 만들어주지는 않습니다. 서빙 엔진 (serving engine)과 분산 런타임 (distributed runtime)이 그 작업을 수행해야 합니다.
Kubernetes는 하드웨어에 대한 액세스를 스케줄링할 뿐, 모델을 대신 샤딩 (shard)해주지는 않습니다.
FP16과 BF16은 작지 않습니다
FP16과 BF16은 종종 메모리 최적화 기법인 것처럼 논의되며, FP32와 비교했을 때 실제로 그렇습니다. FP32는 파라미터당 4 바이트를 사용합니다. FP16과 BF16은 2 바이트를 사용합니다. 가중치 메모리를 절반으로 줄이는 것은 매우 큰 일입니다.
하지만 1조 개의 파라미터 규모에서는, 거대한 것의 절반이라 할지라도 여전히 거대합니다.
FP16 또는 BF16 환경에서 175B 파라미터 모델은 약 350 GB의 가공되지 않은 가중치를 가집니다. 이는 이미 일반적인 단일 GPU에 들어가지 않는 크기입니다. 671B 파라미터 모델은 FP16 또는 BF16에서 약 1.34 TB입니다. 1T 밀집 모델은 약 2 TB이며, 1.8T 밀집 모델은 약 3.6 TB가 될 것입니다.
양자화 (Quantization)는 계산 방식을 변화시킵니다. FP8을 사용하면 1T 파라미터의 가중치(raw weights)를 약 1 TB로 줄일 수 있습니다. FP4를 사용하면 약 500 GB까지 줄어듭니다. NVIDIA의 1조 파라미터 추론 (inference) 기술 문서에서는 1.8T 파라미터를 가진 GPT MoE 모델을 예로 들어 FP4로 저장했을 때 가중치가 약 900 GB가 된다고 설명합니다. 192 GB GPU를 사용할 경우, 이 가중치들을 담기 위한 이론적 최소치는 단 5개의 GPU입니다.
"최소"라는 단어를 떠올리기 전까지는 이 숫자가 놀라울 정도로 작게 들릴 것입니다.
5개의 GPU가 가중치를 담을 수는 있겠지만, 토큰을 충분히 빠르게 생성하지 못할 수도 있습니다. KV 캐시 (KV cache)를 위한 충분한 공간을 남겨두지 못할 수도 있습니다. 긴 문맥 (long-context) 모델의 경우, 활성화된 복제본 (replica) 하나당 KV 캐시가 수십 기가바이트를 점유할 수 있으며, 실제 동시 접속 (concurrency) 상황에서는 GPU 메모리를 두고 가중치와 직접 경쟁하게 됩니다. 또한, 동일한 5개의 GPU가 통신량이 너무 많아질 수도 있습니다. 첫 번째 토큰 생성 시간 (time to first token)이 매우 나쁠 수도 있습니다. 멋진 데모 하나는 지원할 수 있을지 몰라도, 실제 트래픽 아래에서는 무너져 내릴 수 있습니다.
메모리 하한선 (memory floor)은 배포가 가능한지 여부를 알려줄 뿐, 그것이 좋은 배포인지는 알려주지 않습니다.
메모리는 하나의 문제일 뿐이기에 모델을 분할한다
모델이 하나의 GPU에 들어가지 않을 때, 크게 두 가지 방법을 선택할 수 있습니다.
모델을 더 작게 만드는 것입니다. 양자화 (quantize)하거나, 증류 (distill)하거나, 더 작은 체크포인트 (checkpoint)를 선택하거나, 문맥 길이 (context length)를 줄이거나, 어댑터 (adapters)를 사용하거나, 일부 트래픽을 더 저렴한 모델로 라우팅 (route)하는 방법이 있습니다. 이것들은 유효하며 종종 올바른 운영 (production) 환경의 선택지이지만
- 텐서 병렬화 (Tensor parallelism)
- 파이프라인 병렬화 (Pipeline parallelism)
- 전문가 병렬화 (Expert parallelism)
이 방식들은 종종 데이터 병렬화 (Data parallelism)와 결합되어 사용됩니다. 데이터 병렬화는 샤딩 (sharded)된 모델의 여러 독립적인 복제본 (replicas)을 실행하여 더 많은 트래픽을 처리하는 방식입니다. 모델 복제본이 어딘가에 들어갈 수만 있다면 데이터 병렬화는 이해하기 쉽습니다. 어려운 점은 애초에 하나의 복제본을 존재하게 만드는 것입니다.
텐서 병렬화 (Tensor parallelism)는 레이어의 내부를 분할합니다
텐서 병렬화 (Tensor parallelism)는 모델 레이어 내부의 개별 텐서 (tensors)를 여러 GPU에 걸쳐 분할합니다. 트랜스포머 (transformer) 레이어의 전체 행렬 곱셈 (matrix multiplication)을 하나의 GPU에 배치하는 대신, 서빙 엔진 (serving engine)이 레이어의 작업을 여러 GPU로 나누고 그 결과를 결합합니다.
트랜스포머는 분할 가능한 대규모 행렬 연산을 가지고 있기 때문에 이 방식이 유용합니다. Megatron-LM은 GPT와 같은 모델을 위한 이러한 형태의 텐서 모델 병렬화 (tensor model parallelism) 방식을 대중화했으며, vLLM의 분산 서빙 (distributed serving) 문서에서도 여전히 Megatron-LM의 텐서 병렬 알고리즘을 구현 기반으로 지목하고 있습니다.
간단한 개념 모델:
하나의 트랜스포머 레이어
GPU 0: 가중치 행렬 (weight matrix)의 한 조각을 소유
GPU 1: 다른 조각을 소유
...
모든 토큰 단계 (token step)마다 GPU들은 각자의 조각을 계산한 다음 부분 결과 (partial results)를 교환합니다. 이는 강력하지만 공짜는 아닙니다. 텐서 병렬화는 빠른 GPU 간 통신 (GPU-to-GPU communication)에 크게 의존합니다. NVLink나 다른 고대역폭 인터커넥트 (high-bandwidth interconnect)가 있는 노드 내부에서는 잘 작동할 수 있습니다. 하지만 노드 간(across nodes) 통신 비용은 매우 심각해질 수 있습니다.
이것이 바로 많은 실무 가이드에서 가능한 경우 텐서 병렬화를 노드 내부로 유지할 것을 권장하는 이유입니다. vLLM의 스케일링 가이드 (scaling guidance)도 동일한 형태를 띱니다. 모델이 하나의 GPU에 담기에는 너무 크지만 하나의 멀티 GPU 머신에는 들어간다면, 텐서 병렬화를 사용하십시오. 노드에 4개의 GPU가 있다면 tensor_parallel_size=4가 명확한 시작점입니다.
텐서 병렬화는 하나의 레이어를 여러 GPU에 걸쳐 더 넓게 만듭니다. 이는 메모리와 토큰당 연산 (per-token compute)에 도움이 되지만, 해당 GPU들을 매우 긴밀하게 결합시킵니다. 이들은 더 이상 독립적인 포드 (pods)가 아닙니다. 하나의 추론 머신 (inference machine)을 구성하는 조각들입니다.
파이프라인 병렬화 (Pipeline parallelism)는 레이어 스택을 분할합니다
파이프라인 병렬화 (Pipeline parallelism)는 모델을 레이어 단위로 수직으로 절단합니다.
모든 GPU가 모든 레이어에 참여하는 대신, 하나의 GPU 또는 GPU 그룹이 초기 레이어를 담당하고, 다른 그룹이 중간 레이어를, 또 다른 그룹이 후기 레이어를 담당합니다. 요청은 마치 조립 라인을 통과하는 작업처럼 이러한 단계들을 거쳐 이동합니다.
대략적인 모습:
Stage 1: layers 1-20 -> GPU group A
Stage 2: layers 21-40 -> GPU group B
Stage 3: layers 41-60 -> GPU group C
...
파이프라인 병렬화 (Pipeline parallelism)는 모델이 하나의 노드 (node)에 들어갈 수 없을 때 매력적인 선택지입니다. 느린 경계(boundary)를 가로질러 텐서 병렬화 (tensor parallelism)를 확장하는 대신, 각 노드 내부에는 텐서 병렬화 (tensor parallelism)를 유지하고 노드 간에는 파이프라인 병렬화 (pipeline parallelism)를 사용할 수 있습니다. NVIDIA의 Megatron 연구는 정확히 이 패턴을 설명합니다. 즉, 텐서 병렬화 (tensor parallelism)는 DGX A100 노드 내부에서 잘 작동하며, 파이프라인 병렬화 (pipeline parallelism)는 서로 다른 통신 패턴을 사용하기 때문에 노드 간 확장을 돕습니다.
vLLM의 현재 문서에는 동일한 아이디어의 실용적인 서빙 (serving) 버전이 나와 있습니다. 각각 8개의 GPU를 가진 2개의 노드의 경우, 텐서 병렬화 (tensor parallelism) 크기를 8로, 파이프라인 병렬화 (pipeline parallelism) 크기를 2로 설정하십시오. 쉽게 말해, 노드 내부의 8개 GPU에 각 레이어를 분할한 다음, 모델의 레이어들을 2개의 노드에 걸쳐 분할하는 것입니다.
tensor_parallel_size = 노드당 GPU 수
pipeline_parallel_size = 노드 수
이 규칙이 절대적인 것은 아니지만, 좋은 첫 번째 멘탈 모델 (mental model)이 될 수 있습니다.
파이프라인 병렬화 (Pipeline parallelism)는 자체적인 고충도 수반합니다. 파이프라인에는 작업이 오기를 기다리는 동안 일부 단계가 유휴 상태로 머무는 버블 (bubbles)이 발생할 수 있습니다. 학습 (training) 시스템은 마이크로배치 (microbatches)와 스케줄링 트릭을 통해 이에 대응합니다. 추론 (inference)의 경우, 서빙 엔진 (serving engine)이 파이프라인을 통해 서로 다른 요청을 지속적으로 공급함으로써 단계들을 더 바쁘게 유지할 수 있지만, 이는 배치 (batching), 트래픽 형태 (traffic shape), 그리고 구현 방식에 따라 달라집니다. 운영 측면에서의 핵심은 간단합니다. 단계를 더 많이 추가할수록 모델은 컨테이너화된 API (containerized API)보다는 분산 워크플로 (distributed workflow)처럼 작동하기 시작합니다.
Kubernetes는 Pod를 계속 유지할 수 있습니다. 서빙 엔진 (serving engine)은 파이프라인을 가득 채운 상태로 유지해야 합니다.
이러한 형태에서 "레플리카 (replica)"는 대부분의 플랫폼 팀이 처음에 가지는 사고 모델 (mental model)보다 이미 더 큽니다. 서빙 레플리카는 단일 컨테이너가 아닙니다. 그것은 랭크 (ranks), 워커 (workers), 그리고 디바이스 (devices)들의 조정된 집합입니다.
MoE 모델은 독특하기 때문에 전문가 병렬성 (Expert parallelism)이 존재합니다
Mixture-of-Experts (MoE) 모델은 또 다른 변수를 추가합니다.
밀집 모델 (dense model)은 보통 모든 토큰에 대해 동일한 파라미터를 사용합니다. 모델이 70B 파라미터를 가지고 있다면, 각 토큰은 해당 밀집 스택 (dense stack)을 통과합니다. 만약 모델이 1T 밀집 파라미터를 가지고 있다면, 서빙 시스템은 엄청난 양의 메모리와 연산량을 처리해야 합니다.
MoE 모델은 다릅니다. 이들은 많은 전문가 피드포워드 네트워크 (expert feed-forward networks)를 포함하며, 라우터 (router)가 각 토큰을 처리할 전문가를 선택합니다. 이는 총 파라미터 수는 거대하지만, 특정 토큰에 대해 활성화되는 파라미터는 그중 일부에 불과한 모델을 만듭니다.
다음은 많은 혼란으로부터 사람들을 구해주는 문장입니다:
1조 개의 파라미터가 항상 토큰당 1조 개의 파라미터 연산을 의미하는 것은 아닙니다.
Switch Transformer 논문은 대규모 환경에서 이 개념을 유명하게 만들었습니다. 이 논문은 MoE 모델을 희소 활성화 (sparsely activated) 모델로 설명합니다. 즉, 파라미터 수는 방대하지만, 각 토큰이 소수의 전문가에게 라우팅되기 때문에 연산량은 대략 일정하게 유지됩니다. Switch는 각 토큰을 하나의 전문가에게 보내는 방식으로 라우팅을 더욱 단순화했습니다.
최신 공개 모델들은 더 친숙한 형태로 동일한 개념을 보여줍니다. DeepSeek-V3는 671B 파라미터의 MoE 모델로 보고되었지만, 각 토큰에 대해 활성화되는 파라미터는 37B에 불과합니다. 그렇다고 해서 이 모델이 "실제로 37B"인 것은 아닙니다. 비활성 전문가들은 여전히 존재합니다. 그들의 가중치 (weights) 또한 어딘가에 존재해야 합니다. 하지만 하나의 토큰을 위한 연산 경로 (compute path)는 총 파라미터 수가 시사하는 것보다 훨씬 작습니다.
이러한 구분은 용량 계획 (capacity planning)에서 중요합니다. 총 파라미터는 스토리지와 배치 (placement)를 결정합니다. 활성 파라미터는 토큰당 연산을 결정합니다. 둘 다 중요하지만, 이 둘은 같은 숫자가 아닙니다.
전문가 병렬성 (Expert parallelism)은 서로 다른 전문가 (experts)를 서로 다른 GPU 또는 노드에 배치하는 시스템적 기법입니다. 토큰이 전문가에게 라우팅 (routing)될 때, 서빙 시스템 (serving system)은 해당 전문가를 보유한 장치로 토큰 표현 (token representations)을 보내고, 전문가 연산을 수행한 뒤, 그 결과를 다시 모델 흐름 (model flow)으로 결합합니다.
이는 새로운 병목 현상 (bottleneck)을 만들어냅니다: 바로 토큰 라우팅 (token routing)과 올투올 통신 (all-to-all communication)입니다. 만약 라우터가 특정 전문가에게 너무 많은 트래픽을 보내면, 해당 전문가는 핫 (hot) 상태가 됩니다. 만약 전문가들이 여러 노드에 분산되어 있다면, 네트워크가 클러스터 전체에 토큰 활성화 값 (token activations)을 실어 나르기 시작합니다. 만약 서빙 엔진 (serving engine)이 통신 (communication)과 연산 (compute)을 적절히 중첩 (overlap)시키지 못한다면, GPU는 대기하게 됩니다.
MoE는 마법이 아닙니다. 이는 밀집 연산 (dense compute)을 라우팅 (routing), 부하 분산 (load balancing), 메모리 배치 (memory placement), 그리고 통신 (communication)과 맞바꾸는 것입니다.
1조 개의 파라미터를 가진 MoE는 1조 개의 파라미터를 가진 밀집 모델 (dense model)과 같지 않습니다
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기