메뉴
HN
Hacker News • 35일 전

텍스트-음성 변환 모델을 50ms 미만 응답으로 만든 방법

IMP
7/10
핵심 요약

Qwen3-TTS 1.7B CustomVoice를 단일 NVIDIA H100에서 서빙하여 초당 10건의 요청(RPS)을 처리하면서도 p95 첫 오디오 지연시간(TTFA) 50ms 미만을 달성한 오픈소스 구현을 소개한 글이다. 선행 무음 제거와 코덱 프레임 누적 튜닝을 통해 기존 vLLM-Omni, SGLang-Omni, VoxServe 등 대비 가장 낮은 지연시간을 확보했으며, 비용도 백만 자당 약 2달러로 ElevenLabs(100달러)나 Cartesia(49달러)보다 훨씬 저렴하다.

번역된 본문

TL;DR: 저희의 Qwen3-TTS 1.7B CustomVoice 구현은 단일 NVIDIA H100 SXM에서 초당 10건의 요청(RPS)을 처리하면서 p95 첫 오디오까지의 시간(TTFA) 50ms 미만을 달성하고 실시간 재생을 유지합니다. 저희는 Poisson 개방형 루프 트래픽 환경에서 다섯 가지 구현체(저희 것, vLLM-Omni, SGLang-Omni△, VoxServe, M*)를 비교했습니다. 각 구현체를 저지연 스트리밍에 맞게 튜닝한 후, 저희 구현만이 유일하게 p95 TTFA 50ms 미만을 달성했습니다. 저희는 10 RPS까지 p95 TTFA 50ms 미만을 유지하며, 20 RPS에서도 100ms 이하를 유지합니다. 저희 시스템은 10 RPS에서 초당 약 630자를 생성합니다. 1× H100 SXM 인스턴스가 시간당 4.29달러이므로, full utilization 기준 백만 자당 약 2달러로 환산됩니다. 비교하자면 ElevenLabs V3는 백만 자당 100달러, Cartesia Sonic 3.5는 더 높은 TTFA에서 백만 자당 49달러입니다. 저희는 구현체와 벤치마크를 오픈소스로 공개합니다. 방법론은 아래에 설명합니다.

"실시간" TTS 정의하기 실시간 TTS 서버가 달성해야 할 것을 논의해 보겠습니다. 네 부분으로 구성된 문제라고 생각합니다:

  1. 낮은 가청 TTFA: 요청 전송부터 첫 가청 샘플까지의 시간이 짧아야 합니다.
  2. 언더런 없음: 재생이 시작되면 클라이언트의 버퍼가 소진되지 않아야 합니다.
  3. 용량: RPS가 증가해도 1과 2가 유지되어야 합니다.
  4. 정상 출력: 음성이 명확하게 이해 가능해야 합니다.

Qwen3-TTS CustomVoice 1.7B는 허용적인 라이선스를 가진 가장 인기 있는 TTS 모델 중 하나이기 때문에 선택했습니다. 위 정의에 따라 단일 NVIDIA H100 SXM에서 높은 RPS를 유지하면서 언더런 없이 낮은 p95 가청 TTFA를 목표로 했습니다. 모든 벤치마크는 Fireworks AI의 LLM 벤치마크를 따라 실제 워크로드에 근접하도록 Poisson 개방형 루프 트래픽에서 5분간 실행됩니다. 각 엔진은 전체 텍스트를 단일 HTTP 요청으로 받고, 오디오 출력은 스트리밍됩니다. 가청 TTFA를 감지하고, 수신된 PCM으로 재생을 재구성하며, 완성된 오디오를 Deepgram STT로 평가합니다.

다른 엔진들의 성능은? 아래 표는 각 엔진의 1 RPS에서 업스트림/기본 결과입니다. 이 실행에서는 호환성을 위한 변경만 적용했습니다.

| 엔진 | p95 가청 TTFA | p95 선행 무음 | 언더런 발생 요청 | | vLLM-Omni | 277.883 ms | 90 ms | 100% | | SGLang-Omni△ | 1,140.69 ms | 80 ms | 0% | | VoxServe | 315.064 ms | 30 ms | 0% | | M* | 1,159.956 ms | 90 ms | 0% |

