Mengatasi Kebocoran Memori dan Latensi Saat Pengembang Solo Menjalankan Supertonic 3 di Server 2 vCPU
Saat mengelola SaaS mandiri dengan anggaran di bawah 500.000 won per bulan, tagihan API TTS berbayar menjadi beban yang berat setiap bulannya. Latensi jaringan juga menjadi masalah. Jika butuh waktu 1 hingga 2 detik hingga suara keluar setelah tombol ditekan di layar, pengguna akan langsung menutup tab tersebut.
Supertonic 3 (Supertonic 3), model sumber terbuka dengan 99 juta parameter, adalah alternatif yang menarik. Menjalankannya secara lokal di server dapat memangkas biaya API menjadi 0.
Namun, menjalankan beberapa baris kode contoh Python sangat berbeda dengan penerapan di produksi nyata. Berikut adalah rangkuman cara saya langsung menyelesaikan masalah hambatan memori dan pemrosesan asinkron yang dihadapi saat menjalankan model ini di server berspesifikasi rendah.
1. Alasan Proses Berhenti di Server RAM 1GB dan Penyetelan Sesi ONNX
Ukuran file bobot ONNX Supertonic 3 adalah sekitar 305MB. Saat model pertama kali dimuat ke dalam memori, Resident Set Size (RSS) berada di kisaran 350MB.
Masalah terjadi ketika pengguna mengirim permintaan dan proses penghitungan tensor audio 44,1kHz dimulai. Puncak memori sesaat melampaui 900MB. Jika Anda menggunakan instance termurah dengan spesifikasi 1 vCPU / 1GB RAM, OOM Killer Linux akan langsung aktif dan menghentikan paksa proses Python.
Batas aman untuk pengoperasian yang stabil adalah instance 2 vCPU / 2GB RAM.
| Spesifikasi Instance Server |
RAM Idle |
RAM Puncak Komputasi |
Penggunaan CPU Rata-rata |
Real-Time Factor (RTF) |
Estimasi Penghematan Biaya Bulanan |
| 1 vCPU / 1GB RAM |
280 MB |
890 MB (Risiko Penghentian Paksa) |
98% |
0.85 (Butuh 0.85 detik untuk menghasilkan 1 detik audio) |
$130 (Dibandingkan API komersial) |
| 2 vCPU / 2GB RAM |
320 MB |
920 MB (Zona Aman) |
48% (Saat utas dibatasi) |
0.28 (Butuh 0.28 detik untuk menghasilkan 1 detik audio) |
$120 (Dibandingkan instance GPU) |
| 4 vCPU / 4GB RAM |
350 MB |
950 MB |
25% |
0.15 |
$90 (Penyetelan instance over-provisioned) |
Untuk mencegah penggunaan CPU melonjak hingga 100% saat beberapa permintaan masuk di server CPU berspesifikasi rendah, Anda harus mengontrol kumpulan utas runtime ONNX secara manual.
- Sesuaikan
intra_op_num_threads dengan jumlah inti fisik server (2 inti).
- Tetapkan
execution_mode ke ORT_SEQUENTIAL dan tentukan inter_op_num_threads ke 1 untuk mencegah context switching yang tidak perlu.
- Aktifkan
enable_cpu_mem_arena untuk mencegah alokasi ulang memori heap yang terlalu sering, dan atur allow_spinning ke 0 untuk menghentikan fenomena CPU menunggu dalam idle loop.
`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
`
Dengan menerapkan opsi ini dan menjalankan pekerja (worker), penggunaan CPU rata-rata dapat dikendalikan di bawah 50% di lingkungan 2 vCPU. Anda dapat menghemat biaya infrastruktur sekitar $120 per bulan tanpa harus menggunakan server GPU yang mahal.
2. Konflik Pustaka C++ dan Penanganan Pengecualian Pra-pemrosesan Teks
Konflik pustaka dinamis C++ sering terjadi saat memuat SDK di lingkungan lokal atau kontainer penerapan.
- Lingkungan Windows: Jika
ImportError: DLL load failed muncul, instal Paket Redistributable Microsoft Visual C++ 2015-2022 dan pastikan lingkungan virtual Python adalah 64-bit.
- Lingkungan Mac: Terjadi error
libomp.dylib karena kompiler Clang tidak memiliki OpenMP. Jalankan brew import libomp di terminal dan tambahkan jalur lib ke variabel lingkungan (export DYLD_LIBRARY_PATH="$(brew --prefix libomp)/lib:$DYLD_LIBRARY_PATH").
- Lingkungan Docker: Saat menggunakan citra
python:3.10-slim, instal terlebih dahulu paket build-essential dan libgomp1 melalui apt.
Setelah menyesuaikan lingkungan, Anda perlu memasang pembersih teks masukan. Jika singkatan bahasa Inggris, angka, dan simbol tercampur, model dapat merusak pelafalan atau menghasilkan suara mesin yang aneh.
`python
import re
from typing import Dict
class SupertonicTextNormalizer:
def init(self):
self.lexicon_map: Dict[str, str] = {
"FastAPI": "FastAPI",
"SaaS": "SaaS",
"TTS": "TTS",
"ONNX": "ONNX",
"Python": "Python",
"SDK": "SDK",
"API": "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("Teks masukan kosong.")
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()
`
Untuk mencegah modul inferensi C++ masuk ke dalam kondisi tunggu tak terbatas pada pola teks tertentu, terapkan batas waktu (timeout) menggunakan asyncio.wait_for dan siapkan kode pertahanan untuk mengembalikan suara panduan kesalahan yang siap pakai jika terjadi kegagalan.
`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"Inferensi TTS gagal atau waktu habis: {err}")
with open("static/audio/fallback_system_error.wav", "rb") as f:
return f.read()
`
Melalui pipa pra-pemrosesan ini, kesalahan pemutaran suara akibat pelafalan yang salah akan berkurang secara drastis. Anda dapat menghemat waktu lima hingga enam jam setiap minggu yang biasanya dihabiskan untuk memeriksa dan memperbaiki masalah pelafalan satu per satu selama tahap QA.
3. Kumpulan Proses dan Streaming In-Memory yang Tidak Menghalangi Event Loop FastAPI
Masalah akan timbul jika Anda menjalankan inferensi Supertonic 3—yang merupakan fungsi sinkron—secara langsung di dalam router asinkron FastAPI. Seluruh event loop tunggal akan berhenti total hingga komputasi C++ selesai, menyebabkan permintaan API ringan dari pengguna lain ikut berada dalam status menunggu.
Tugas inferensi yang intensif komputasi harus dialihkan ke ProcessPoolExecutor terpisah.
`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))
`
Struktur yang menulis audio yang dihasilkan ke diska sebagai file lalu membacanya kembali untuk dikembalikan akan menguras I/O diska pada server berspesifikasi rendah. Jauh lebih cepat untuk melakukan streaming langsung di memori menggunakan io.BytesIO tanpa melewati penyimpanan file.
Jika antrean memanjang akibat lonjakan lalu lintas atau Anda perlu memproses kalimat panjang, pisahkan permintaan menggunakan antrean Redis dan pekerja Celery.
- Ketika pengguna mengirim teks, server langsung menerbitkan
task_id dan mengakhiri respons dengan HTTP 202.
- Pekerja Celery menjalankan model di proses latar belakang untuk menghasilkan suara.
- Setelah pembuatan selesai, data biner diteruskan ke klien melalui WebSocket melalui Redis Pub/Sub.
Jika struktur tersebut terpaksa meninggalkan singgah file sementara (temporary file cache) di diska, jalankan tugas pembersihan latar belakang untuk mencegah kegagalan 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
`
Dengan menerapkan isolasi proses dan streaming in-memory, Anda dapat mempertahankan latensi p95 dalam kisaran 200 milidetik bahkan di bawah permintaan serentak pada instance 2 vCPU. Anda dapat menghubungkan layanan suara on-device mandiri ke layanan Anda secara stabil tanpa mengkhawatirkan tagihan biaya API eksternal yang membengkak.