TuBrief
구독 채널
비디오
커뮤니티

Cómo un desarrollador solo resolvió las fugas de memoria y la latencia al desplegar Supertonic 3 en un servidor de 2 vCPU

TuBrief 편집팀
2026년 8월 24일
0
Computing/Software

원본 영상을 바탕으로 AI의 도움을 받아 작성했습니다. 원본 영상이 기준입니다.

Español한국어English中文العربيةहिन्दीDeutschFrançaisPortuguêsРусский日本語Bahasa Indonesia

관련 영상

Los desarrolladores por fin podrían tener un modelo de TTS local que valga la pena7:58

Los desarrolladores por fin podrían tener un modelo de TTS local que valga la pena

Better Stack

커뮤니티의 다른 글

사내 시스템에 llm api 붙일 때 마주하는 현실적인 한계와 대응법

2026년 9월 13일

레거시 백엔드에 GPT-6 Astra 붙일 때 예산 승인과 보안 통과를 먼저 끝내는 법이 있습니다

2026년 9월 13일

에이전트끼리 대화하다 6천만 원 청구서가 나오는 이유

2026년 9월 13일

사내 RAG 벡터 검색에 Okta 권한 필터를 직접 거는 방법

2026년 9월 13일

브라우저 에이전트에게 내 구글 계정을 통째로 넘기면 안 되는 이유

2026년 9월 12일

Apple Won the AI Race

2026년 9월 12일

댓글 (0)

Log in to leave a comment

아직 작성된 글이 없습니다

© 2026 . All rights reserved.

TuBrief
구독 채널
비디오
커뮤니티
로그인

Cómo un desarrollador solo resolvió las fugas de memoria y la latencia al desplegar Supertonic 3 en un servidor de 2 vCPU

Cuando gestionas un SaaS en solitario con un presupuesto inferior a 500.000 wones al mes, las facturas de la API de TTS de pago son una carga pesada mes a mes. La latencia de la red también es un problema. Si tardas entre 1 y 2 segundos desde que haces clic en un botón en la pantalla hasta que se reproduce el audio, el usuario cierra la pestaña de inmediato.

Supertonic 3 (Supertonic 3), un modelo de código abierto con 99 millones de parámetros, es una alternativa atractiva. Ejecutarlo directamente en un servidor local puede reducir el costo de la API a cero.

Sin embargo, probar unas pocas líneas de código de ejemplo en Python y realizar un despliegue en producción real son cosas completamente distintas. Aquí expongo la forma en que resolví directamente los problemas de cuello de botella de memoria y procesamiento asincrónico que surgen en cuanto levantas este modelo en un servidor de bajos recursos.

1. Por qué los procesos mueren en un servidor con 1 GB de RAM y ajuste de la sesión de ONNX

El tamaño del archivo de pesos ONNX de Supertonic 3 es de aproximadamente 305 MB. Cuando el modelo se carga por primera vez en la memoria, la memoria residente (RSS) se mantiene alrededor de los 350 MB.

El problema ocurre cuando un usuario envía una solicitud y comienza a procesar los tensores de audio de 44.1 kHz. El pico de memoria instantáneo supera los 900 MB. Si utilizas la instancia más económica con especificaciones de 1 vCPU / 1 GB de RAM, el OOM Killer de Linux se activa y finaliza abruptamente el proceso de Python.

El límite inferior para un funcionamiento estable es una instancia de 2 vCPU / 2 GB de RAM.

Especificación de la instancia del servidor RAM en estado inactivo RAM en pico de procesamiento Uso promedio de CPU Factor de tiempo real (RTF) Reducción de costos mensuales estimados
1 vCPU / 1 GB de RAM 280 MB 890 MB (riesgo de cierre forzoso) 98% 0.85 (tarda 0.85s en generar 1s de audio) $130 (vs. API comercial)
2 vCPU / 2 GB de RAM 320 MB 920 MB (zona estable) 48% (con límite de hilos) 0.28 (tarda 0.28s en generar 1s de audio) $120 (vs. instancia de GPU)
4 vCPU / 4 GB de RAM 350 MB 950 MB 25% 0.15 $90 (ajuste por sobreasignación)