이 기본값들은 개선 여지가 상당합니다. 저희는 각 서빙 엔진을 자체적인 지연시간, 연속성, 품질, 용량 요구사항에 맞게 튜닝했습니다.

  1. 선행 무음 제거 모델이 반환하는 첫 PCM에는 첫 지속 사운드 이전에 수십 밀리초의 무음이 포함될 수 있습니다. 이 간극은 가청 TTFA를 이만큼 뒤로 밀어냅니다. 저희는 동적 트림을 추가했습니다. 짧은 RMS 윈도우에서 지속 음성을 감지하고, 시작점 이전의 샘플을 제거한 뒤 나머지 오디오를 정상적으로 스트리밍합니다. 이 변경으로 TTFA가 약 80ms 개선되지만 모델 추론 자체를 빠르게 하지는 않습니다.

  2. 프레임 누적 튜닝 또한 디코딩 및 오디오 청크 릴리스 전에 수집할 코덱 프레임 수를 튜닝했습니다. 초기 청크가 작으면 TTFA는 줄지만 재생 여유 버퍼가 줄어들고 디코더 작업이 더 자주 발생합니다. 청크가 크면 배칭이 쉽고 연속 재생이 안전해지지만 첫 가청 출력이 지연됩니다. 따라서 유용한 구성은 작은 청크로 시작해 이후 출력에는 청크 크기를 늘리는 것입니다. 정확한 설정값은 엔진마다 다릅니다. vLLM-Omni는 codec_chunk_frames와 codec_chunk_ramp 같은 설정을 제공하며, 다른 엔진들도 동등한 청크 또는 스트라이드 제어를 제공합니다. 저희는 이 값들을 반복 탐색하며 낮은 p95 TTFA, 언더런 없음, 부하 증가 시 안정적 동작에 가장 부합하는 설정을 찾았습니다.

기존 서빙 엔진 튜닝 후 성능 다음 표는 선행 무음 및 프레임 누적 튜닝 후 각 엔진의 언더런 없는 프로파일 결과입니다.

| 엔진 | p95 TTFA (1 RPS) | p95 TTFA (6 RPS) | | vLLM-Omni | 56.815 ms | 93.451 ms | | SGLang-Omni△ | 120.879 ms | 273.700 ms | | VoxServe | 49.3 ms | 363.2 ms | | M* | 104.035 ms | 179.501 ms |

VoxServe는 1 RPS에서 p95 TTFA 50ms 미만을 달성하지만, 다른 엔진들은...

