2.8T 모델의 디코드 멈춤을 없애기
1. B300 클러스터에서 Kimi K3 Prefill, Decode 분리하기
초대형 언어모델을 서빙할 때 가장 어려운 문제는 단순히 모델을 GPU 메모리에 올리는 것이 아닙니다.
실제 서비스 환경에서는 긴 문맥을 입력하는 요청과 이미 생성 중인 요청이 동시에 들어옵니다. 이때 대규모 Prefill 연산이 Decode 연산과 동일한 GPU를 점유하면, 생성 중이던 스트림이 순간적으로 멈추는 문제가 발생합니다.
사이오닉은 2.8T 파라미터 규모의 Kimi K3를 B300 서버에서 서빙하면서 이 문제를 직접 확인했습니다.
기존 구성에서는 60K 토큰 이상의 긴 요청이 들어올 때 Decode 스트림의 ITL(Inter-Token Latency)이 수 초까지 증가했습니다. 토큰 생성 처리량 자체는 높았지만, 사용자가 체감하는 응답은 불규칙하게 끊겼습니다.
이번 글에서는 B300 서버를 Prefill 노드와 Decode 노드로 분리하고, 두 노드 사이에서 KV Cache를 전송해 디코드 멈춤 현상을 개선한 과정을 공유합니다.
문제: Prefill이 Decode를 멈춘다
LLM 추론은 크게 두 단계로 나눌 수 있습니다.
Prefill
사용자가 입력한 전체 프롬프트를 한 번에 처리하고, 이후 토큰 생성에 사용할 KV Cache를 계산하는 단계입니다.
프롬프트가 길수록 연산량이 크게 증가합니다. 특히 60K, 200K, 300K 토큰과 같은 장문 요청은 짧은 시간 동안 GPU 연산 자원을 강하게 점유합니다.
Decode
Prefill에서 생성된 KV Cache를 사용해 다음 토큰을 한 개씩 생성하는 단계입니다.
Decode는 연산량보다 메모리 접근과 지연시간이 중요합니다. 처리량이 충분하더라도 각 토큰 사이의 지연시간이 길어지면 사용자는 모델이 멈춘 것처럼 느낍니다.
문제는 두 작업이 동일한 GPU에서 실행될 때 발생합니다.
대규모 Prefill이 들어오면 GPU 스케줄링과 메모리 대역폭이 Prefill에 집중되고, 이미 실행 중인 Decode 스트림이 대기하게 됩니다.
사이오닉의 기존 구성에서 측정한 결과는 다음과 같았습니다.
구성 | 부하 중 ITL p95 | 최악 단일 멈춤 |
기본 구성 | 2,873ms | 수 초 |
Fairness 패치 적용 | 1,075ms | 약 1초 이상 |
Fairness 패치를 통해 일부 개선할 수 있었지만, 근본적으로 Prefill과 Decode가 동일한 GPU 자원을 경쟁하는 구조는 그대로였습니다.
해결 방향: Prefill과 Decode를 물리적으로 분리한다
사이오닉은 B300 서버의 역할을 다음과 같이 분리했습니다.
•
b300-04: Prefill Worker
•
b300-07: Decode Worker
•
Frontend Router: 요청 라우팅 및 Worker 탐색
•
etcd: Worker 등록 및 서비스 디스커버리
•
NATS: 컴포넌트 간 메시징
•
NIXL·RDMA: Prefill에서 Decode로 KV Cache 전송
초기 테스트는 한 대를 Prefill 머신으로, 다른 한 대를 Decode 머신으로 재시작하는 방식으로 진행했습니다. 이후 안정성이 확인되면 Decode 노드를 추가하는 형태로 확장할 수 있도록 구성했습니다.
전체 요청 흐름은 다음과 같습니다.
Client
│
▼
Dynamo Frontend Router
│
├── Prompt 전달 ───────────▶ Prefill Worker
│ │
│ │ KV Cache 생성
│ ▼
│ NIXL / RDMA
│ │
└── Decode 요청 ───────────▶ Decode Worker
│
▼
Token Streaming
Plain Text
복사
Prefill Worker는 입력 토큰을 처리하고 KV Cache를 생성합니다.
생성된 KV Cache는 CPU를 거쳐 복사하는 대신, InfiniBand와 RDMA 기반의 NIXL 전송 백엔드를 통해 Decode Worker로 전달됩니다.
Decode Worker는 전달받은 KV Cache를 바탕으로 토큰 생성만 담당합니다. 따라서 새로운 장문 요청이 지속적으로 들어와도 기존 Decode 스트림은 Prefill 연산의 영향을 거의 받지 않습니다.
Kubernetes 없이 Docker Worker를 연결하기
처음에는 B300 노드 전체를 k8s 클러스터에 넣고 DynamoGraphDeployment로 관리하는 구성을 검토했습니다.
그러나 실제 운영 중인 B300 서버에는 기존 Docker 컨테이너와 Slurm 환경이 존재했습니다. GPU 서버를 새 Kubernetes 클러스터에 편입하는 것은 테스트 범위를 넘어서는 큰 변경이었습니다.
대신 다음과 같은 혼합 구성을 사용했습니다.
•
Frontend, etcd, NATS는 기존 Kubernetes 환경에서 실행
•
B300의 Prefill·Decode Worker는 Docker로 실행
•
Docker Worker가 Kubernetes 내부의 etcd와 NATS에 직접 접속
•
Worker가 시작될 때 자신의 정보를 etcd에 자동 등록
•
Frontend가 etcd에서 Worker를 탐색
Worker 컨테이너에는 다음 환경변수만 추가하면 됩니다.
-e ETCD_ENDPOINTS=http://etcd1의 주소,http://etcd2의 주소,http://etcd3의 주소
-e NATS_SERVER=nats://nat 주소
Plain Text
복사
그리고 Worker는 일반적인 SGLang 서버가 아니라 Dynamo 어댑터를 통해 실행해야 합니다.
python3 -m dynamo.sglang
Plain Text
복사
일반 sglang.launch_server로 실행한 Worker는 Dynamo의 서비스 디스커버리에 등록되지 않습니다.
또한 컨테이너에서 호스트의 InfiniBand 장치와 네트워크를 직접 사용하기 위해 --network host 설정이 필요합니다.
docker run \
--network host \
--privileged \
-e ETCD_ENDPOINTS=... \
-e NATS_SERVER=... \
dynamo-sglang-k3:1.3.0.post1 \
python3 -m dynamo.sglang \
...
Plain Text
복사
Prefill과 Decode Worker는 동일한 served-model-name을 사용해야 합니다.
kimi-k3-ultrafast
Plain Text
복사
Frontend는 모델 이름을 기준으로 Prefill과 Decode Worker를 하나의 추론 파이프라인으로 연결합니다. Docker Worker가 etcd에 자신을 등록하고, Frontend가 이를 자동으로 발견하는 구조입니다.
Kimi K3 전용 Dynamo 이미지 만들기
공식 Dynamo 이미지에는 범용 SGLang 백엔드가 포함되어 있지만, 사이오닉에서 사용하던 Kimi K3 최적화 환경과는 차이가 있었습니다.
기존 Kimi K3 이미지에는 다음과 같은 최적화와 패치가 적용되어 있었습니다.
•
Kimi K3 전용 SGLang 코드
•
Mamba Cache 처리
•
Hierarchical Cache
•
DSPARK Speculative Decoding
•
TensorRT-LLM MLA Attention Backend
•
Decode 전용 Attention Backend
•
Cache Copy-on-Write 안정화 패치
•
장시간 부하 테스트에서 발견한 캐시 및 프록시 관련 패치
이 환경을 유지하면서 Dynamo Worker로 동작하도록 커스텀 이미지를 만들었습니다.
FROM lmsysorg/sglang:kimi-k3
RUN pip install "ai-dynamo[sglang]==1.3.0"
COPY patches/sglang/srt/ \
/sgl-workspace/sglang/python/sglang/srt/
Plain Text
복사
최종적으로 두 노드에 동일한 이미지를 준비했습니다.
dynamo-sglang-k3:1.3.0.post1
Plain Text
복사
Prefill과 Decode는 동일한 이미지를 사용하지만, 실행 인자에서 역할이 구분됩니다.
Prefill Worker
--disaggregation-mode prefill
--disaggregation-transfer-backend nixl
--chunked-prefill-size 32768
--max-prefill-tokens 32768
--enable-hierarchical-cache
--hicache-ratio 3
Plain Text
복사
Decode Worker
--disaggregation-mode decode
--disaggregation-transfer-backend nixl
--speculative-algorithm DSPARK
--speculative-dspark-block-size 7
--speculative-draft-window-size 4096
--disaggregation-decode-extra-slots 16
Plain Text
복사
실제 실행 스크립트는 다음과 같이 분리해 관리했습니다.
scripts/serve_dynamo_prefill.sh
scripts/serve_dynamo_decode.sh
Plain Text
복사
2.8T 모델은 시작하는 데도 시간이 걸린다
Kimi K3는 2.8T 파라미터 규모의 모델입니다.
Worker 컨테이너가 실행됐다고 해서 즉시 요청을 받을 수 있는 것은 아닙니다.
모델 가중치를 로딩하고 CUDA Graph를 캡처하며 각종 커널을 준비하는 과정에 약 25분이 소요됐습니다. 이 과정이 끝나기 전까지 /v1/models 결과가 비어 있을 수 있습니다.
따라서 다음 순서로 상태를 확인해야 합니다.
1. etcd 연결 확인
curl http://etcd-vip/health
Plain Text
복사
정상이라면 다음과 유사한 응답이 반환됩니다.
{"health":"true"}
Plain Text
복사
2. Worker 로딩 로그 확인
docker logs -f <container-name>
Plain Text
복사
3. 모델 등록 확인
curl https://dynamo-frontend/v1/models
Plain Text
복사
모델 등록이 완료되면 data 안에 kimi-k3-ultrafast가 나타납니다.
4. 추론 요청
curl https://kimi.sionic.tech/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{
"model": "kimi-k3-ultrafast",
"messages": [
{
"role": "user",
"content": "안녕"
}
],
"max_tokens": 32
}'
Plain Text
복사
Prefill과 Decode가 정상적으로 분리됐다면 양쪽 Worker 로그에 동일한 요청의 처리 기록이 남습니다.
예상하지 못한 문제: Worker는 등록됐는데 모델이 보이지 않는다
Prefill Worker와 Decode Worker의 로딩이 모두 완료됐지만, Frontend의 모델 목록은 계속 비어 있었습니다.
{
"object": "list",
"data": []
}
Plain Text
복사
처음에는 etcd 등록 실패나 네트워크 연결 문제로 의심했습니다.
그러나 동일한 etcd를 사용하는 검증용 Frontend에서는 모델이 정상적으로 노출됐습니다. Worker 쪽 등록과 Prefill·Decode 연결은 정상이라는 의미였습니다.
원인은 Frontend의 모델 경로였습니다.
Dynamo Frontend는 등록된 모델의 Model Card를 생성하는 과정에서 토크나이저 설정을 읽습니다. 하지만 Kubernetes에서 실행 중인 Frontend Pod에는 다음 경로가 존재하지 않았습니다.
/raid/models/Kimi-K3
Plain Text
복사
Frontend는 로컬 토크나이저를 읽지 못하자 Hugging Face 조회로 폴백했고, 해당 경로를 찾지 못하면서 모델 등록 결과가 API에 반영되지 않았습니다.
해결 방법은 전체 2.8T 가중치를 Frontend에 마운트하는 것이 아니었습니다.
Frontend가 실제로 필요로 하는 것은 토크나이저와 설정 파일뿐이었습니다.
사이오닉은 가중치를 제외한 토크나이저 및 설정 파일 15개를 약 1.4MB 크기의 번들로 만들었습니다.
/mnt/nas/models/.k3sync/kimi-k3-tokenizer-bundle.tgz
Plain Text
복사
이 번들을 Frontend Pod의 /raid/models/Kimi-K3에 마운트하자 모델이 즉시 목록에 나타났고, 실제 추론 요청까지 정상적으로 처리됐습니다.
초대형 모델을 분산 서빙할 때 모든 컴포넌트가 전체 가중치를 필요로 하는 것은 아닙니다.
Frontend에는 모델의 정체성을 확인할 최소한의 메타데이터와 토크나이저만 제공하고, 실제 가중치는 GPU Worker에만 배치하는 편이 훨씬 효율적입니다.
결과: ITL p95 2,873ms에서 29.2ms로
PD 분리의 효과를 확인하기 위해 다음과 같은 부하 테스트를 진행했습니다.
•
Decode 스트림 8개를 동시에 실행
•
60K 토큰 Prefill 요청 29개 투입
•
총 Prefill 입력 약 174만 토큰
•
Prefill 처리량 약 14.5K tok/s
•
테스트 시간 120초
결과는 다음과 같았습니다.
구성 | 부하 중 ITL p95 | 최악 단일 멈춤 | 스트림 처리량 |
기본 구성 | 2,873ms | 수 초 | 측정 제외 |
Fairness 패치 | 1,075ms | 약 1초 이상 | 측정 제외 |
Prefill·Decode 분리 | 29.2ms | 403ms | 36.0 → 34.2 tok/s |
ITL p95는 기존 2,873ms에서 29.2ms로 감소했습니다.
약 98.9%의 지연시간 감소입니다.
동시에 Decode 스트림 처리량은 36.0 tok/s에서 34.2 tok/s로 약 5% 감소하는 데 그쳤습니다.
즉, 처리량을 일부 양보하는 대신 서비스에서 훨씬 중요한 안정적인 토큰 스트리밍을 확보했습니다.
Prefill·Decode 분리는 단순한 처리량 최적화가 아닙니다.
초대형 모델을 실제 API 서비스로 제공하기 위한 지연시간 격리 기술에 가깝습니다. 실험에서도 PD 분리 이후 장문 Prefill 부하가 이어지는 동안 Decode 멈춤 현상이 사실상 해소됐습니다.
Hierarchical Cache 안정성 테스트
PD 분리 이후에는 Hierarchical Cache가 장시간 부하에서 안정적으로 동작하는지도 확인했습니다.
다음 세 가지 형태로 30분 동안 200개 이상의 요청을 투입했습니다.
부하 형태 | 강도 | 결과 |
신규 요청 Flood | 4×60K, 154개 요청 | 정상, Evictable Cache 재사용 |
Cache Pool 포화 | 4×200K, Device Pool 초과 | 정상, Available Slot 2~6 유지 |
재방문 요청 폭풍 | 12×300K, Host Load-back 12회 | 정상, 최저 Available 3 유지 |
테스트 중 Cache Pool의 available과 evictable이 동시에 0이 되는 크래시 조건은 발생하지 않았습니다.
Host Cache에서 Device Cache로 데이터를 다시 로드하는 상황에서도 ACK 처리 흐름이 요청 유입 속도를 따라갔습니다.
이 테스트를 통해 다음 구성을 함께 사용할 수 있음을 확인했습니다.
•
Prefill·Decode Disaggregation
•
Hierarchical KV Cache
•
Host Cache Load-back
•
DSPARK Speculative Decoding
•
NIXL RDMA Transfer
•
Kimi K3의 Mamba Cache
초대형 모델 서빙에서 처리량보다 중요한 것
LLM 서빙 성능은 흔히 초당 생성 토큰 수로 표현됩니다.
하지만 실제 서비스에서는 평균 처리량만큼이나 다음 지표가 중요합니다.
•
TTFT(Time to First Token)
•
ITL(Inter-Token Latency)
•
ITL p95·p99
•
긴 Prefill 요청 유입 시 Decode 안정성
•
동시 요청 간 지연시간 격리
•
KV Cache 전송 및 재사용 효율
•
Cache Pool 포화 상황에서의 안정성
평균 500 tok/s를 생성하는 모델이라도 수십 초마다 스트림이 2~3초씩 멈춘다면 좋은 사용자 경험을 제공하기 어렵습니다.
반대로 일부 처리량을 양보하더라도 토큰이 일정하게 출력되면 사용자는 모델이 훨씬 빠르다고 느낍니다.
이번 실험에서 PD 분리는 Decode 처리량을 약 5% 희생했지만, ITL p95를 약 99% 줄였습니다.
이는 초대형 모델을 벤치마크가 아니라 실제 API 서비스로 운영하기 위한 중요한 전환입니다.
다음 단계
현재 구성은 하나의 Prefill Worker와 하나의 Decode Worker를 기준으로 검증했습니다.
다음 단계에서는 요청 특성에 따라 Decode Pool을 분리할 수 있습니다.
예를 들면 다음과 같습니다.
•
256K 이하 요청을 처리하는 고동시성 Decode Pool
•
256K 이상 요청을 처리하는 장문 전용 Decode Pool
•
Prefill 부하에 따라 동적으로 Worker를 선택하는 Router
•
KV Cache 크기와 Decode Slot을 고려한 부하 분산
•
여러 Decode 노드 사이의 Prefix Cache 활용
•
장애 발생 시 Worker 자동 제외 및 재등록
•
Prefill·Decode 비율의 자동 조절
초대형 모델에서는 모든 요청을 동일한 Worker Pool에 넣는 방식보다, 문맥 길이와 예상 생성 길이에 맞게 자원을 분리하는 방식이 효율적입니다.
Prefill은 연산 집약적이고 Decode는 메모리 및 지연시간 집약적입니다.
서로 다른 성격의 워크로드를 동일한 GPU에서 경쟁시키는 대신, 각 단계에 맞는 자원을 독립적으로 확장하는 것이 대규모 추론 인프라의 핵심입니다.
마무리
2.8T 파라미터 모델을 실행하는 것과 안정적인 서비스로 제공하는 것은 전혀 다른 문제입니다.
모델이 GPU에 올라가고 높은 처리량을 기록하더라도, 긴 요청 하나가 다른 사용자들의 생성을 멈추게 한다면 프로덕션 서비스로 사용하기 어렵습니다.
사이오닉은 B300 서버에서 Kimi K3의 Prefill과 Decode를 물리적으로 분리하고, NIXL·RDMA 기반으로 KV Cache를 전달하는 구조를 구성했습니다.
그 결과 대규모 Prefill 부하 상황에서 ITL p95를 2,873ms에서 29.2ms로 줄였으며, Decode 처리량 감소는 약 5% 수준으로 제한했습니다.
또한 Hierarchical Cache, Mamba Cache, DSPARK Speculative Decoding이 결합된 환경에서 200개 이상의 장문 요청을 처리하며 안정성을 확인했습니다.
초대형 모델의 크기가 더 이상 느리고 불안정한 응답의 이유가 되지 않도록.
사이오닉은 GPU 커널부터 KV Cache, 분산 추론 아키텍처까지 직접 최적화하며 더 빠르고 안정적인 AI 인프라를 만들어가고 있습니다.