Para evitar que el uso de la CPU se dispare al 100% cuando llegan múltiples solicitudes en un servidor con una CPU de bajos recursos, es necesario controlar manualmente el grupo de hilos del tiempo de ejecución de ONNX.

  • Se ajusta intra_op_num_threads al número de núcleos físicos del servidor (2).
  • Se establece execution_mode en ORT_SEQUENTIAL y inter_op_num_threads en 1 para evitar cambios de contexto innecesarios.
  • Se activa enable_cpu_mem_arena para prevenir reasignaciones frecuentes de memoria en el montón (heap), y se configura allow_spinning en 0 para evitar que la CPU permanezca en bucles vacíos esperando.

`python
import onnxruntime as ort

def get_optimized_session_options(cpu_cores: int = 2) -> ort.SessionOptions:
options = ort.SessionOptions()
options.intra_op_num_threads = cpu_cores
options.execution_mode = ort.ExecutionMode.ORT_SEQUENTIAL
options.inter_op_num_threads = 1
options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
options.enable_cpu_mem_arena = True
options.add_session_config_entry("session.dynamic_block_base", "4")
options.add_session_config_entry("session.intra_op.allow_spinning", "0")
return options

`

Al aplicar estas opciones y levantar el trabajador, puedes mantener el uso promedio de la CPU por debajo del 50% en un entorno de 2 vCPU. Incluso sin usar un servidor con una GPU costosa, te ahorras unos 120 dólares al mes en costos de infraestructura.

2. Conflictos de bibliotecas de C++ y manejo de excepciones en el preprocesamiento de texto

Al cargar el SDK en un entorno local o en un contenedor de despliegue, a menudo ocurren conflictos con las bibliotecas dinámicas de C++.

  • Entorno de Windows: Si aparece ImportError: DLL load failed, instala el paquete redistribuible de Microsoft Visual C++ 2015-2022 y asegúrate de que Python esté en un entorno virtual de 64 bits.
  • Entorno de Mac: El compilador Clang no tiene OpenMP, lo que genera el error libomp.dylib. Ejecuta brew install libomp en la terminal y añade la ruta de lib (export DYLD_LIBRARY_PATH="$(brew --prefix libomp)/lib:$DYLD_LIBRARY_PATH") a las variables de entorno.
  • Entorno de Docker: Al usar la imagen python:3.10-slim, instala previamente los paquetes build-essential y libgomp1 mediante apt.

Una vez configurado el entorno, debes adjuntar un depurador de texto de entrada. Si se mezclan abreviaturas en inglés, números y símbolos, el modelo puede corromper la pronunciación o emitir extraños ruidos mecánicos.

`python
import re
from typing import Dict

class SupertonicTextNormalizer:
def init(self):
self.lexicon_map: Dict[str, str] = {
"FastAPI": "패스트 에이피아이",
"SaaS": "새스",
"TTS": "티티에스",
"ONNX": "온닉스",
"Python": "파이썬",
"SDK": "에스디케이",
"API": "에이피아이",
}
self.currency_pattern = re.compile(r'(\d+)\s원')
self.date_pattern = re.compile(r'(\d{4})년\s
(\d{1,2})월\s*(\d{1,2})일')
self.time_pattern = re.compile(r'(\d{1,2}):(\d{2})')
self.special_char_pattern = re.compile(r'[^\w\s.,!?~<>]')

def normalize(self, text: str) -> str:
    if not text or not text.strip():
        raise ValueError("입력 텍스트가 비어 있습니다.")

    for word, pronunciation in self.lexicon_map.items():
        text = re.sub(rf'\b{re.escape(word)}\b', pronunciation, text, flags=re.IGNORECASE)

    text = self.date_pattern.sub(r'\1년 \2월 \3일', text)
    text = self.time_pattern.sub(r'\1시 \2분', text)

    tags = re.findall(r'<[^>]+>', text)
    text_placeholder = re.sub(r'<[^>]+>', ' ___TAG___ ', text)
    text_cleaned = self.special_char_pattern.sub('', text_placeholder)
    for tag in tags:
        text_cleaned = text_cleaned.replace('___TAG___', tag, 1)

    return re.sub(r'\s+', ' ', text_cleaned).strip()

`

Para evitar situaciones en las que el módulo de inferencia de C++ entre en una espera infinita con ciertos patrones de texto, establece un tiempo de espera con asyncio.wait_for y añade un código defensivo que devuelva un audio de guía de error preparado en caso de fallo.