원문 보기
원문 보기 (영어)
TL;DR Our Qwen3-TTS 1.7B CustomVoice implementation achieves 10 requests per second (RPS) and sub-50 ms p95 time-to-first-audio (TTFA) while maintaining real-time playback on a single NVIDIA H100 SXM. × We compare five implementations: ours, vLLM-Omni, SGLang-Omni △ , VoxServe, and M*, under Poisson open-loop traffic. After tuning each implementation for low-latency streaming, ours is the only one to achieve sub-50 ms p95 TTFA . We maintain sub-50 ms p95 TTFA through 10 RPS and keep it below 100 ms even at 20 RPS . Our system produces approximately 630 characters per second at 10 RPS. At $4.29 per hour for a 1× H100 SXM instance, this translates to ~$2 per 1M characters at full utilization 1 . For comparison , ElevenLabs V3 is $100 / 1M and Cartesia Sonic 3.5 is $49 / 1M at a higher TTFA . We open source the implementation and benchmark . Our methodology is explained below. Defining “Real-time” TTS Let’s start by discussing what a real-time TTS server needs to achieve. We think it’s a four-part problem: Low Audible TTFA: Time from request dispatch to the first audible sample must be low. Zero underruns: Once playback starts, the client must not run out of buffered audio. Capacity: 1 and 2 must hold as RPS increases. Non-malformed output: Speech must be intelligible. We choose Qwen3-TTS CustomVoice 1.7B because it is one of the most popular TTS models with a permissive license. Based on the above definition, we target low p95 audible TTFA with zero underruns while maintaining high RPS on a single NVIDIA H100 SXM. All benchmarks run for five minutes under Poisson open-loop traffic to approximate real workloads, following Fireworks AI’s LLM benchmark . Each engine receives the complete text in a single HTTP request, while audio output remains streamed. We detect audible TTFA, reconstruct playback from received PCM, and evaluate the completed audio using Deepgram STT. How Do Other Engines Perform? The table below shows the upstream/default result at 1 RPS for each engine. We only apply changes for compatibility in this run. Engine p95 audible TTFA p95 leading silence Requests with underruns vLLM-Omni 277.883 ms 90 ms 100% SGLang-Omni △ 1,140.69 ms 80 ms 0% VoxServe 315.064 ms 30 ms 0% M* 1,159.956 ms 90 ms 0% These defaults have substantial room for improvement. We tune each serving engine for its own latency, continuity, quality, and capacity requirements. 1. Remove leading silence The first PCM returned by a model can contain tens of milliseconds of silence before the first sustained sound. This gap pushes audible TTFA back like so: We add a dynamic trim. It detects sustained speech from short RMS windows, removes samples before onset, and streams the remaining audio normally. This change improves TTFA by ~80ms but does not make model inference itself faster. 2. Tune frame accumulation We also tune how many codec frames are collected before decoding and releasing an audio chunk. Smaller initial chunks reduce TTFA, but provide less playback headroom and create more frequent decoder work. Larger chunks are easier to batch and make continuous playback safer, but delay the first audible output. A useful configuration therefore starts with a small chunk and increases the chunk size for later output. The exact knobs differ by engine: vLLM-Omni exposes settings such as codec_chunk_frames and codec_chunk_ramp ; the other engines provide equivalent chunk or stride controls. We iterate over these values to find the config that best matches: low p95 TTFA, zero underruns and stable behavior as load increases. Performance after tuning existing serving engines The following table shows the selected no-underrun profile for each engine after leading-silence and frame-accumulation tuning. Engine p95 TTFA (1 RPS) p95 TTFA (6 RPS) vLLM-Omni 56.815 ms 93.451 ms SGLang-Omni △ 120.879 ms 273.700 ms VoxServe 49.3 ms 363.2 ms M* 104.035 ms 179.501 ms VoxServe reaches sub-50 ms p95 TTFA at 1 RPS, while the other three engines do not. By around 6 RPS, every engine is at roughly 100 ms p95 TTFA or higher 2 . How We Optimized Qwen3-TTS We first need to understand Qwen3-TTS architecture. It is a 3-part model performing hierarchical multi-codebook generation. The Talker predicts the first codebook token for each audio frame, the Code Predictor generates the remaining 15 codebook tokens, and the causal Codec converts codebook tokens into waveform samples. Each module has its own compute profile, batching behavior, and latency requirements. Rather than optimizing each module in isolation, we focus on a broader question: how should a serving system coordinate these heterogeneous tasks? 1. Bringing three modules under one scheduler Most Qwen3-TTS serving implementations are split into two stages: the Talker and Code Predictor run together, while the Codec runs separately. This separation enables token generation and waveform decoding to overlap across requests. We take this a step further. We expose the Talker, Code Predictor, and Codec as three independently schedulable tasks. The key is not merely splitting them into parts, but bringing all three onto a shared scheduling surface managed by one scheduler. This design draws inspiration from M* ( arXiv ). With this setup, the scheduler can decide whether to run the Talker, advance the Code Predictor, or prioritize a Codec job that is approaching its playback deadline. It can also batch requests waiting for the same module. Instead of following a fixed execution order, we can rearrange work according to urgency. Combining the Talker and Code Predictor may appear more efficient because it removes an intermediate boundary. However, the combined operation can become a non-preemptible unit of work that blocks more urgent Code Predictor or Codec jobs. Keeping the modules separate creates shorter units of work and gives the scheduler more opportunities to interleave requests. 2. Scheduling around the needs of speech streaming Speech streaming has two distinct notions of urgency. Before the first chunk of audio arrives, every millisecond increases TTFA, so we need to prioritize this path. But once playback begins, the goal changes: the next chunk only needs to arrive before the current audio finishes playing. Producing it earlier provides no user-visible benefit. Thus, we give high priority to requests that have not produced their first audio, while established streams become urgent only as they approach a playback deadline. Running every urgent request alone would destroy batching efficiency. Instead, our scheduler selects an urgent request as an anchor and fills the rest of the batch with compatible work. This helps the critical request meet its deadline while making effective use of the GPU. This policy works especially well because all three modules share a scheduling surface, allowing the scheduler to choose both the request and the pipeline stage to advance. 3. Exploiting the regular structure of the Code Predictor The Code Predictor is an autoregressive transformer, but its execution is unusually regular. It always performs a fixed number of steps (15) per frame to fill the remaining audio codebooks. We exploit its fixed structure to preallocate its KV cache and capture the entire frame-generation loop as a single CUDA graph. We also use a Triton attention kernel specialized for its short, bounded context. By replacing a host-driven sequence with a fixed GPU program, we lower latency and simplify the execution system. 4. Rebuilding the Codec around cached state The Qwen3-TTS Codec is made up of Transformers and CNNs. Generating the next audio chunk depends on both the Transformer context and convolutional state from previous chunks. A naive implementation reprocesses the full frame history on every update, repeatedly decoding old audio as the utterance grows. To avoid this, we use a state-cache-based Codec. Each request retains the Transformer context and convolutional state needed by the next