음성 AI 모델을 프로덕션에 올리기까지: Kanana-O 서빙 최적화 여정 (새 탭에서 열림)
Kanana-O를 실시간 음성 대화 서비스로 제공하려면 모델 학습과는 별개의 서빙 최적화가 필요하다. 카카오는 Thinker·Talker·VoiceBox로 구성된 파이프라인에 특화된 Kanana-Omni Server를 구축해 임베딩 전달, 스트리밍 지연, 프로세스 안정성, 동시 요청 처리 문제를 해결했다. 그 결과 동시 사용자 64명 기준으로 vllm-omni 대비 상대 처리량 1.6배를 달성했다.
Kanana-O의 멀티모달 음성 생성 구조
- Thinker는 텍스트·이미지·오디오 입력을 이해하고 텍스트를 생성하는 대형 언어 모델이다.
- Talker는 Thinker의 텍스트 임베딩을 받아 음성 토큰을 순차적으로 생성한다.
- VoiceBox는 음성 토큰을 실제 오디오 파형으로 변환한다.
- 프로덕션 환경에서는 다음 조건을 동시에 만족해야 한다.
- 수백 밀리초 안에 첫 음성을 제공해야 한다.
- 여러 사용자의 요청을 동시에 처리해야 한다.
- 텍스트와 오디오를 끊김 없이 스트리밍해야 한다.
- 각 모델의 GPU 메모리 요구량 차이를 관리해야 한다.
범용 멀티모달 서빙 프레임워크의 한계
- Thinker가 Talker에 전달하는 값은 토큰 ID가 아니라 수천 차원의 히든 스테이트 임베딩이다.
- 임베딩을 직렬화하거나 CPU로 복사하면 지연이 커지므로 GPU 메모리 주소를 직접 공유하는 zero-copy 방식이 필요했다.
- Talker는 매 스텝 음성 토큰을 생성하지만 VoiceBox는 일정량의 토큰이 쌓인 뒤 청크 단위로 소비한다.
- Talker의 입력에는 다음 값이 누적된다.
- 화자 특성을 나타내는 스피커 임베딩
- Thinker의 출력 임베딩
- 이전 스텝에서 생성한 음성 임베딩
- 이처럼 생산자와 소비자의 속도가 다르고 입력 길이가 계속 증가하는 구조는 일반적인 LLM 서빙 패턴과 맞지 않았다.
- 자체 서버를 구축한 결과 상대 처리량은 다음과 같았다.
- naive 구현: 0.44
- vllm-omni: 1
- Kanana-Omni Server: 1.6
공유 메모리와 CUDA IPC를 활용한 데이터 전달
- 프로세스 간 임베딩 전달을 매번 직렬화·역직렬화하면 토큰마다 큰 오버헤드가 발생한다.
- 서버 시작 시 OS 레벨에서 공유 메모리 블록을 미리 할당해 런타임 할당·해제를 제거했다.
- Thinker는 공유 메모리 풀의 블록에 데이터를 쓰고, Talker에는 실제 데이터 대신 블록 번호와 크기 같은 메타데이터만 전달한다.
- 같은 노드의 GPU 간 텐서 전달에는 CUDA IPC를 사용했다.
- GPU 텐서를 CPU로 내렸다가 다시 GPU로 올리는
Device → Host → Device복사를 제거해 컴포넌트 간 통신 지연을 줄였다.
Cascaded Streaming Pipeline
- Thinker가 전체 텍스트를 생성한 뒤 Talker를 실행하는 순차 방식은 첫 음성 응답까지 수 초가 걸릴 수 있다.
- 세 컴포넌트를 비동기 태스크와 큐로 연결해 파이프라인을 겹쳐 실행했다.
- Thinker가 첫 청크를 생성하면 Talker는 즉시 음성 토큰 생성을 시작한다.
- Talker가 앞선 토큰을 생성하는 동안 VoiceBox는 이전 토큰 청크를 오디오로 합성한다.
- 각 단계가 동시에 진행되므로 전체 처리 시간보다 첫 음성까지의 체감 지연을 크게 줄일 수 있다.
프로세스 분리와 장애 격리
- Thinker와 Talker는 각각 독립적인 vLLM 엔진으로 실행된다.
- 하나의 프로세스에서 두 엔진을 구동하면 CUDA 컨텍스트 충돌이나 GPU 메모리 관리 간섭이 발생할 수 있다.
- 따라서 두 모델을 별도 프로세스로 분리하고 각자의 CUDA 컨텍스트에서 실행했다.
- 프로세스 생성에는
fork대신spawn을 사용했다.fork: 부모의 CUDA 상태와 메모리를 복제해 충돌 위험이 있다.spawn: 깨끗한 Python 인터프리터에서 시작해 GPU 상태 오염을 줄인다.
- Thinker가 OOM으로 종료되더라도 Talker와 API 서버까지 함께 영향을 받지 않아 독립적인 재시작과 장애 대응이 가능하다.
vLLM continuous batching을 이용한 동시 처리
- 요청마다 이미지·오디오 입력 크기와 누적 임베딩 길이가 달라 직접 배치를 구성하기 어렵다.
- 수동 배칭은 padding 낭비와 복잡한 동기화 문제를 유발한다.
- Kanana-Omni Server는 배치 구성을 vLLM의 continuous batching 스케줄러에 위임했다.
- 서버는 요청을 비동기로 빠르게 엔진에 제출하고, vLLM이 GPU 메모리와 요청 상태를 고려해 forward pass를 묶는다.
- 각 요청은
request_id로 결과를 구분하므로 여러 요청이 하나의 배치에서 처리돼도 개별 응답을 독립적으로 스트리밍할 수 있다. - 내부 루프는 요청을 받자마자
asyncio.create_task(generate(...))로 실행해 다음 요청을 즉시 수락한다.
FastAPI workers=1과 비동기 처리
- 일반적인 FastAPI 서버처럼 여러 워커를 실행하면 각 워커가 vLLM 모델을 별도로 로드한다.
- 그 결과 GPU 메모리 사용량과 모델 로딩 시간이 워커 수만큼 증가해 멀티워커 구성이 현실적으로 어렵다.
- 모델별 서버를 따로 두면 워커 확장은 가능하지만, Thinker와 Talker 사이의 임베딩이 네트워크를 거쳐 zero-copy 이점을 잃게 된다.
- 따라서 API 서버는
workers=1로 운영하고, 단일 이벤트 루프가 블로킹되지 않도록 요청부터 오디오 생성까지 전체 경로를async/await로 구성했다. - 단일 워커 환경에서는 동기 작업 하나가 모든 사용자의 처리를 막을 수 있으므로, 끝에서 끝까지 비동기 설계가 필수다.
실시간 멀티모달 모델을 서비스할 때는 모델 자체보다 모델 간 데이터 이동, 스트리밍 타이밍, GPU 메모리 관리, 장애 격리가 성능을 좌우한다. 특히 모델 간 임베딩 전달이 빈번하다면 공유 메모리와 CUDA IPC를 검토하고, 긴 생성 파이프라인은 비동기 스트리밍과 continuous batching으로 구성하는 것이 효과적이다.