`python
import asyncio
import logging

logger = logging.getLogger("TTSPipeline")

async def synthesize_with_fallback(tts_engine, text: str, voice_style, timeout_sec: float = 3.0) -> bytes:
try:
normalizer = SupertonicTextNormalizer()
cleaned_text = normalizer.normalize(text)

    loop = asyncio.get_running_loop()
    wav_data = await asyncio.wait_for(
        loop.run_in_executor(
            None, 
            lambda: tts_engine.synthesize(text=cleaned_text, lang="ko", voice_style=voice_style)
        ),
        timeout=timeout_sec
    )
    return wav_data
except Exception as err:
    logger.error(f"TTS 추론 실패 또는 타임아웃: {err}")
    with open("static/audio/fallback_system_error.wav", "rb") as f:
        return f.read()

`

Pasar por esta canalización de preprocesamiento reduce significativamente los errores de reproducción de voz causados por una pronunciación incorrecta. Durante la fase de QA, te ahorras de cinco a seis horas cada semana comprobando y corrigiendo problemas de pronunciación uno por uno.

3. Grupo de procesos que no bloquea el bucle de eventos de FastAPI y transmisión en memoria

Si ejecutas el inferenciador de Supertonic 3 (que es una función síncrona) directamente dentro de un enrutador asíncrono de FastAPI, surgirán problemas. Todo el bucle de eventos único se detiene hasta que finaliza el cálculo de C++, lo que provoca que incluso las solicitudes de API ligeras de otros usuarios entren en un estado de espera.

Las tareas de inferencia de uso intensivo de cómputo deben delegarse a un ProcessPoolExecutor independiente.

`python
from fastapi import FastAPI, HTTPException
from fastapi.responses import StreamingResponse
from concurrent.futures import ProcessPoolExecutor
import asyncio
import io
import os

app = FastAPI()
process_pool = ProcessPoolExecutor(max_workers=min(4, os.cpu_count() or 1))

def sync_tts_inference(text: str, voice_style_name: str):
from supertonic import TTS
tts = TTS(auto_download=False)
style = tts.get_voice_style(voice_style_name)
wav, _ = tts.synthesize(text=text, lang="ko", voice_style=style)
return wav.tobytes()

@app.post("/api/v1/tts/realtime")
async def generate_speech_realtime(text: str, voice: str = "M1"):
loop = asyncio.get_running_loop()
try:
audio_bytes = await loop.run_in_executor(process_pool, sync_tts_inference, text, voice)
return StreamingResponse(io.BytesIO(audio_bytes), media_type="audio/wav")
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

`

Una estructura que escribe el audio generado en el disco como archivo y luego lo lee para devolverlo desgasta la E/S de disco del servidor de bajos recursos. Transmitir directamente desde la memoria con io.BytesIO sin pasar por el almacenamiento de archivos es mucho más rápido.

Si el tráfico se acumula, la cola de espera se alarga o necesitas procesar oraciones largas, separa las solicitudes usando una cola de Redis y trabajadores de Celery.

  • Cuando un usuario envía texto, el servidor emite inmediatamente un task_id y finaliza la respuesta con un HTTP 202.
  • El trabajador de Celery ejecuta el modelo en un proceso en segundo plano para generar la voz.
  • Una vez completada la generación, los datos binarios se transfieren al cliente a través de WebSockets pasando por Redis Pub/Sub.

Si inevitablemente debes dejar una caché de archivos temporales en el disco, ejecuta una tarea de limpieza en segundo plano para prevenir fallos por disco lleno (Disk Full).

`python
import os
import time
import glob

AUDIO_CACHE_DIR = "/tmp/supertonic_audio_cache"
MAX_FILE_AGE_SECONDS = 600

def cleanup_ephemeral_audio_files():
now = time.time()
if not os.path.exists(AUDIO_CACHE_DIR):
return
for filepath in glob.glob(os.path.join(AUDIO_CACHE_DIR, "*.wav")):
try:
if now - os.path.getmtime(filepath) > MAX_FILE_AGE_SECONDS:
os.remove(filepath)
except Exception:
pass

`

Al contar con aislamiento de procesos y transmisión en memoria, puedes mantener la latencia p95 en torno a los 200 milisegundos incluso al procesar solicitudes simultáneas en una instancia de 2 vCPU. Puedes integrar de manera estable un servicio de voz independiente en el dispositivo, sin preocuparte por sustos en la factura de la API externa.