로컬 TTS 메모리를 5GB에서 0.8GB로 — chatterbox에서 Kokoro로
지난 글에서 저는 로컬 영상 파이프라인의 TTS 프로세스가 SIGKILL로 죽은 사건을 다뤘습니다. 램이 아니라 디스크 여유가 부족해 스왑을 늘리지 못한 게 원인이었고, 응급 처방으로 절대값 기준의 preflight 가드를 세웠다는 이야기였습니다. 그런데 그 글 끝에서 스스로 인정했듯이, 가드는 방어일 뿐입니다. 위험한 순간에 멈춰 세우기는 해도, 프로세스가 요구하는 메모리 자체를 줄여주지는 않습니다.
OOM의 근본 해결은 결국 메모리를 실제로 줄이는 것이라는 생각이 들었습니다. 그래서 TTS 엔진을 처음부터 다시 검토하기로 했습니다. 이 글은 5GB짜리 엔진을 0.8GB짜리로 갈아탄 기록입니다.
chatterbox의 5GB 메모리는 버그가 아니었습니다
먼저 짚어둘 것이 있습니다. chatterbox가 fp32 PyTorch로 돌 때 RSS로 5~6GB를 쓰는 것 자체는 비정상이 아닙니다. 그 규모의 모델이라면 원래 그 정도를 씁니다. 즉 5GB는 버그가 아니라 정상 사용량입니다.
문제는 다른 데 있었습니다. 반복해서 쓸 때 메모리가 조금씩 새는 것이었습니다. 한 번 작업할 때 음성 클립을 여러 개 만들어야 하고, 클립마다 generate()를 새로 호출하는 구조라, 누수가 쌓이면 결국 앞 글에서 본 스왑 폭증으로 이어집니다.
알려진 누수였고, 완전히는 막지 못했습니다
확인해 보니 이건 저만 겪는 일이 아니었습니다. generate() 호출마다 대략 130~430MB가 새는 현상이 MPS 기준으로 보고돼 있었고(GitHub 이슈 #218, #352, #205), CPU 모드에서는 그 정도가 10배까지 나빠집니다. gc와 torch.mps.empty_cache를 조합해 봤지만 누수의 70% 정도만 완화될 뿐이었습니다.
근본 원인은 PyTorch MPS의 캐싱 얼로케이터에 있습니다. 해제된 메모리를 곧바로 OS에 돌려주지 않고 그래프 캐시(graphCache) 형태로 쥐고 있는 동작(pytorch#105839) 때문에, 애플리케이션 레벨에서 아무리 정리해도 한계가 있었습니다. 그렇다면 fp16으로 절반이라도 줄여보자 싶었는데, 여기서 또 막혔습니다.
# PyPI 최신 chatterbox-tts 0.1.7
from_pretrained(device) # dtype 파라미터 자체가 없음 → fp16 지정 불가
설치본인 chatterbox-tts 0.1.7의 from_pretrained에는 dtype을 넘길 자리가 아예 없었습니다. 결국 메모리 절감은 엔진 안에서 옵션으로 풀 문제가 아니라, 엔진을 바꿔야 하는 문제라는 결론에 이르렀습니다.
Kokoro로 갈아탄 이유
대안으로 고른 것은 mlx-audio와 Kokoro-82M입니다. Kokoro는 Apple Silicon에 네이티브인 MLX 프레임워크 위에서 도는 경량 TTS입니다. 마침 제 경우에는 목소리가 고정이어도 전혀 문제가 없었습니다. 특정 화자의 목소리를 복제할 필요가 없으니, 보이스 클론을 포기하는 대신 가벼움을 얻는 교환이 딱 들어맞았습니다.
실측 비교는 다음과 같습니다.
| 항목 | chatterbox (fp32 PyTorch MPS) | Kokoro-82M (mlx-audio) |
|---|---|---|
| peak RSS | 5.0GB | 0.78GB |
| 카드당 생성 | 15초~9분 (스왑 요동) | 약 0.7초 |
| 역전사 정확도 | 통과 | whisper 4/4 원문 완전 일치 |
| 모델 크기 | 약 3GB 가중치 | 349MB |
| 보이스 클론 | 가능 | 불가 (고정 음성) |
peak RSS가 5.0GB에서 0.78GB로 내려갔습니다. 앞 글의 스왑 폭증을 떠올리면, 이 정도면 애초에 스왑을 건드릴 일이 없는 수준입니다. 카드 한 장을 만드는 데 최악의 경우 9분까지 걸리던 것이 0.7초로 줄어든 것도 극적이었습니다. 다만 그 9분은 순수한 연산 시간이 아니라 스왑에 눌린 시간이었다는 점을 앞 글에서 이미 확인한 터라, 이 개선은 오히려 자연스럽게 납득이 갔습니다.
물론 트레이드오프는 분명합니다. Kokoro는 목소리를 복제할 수 없고, af_heart·af_bella·bm_george 같은 고정 음성 중에서 고르는 방식입니다. 만약 특정인의 목소리가 꼭 필요한 작업이라면, chatterbox를 폴백으로 남겨두는 하이브리드 구성이 현실적이라는 생각이 들었습니다. 제 경우엔 그럴 필요가 없어서 Kokoro 단일로 갔습니다.
설치에서 만난 두 개의 함정
갈아타는 길이 매끄럽지만은 않았습니다. 두 가지 함정을 실제로 밟았습니다.
첫째, mlx-audio만 설치하면 끝이 아니었습니다. 영어 음소 변환(G2P)을 담당하는 misaki[en]를 따로 깔아야 했고, 이걸 빠뜨리면 설치 시점이 아니라 정작 음성을 생성하려는 순간에 이런 에러가 납니다.
Kokoro requires the optional 'misaki' package
둘째는 조금 더 성가셨습니다. mlx-audio 0.4.4의 istftnet 보코더에 버그가 있었습니다. 내부에서 interpolate()의 출력 크기를 계산할 때 올림(ceil)을 쓰는데, PyTorch 원본은 내림(floor)을 씁니다. 이 한 칸 차이 때문에 특정 텍스트 길이에서 [broadcast_shapes] 크래시가 났고, 하필 실전 카드 길이에서 재현됐습니다.
# 우회 방식 (개념)
SineGen._f02sine 의 출력을 원래 길이로 slice → 한 칸 어긋남 흡수
# site-packages 원본은 수정하지 않고, 헬퍼 안에서 self-contained 하게 패치
site-packages를 직접 건드리면 재현성이 깨지므로, 헬퍼 안에서 SineGen._f02sine의 결과를 원래 길이로 잘라내는 monkey-patch로 감쌌습니다. 라이브러리 원본은 그대로 두고, 제 코드 안에서만 유효하도록 self-contained하게 처리한 것입니다.
가장 중요한 설계는 "분리"였습니다
엔진을 바꾸면서 다시 확인한 원칙이 하나 있습니다. 앞 글과도 이어지는 이야기인데, TTS 로드와 ffmpeg 렌더를 같은 프로세스에서 하지 않는 것입니다. TTS만 담당하는 단명(short-lived) 프로세스를 먼저 띄워 음성을 전부 만들고 종료시키면, OS가 그 메모리를 전량 회수합니다. 그다음 렌더 단계는 무거운 모델을 아예 올리지 않습니다.
모델이 무거울수록 이 분리가 OOM 방어의 핵심이 됩니다. 앞 글의 사고도 따지고 보면 TTS와 렌더를 한 스크립트에 욱여넣은 데서 비롯됐습니다. 여기에 결정적 도구(whisper 전사, ffmpeg 렌더)와 판단이 필요한 부분을 분업시키고 외부 LLM API는 쓰지 않는다는 원칙까지 더하면, 파이프라인 전체가 한결 예측 가능해집니다.
이번 일에서 제가 얻은 교훈은 이렇게 정리됩니다. 정상 사용량이라도 반복과 중첩, 그리고 플랫폼 특유의 메모리 회계(MPS)를 만나면 얼마든지 OOM이 될 수 있습니다. 방어(가드)와 근본(경량 엔진)은 서로 다른 층위의 해결책이고, 애석하게도 둘 중 하나만으로는 부족합니다. 결국 둘 다 필요하다는 것이 이번 삽질에서 얻은 결론이었습니다.
관련 글
내 목소리를 무료로 AI에 클로닝하기 — Mac 오픈소스 TTS 실전기
무료 플랜이 있다고 내 목소리가 무료로 클론되는 건 아니었습니다. Mac에서 오픈소스 TTS(XTTS·Chatterbox)로 AI 음성 클로닝을 직접 시도하며, 라이선스가 가른 선택과 레퍼런스 정제·파라미터 튜닝·GPU 신화까지 더듬어본 실전기입니다.
RAM 32GB가 남는데 프로세스가 죽었다 — macOS 스왑과 Jetsam OOM
RAM 32GB가 넉넉히 남아도 macOS 프로세스는 SIGKILL로 죽을 수 있습니다. 진짜 원인은 디스크 여유 공간 부족으로 동적 스왑을 늘리지 못한 것이었고, Jetsam이 개입하는 위험의 임계는 사용률 %가 아니라 절대 여유 GB라는 사실을 실측으로 확인했습니다.
영상에서 텍스트를 뽑고 싶었다 — ffmpeg, whisper-cpp, VAD로 로컬 STT 파이프라인 만들기
영상 파일에서 텍스트를 추출하는 로컬 STT 파이프라인을 구축한 경험입니다. ffmpeg가 오디오를 왜 16kHz 모노 PCM으로 변환하는지, whisper-cpp가 Python 없이 어떻게 동작하는지, VAD가 STT 품질을 어떻게 개선하는지 하나씩 따라가봅니다.