LAION · MOSS voice-acting · serving guide

Serving the 4.55B on SGLang-Omni (A100)

What this page is. A complete, copy-paste, for-dummies guide to serving laion/moss-tts-local-transformer-4.55b-voice-acting on a single NVIDIA A100-80GB with SGLang-Omni — the OpenAI-compatible speech server. It covers the exact install, the exact launch command, every trap we hit (with the verbatim error text), the full HTTP API, and a measured speed & tuning chapter with the knobs that maximize throughput for offline dataset generation.

Every number on this page was measured on one A100-80GB (SXM) with torch 2.9.1+cu128, SGLang-Omni 0.5.8, flashinfer 0.6.1, Python 3.12. Read numbers as "per one A100"; the model is small enough that one card is the natural unit.

1 · What SGLang-Omni is 2 · A100 setup (step-by-step) 3 · The serve command 4 · Run it as a service 5 · API reference 6 · Traps & fixes 7 · Speed & tuning 8 · Recipes 9 · Napkin math

1 · What SGLang-Omni is, and why it is the fast path

SGLang-Omni is a serving framework for omni / speech models built on top of SGLang. It runs the model as a pipeline of stages and exposes them behind an OpenAI-compatible HTTP server. For this model the pipeline is:

# three stages, one GPU
preprocessing  # tokenize text, encode any reference audio with the ~1B causal codec
   -> tts_engine   # the 4.55B Qwen3 backbone + 12-codebook local-transformer, run by SGLang
   -> vocoder      # MOSS-Audio-Tokenizer-v2 decodes RVQ codes back to 48 kHz stereo PCM

Why this is the fast path for the 4.55B (versus a plain transformers generate loop):

2 · A100 setup — step by step

2.0 · Assumptions

2.1 · Create the venv (uv) and install SGLang-Omni

We use uv. Install it once (curl -LsSf https://astral.sh/uv/install.sh | sh), then:

# create the venv ON A REAL DISK (see trap 5) — /home, not /tmp
uv venv /home/$USER/sglang_moss_venv --python 3.12
source /home/$USER/sglang_moss_venv/bin/activate

# install SGLang-Omni (this pulls torch 2.9.x+cu128, sglang, flashinfer, transformers, soundfile, ...)
uv pip install sglang-omni
# soundfile is needed to decode base64 / data-URI reference audio
uv pip install soundfile

Key package versions this guide was validated against (yours may be newer):

packageversionwhy it matters
python3.12runtime
torch2.9.1+cu128ships its own CUDA 12.8 runtime — no system CUDA needed to serve
sglang-omni0.5.8the server + moss_tts_local pipeline
flashinfer-python0.6.1attention kernels + CUDA-graph capture
transformers≥4.45 (remote-code)the model's custom code + processor
soundfilelatestdecode reference audio for voice cloning
flash-attnNOT installednot needed; FA2 path is broken for this remote code

2.2 · Download the model

# the LAION 4.55B voice-acting checkpoint (merged weights, Apache-2.0, public)
hf download laion/moss-tts-local-transformer-4.55b-voice-acting
# the v2 audio tokenizer / vocoder (pulled automatically on first run, or pre-fetch it)
hf download OpenMOSS-Team/MOSS-Audio-Tokenizer-v2

First run also JIT-compiles a flashinfer attention kernel into ~/.cache/flashinfer and captures CUDA graphs; expect the first startup to take a couple of minutes longer than later ones.

2.3 · The config file

SGLang-Omni selects the pipeline from a tiny YAML. The stock reference-less / voice-clone config is:

# examples/configs/moss_tts_local.yaml
config_cls: MossTTSLocalPipelineConfig
model_path: OpenMOSS-Team/MOSS-TTS-Local-Transformer-v1.5
relay_backend: shm

The model_path inside the YAML only names the architecture family; the actual weights come from the --model-path you pass on the command line (the LAION checkpoint). The three stages (preprocessing → tts_engine → vocoder) are filled in by MossTTSLocalPipelineConfig's defaults, so you do not have to spell them out. relay_backend: shm uses shared memory to move audio between stages (see trap 3).

3 · The serve command (every flag explained)

cd /path/to/sglang-omni          # repo dir that holds examples/configs/
env -u LD_LIBRARY_PATH \        # TRAP 1 — strip a leaked cuDNN path or the 1st request SIGABRTs
  CUDA_VISIBLE_DEVICES=6 \        # pin to one GPU
  sgl-omni serve \
    --model-path laion/moss-tts-local-transformer-4.55b-voice-acting \  # the LAION weights
    --config examples/configs/moss_tts_local.yaml \                      # the pipeline
    --port 8000
tokenwhat it does
env -u LD_LIBRARY_PATHRemoves an inherited LD_LIBRARY_PATH so stage subprocesses don't pick up a foreign cuDNN. Mandatory (trap 1).
CUDA_VISIBLE_DEVICES=6Which physical GPU to use. The process then sees it as cuda:0.
--model-pathHF id or local dir of the real weights to load (the LAION 4.55B).
--configThe pipeline YAML from 2.3.
--portHTTP port for the OpenAI-compatible API.

When it is up you will see Uvicorn running on http://0.0.0.0:8000 and Application startup complete. Sanity-check:

curl -s http://localhost:8000/v1/models

Note (trap 6): /v1/models reports the base model id (OpenMOSS-Team/MOSS-TTS-Local-Transformer-v1.5) — this is cosmetic. The LAION weights you passed with --model-path are the ones actually loaded and generating.

4 · Run it as a persistent background service

For a long-lived server, detach it and log to a file:

# start, fully detached, logging to a file
cd /path/to/sglang-omni
nohup env -u LD_LIBRARY_PATH CUDA_VISIBLE_DEVICES=6 \
  sgl-omni serve --model-path laion/moss-tts-local-transformer-4.55b-voice-acting \
    --config examples/configs/moss_tts_local.yaml --port 8000 \
  > ~/moss_server.log 2>&1 &
echo $! > ~/moss_server.pid          # remember the pid

# health / readiness
curl -s http://localhost:8000/v1/models >/dev/null && echo UP || echo "not ready"

# watch the log
tail -f ~/moss_server.log

# stop it cleanly (kills the stage subprocesses too)
kill "$(cat ~/moss_server.pid)"

Stopping gotcha. The server spawns one stage subprocess that holds the GPU memory. If you kill -9 only the parent, that child can survive and keep ~60 GB pinned. Always verify with nvidia-smi --query-compute-apps=pid,used_memory --format=csv and kill any orphan before relaunching, or you will hit a spurious CUDA out of memory on the next start.

5 · API reference (POST /v1/audio/speech)

OpenAI-compatible speech endpoint. Output is 48 kHz stereo 16-bit WAV — use it raw, no post-processing.

5.1 · Reference-less (no voice clone)

curl -X POST http://localhost:8000/v1/audio/speech \
  -H "Content-Type: application/json" \
  -d '{"input": "The lighthouse blinked twice before the storm rolled in."}' \
  --output out.wav

Python:

import requests
r = requests.post("http://localhost:8000/v1/audio/speech",
                  json={"input": "The lighthouse blinked twice before the storm rolled in."})
r.raise_for_status()
open("out.wav","wb").write(r.content)

5.2 · Style / director instruction & language

This is a voice-acting model: put the performance direction in instructions.

curl -X POST http://localhost:8000/v1/audio/speech -H "Content-Type: application/json" -d '{
  "input": "You dare enter my lair?!",
  "instructions": "A furious ancient dragon, low and booming, building to a roar. Includes a growl.",
  "language": "English"
}' --output dragon.wav

5.3 · Voice cloning (reference clip + transcript)

Pass a reference clip in references (or the ref_audio/ref_text shorthand). audio_path may be a local path, an HTTP(S) URL, or a base64 data URI. Supplying the transcript (text) materially improves cloning.

curl -X POST http://localhost:8000/v1/audio/speech -H "Content-Type: application/json" -d '{
  "input": "Get the trust fund to the bank early.",
  "references": [{
    "audio_path": "https://huggingface.co/datasets/zhaochenyang20/seed-tts-eval-mini/resolve/main/en/prompt-wavs/common_voice_en_10119832.wav",
    "text": "We asked over twenty different people, and they all said it was his."
  }]
}' --output clone.wav

Base64 data-URI form (decoded server-side with soundfile):

{"ref_audio": "data:audio/wav;base64,UklGR....", "ref_text": "Transcript of the clip."}

5.4 · Streaming (Server-Sent Events)

Set "stream": true to receive SSE. Each event is data: {"object":"audio.speech.chunk", "audio":{"data":"<base64 WAV>"}}; the final event has audio: null, then data: [DONE].

curl -N -X POST http://localhost:8000/v1/audio/speech -H "Content-Type: application/json" -d '{
  "input": "This sentence is streamed back as it is generated.",
  "stream": true
}'

Streaming granularity for this model. The moss_tts_local pipeline emits a coarse first chunk — for a one-sentence prompt the whole utterance arrives in the first audio event, so time-to-first-audio ≈ full generation time (see 7.1). The generic initial_codec_chunk_frames request field exists in the protocol but is only honored by other TTS pipelines (e.g. higgs), not by moss_tts_local — we tested values 2/4/8 and the first-chunk size and TTFA did not change. If you need true low-latency token-by-token streaming, that knob is not (yet) wired for this model.

5.5 · Duration control

The model conditions on a target codec-frame count. Set it inline with ${token:N} (stripped before synthesis) or with the token_count parameter (aliases duration_tokens, tokens). Larger N → longer audio. Omit it to let the model choose.

{"input": "${token:150}A sentence with an explicit duration target.", "ref_audio": "..."}

5.6 · Generation parameters

paramdefaultnotes
input(required)text; may carry ${token:N} and inline markup ([pause 0.5s], IPA…)
references / ref_audio,ref_textnullvoice-clone reference clip + transcript
instructions (alias instruct)nullfree-text performance direction
languagenulllanguage hint; omit to auto-detect
streamfalseSSE streaming
token_count / duration_tokensnulltarget duration in codec frames (>0)
temperature / top_p / top_k0.8 / 0.8 / 30server defaults; per-channel text_*/audio_* overrides accepted
repetition_penalty1.1audio repetition penalty
seednullnon-negative int; reproducible at any concurrency
max_new_tokens4096hard cap on generated frames

6 · Common traps & fixes

Trap 1 — the SIGABRT on the very first request (cuDNN symbol)

Symptom: the server starts fine, then the first request kills a stage subprocess with a SIGABRT and an error like
undefined symbol: cudnnGetLibConfig (or a cuDNN version mismatch abort).

Cause
Your login shell exports an LD_LIBRARY_PATH (e.g. from a conda "ml-general" env) that points at a different cuDNN. It is inherited by the stage subprocesses and collides with the cuDNN that torch ships, so the dynamic loader binds the wrong library and aborts on first use.
Fix
Launch with env -u LD_LIBRARY_PATH in front of sgl-omni (as in §3). This strips the variable for the server and all its children.
Verify
Send the reference-less curl from §5.1 — you get a valid out.wav instead of a 500/subprocess death.

Trap 2 — looking for the wrong cookbook file

Symptom: you go looking for docs/cookbook/moss_tts_local.md and it doesn't exist.

Cause
The upstream cookbook page is named docs/cookbook/moss_tts.md (it documents the MOSS-TTS family; the local-transformer pipeline shares the same /v1/audio/speech surface).
Fix
Read docs/cookbook/moss_tts.md. There is no _local variant of that file.

Trap 3 — libibverbs.so.1 / mooncake import error (harmless)

Symptom: at startup:
Failed to import mooncake: libibverbs.so.1: cannot open shared object file … MooncakeRelay will not work.

Cause
Mooncake is an optional RDMA relay backend needing InfiniBand libs you don't have.
Fix
Ignore it. The config uses relay_backend: shm (shared memory), which is what you want on a single host. Nothing to install.

Trap 4 — glmasr import warning (harmless)

Symptom: Ignore import error when loading sglang.srt.models.glmasr: cannot import name 'GlmAsrConfig' from 'transformers'.

Cause
SGLang tries to register an unrelated ASR model your transformers version doesn't ship.
Fix
Ignore it. It has nothing to do with TTS and the server continues loading normally.

Trap 5 — venv on /tmp fills up / disappears

Symptom: install fails with no space left on device, or the venv/model vanishes after a reboot.

Cause
On many boxes /tmp is a small, volatile tmpfs (RAM-backed). The venv + torch + model cache are many GB.
Fix
Create the venv and set the HF cache on a real disk (e.g. under /home). Only put throwaway logs/outputs on /tmp.

Trap 6 — /v1/models shows the "wrong" model name

Symptom: /v1/models returns OpenMOSS-Team/MOSS-TTS-Local-Transformer-v1.5, not the LAION id — "did it load the wrong weights?"

Cause
The reported id is the pipeline/architecture name from the config, purely cosmetic.
Fix
Nothing — the weights from --model-path (the LAION 4.55B) are the ones loaded and generating. If you want the name to match, pass --model-name.

Trap 7 — FileNotFoundError: 'ninja' only when you enlarge CUDA graphs (tuning)

Symptom: the stock server starts fine, but the moment you add larger cuda_graph_bs for tuning, startup dies during graph capture with
FileNotFoundError: [Errno 2] No such file or directory: 'ninja' (deep in a flashinfer get_batch_prefill_module → run_ninja stack).

Cause
Capturing a batch size flashinfer hasn't compiled yet triggers a JIT build of an attention kernel, which needs ninja and a real nvcc on PATH. The stock config's batch sizes are already compiled into ~/.cache/flashinfer, so a normal serve never needs them.
Fix
Only relevant if you tune graph sizes. Install ninja (uv pip install ninja) and put a full CUDA toolkit's nvcc on PATH for the launch: PATH="$VENV/bin:/usr/local/cuda/bin:$PATH" CUDA_HOME=/usr/local/cuda sgl-omni serve …. The pip nvidia-cuda-nvcc-cu12 wheel ships only ptxas, not the full nvcc — you need a real toolkit.
Verify
Startup logs show Capturing batches (bs=64 …) down to bs=1 and then Application startup complete.

7 · Speed & tuning (measured)

All measurements: one A100-80GB, bf16, reference-less, a fixed pool of short (1-sentence) and medium (3-sentence) English prompts, mean clip ~9 s. Client is the async benchmark in this repo's serving notes. We define RTF = audio-seconds produced per wall-second — numerically equal to audio-hours generated per GPU-hour. Every run is preceded by a 5-request warm-up so CUDA-graph capture and lazy init don't pollute the numbers.

0.74 sbest TTFA (stream, conc 1) 4.5×single-stream RTF 35×peak offline RTF = audio-hrs / A100-hr 96best offline concurrency ~68 GBVRAM at peak

7.1 · Time-to-first-audio (TTFA) and how to lower it

Streaming, concurrency 1, 20 requests. TTFA = wall time from request send to the first audio SSE event.

configTTFA minTTFA p50TTFA p95first-chunk audio
stock (CUDA graphs on)0.74 s1.34 s5.1 s~5 s (whole short clip)
+ initial_codec_chunk_frames=2/4/80.79 s1.35 s4.3 s~6 s (unchanged)

What actually moves TTFA here. For a one-sentence prompt the pipeline emits the entire utterance as the first chunk, so TTFA ≈ full generation latency (~0.7–1.3 s for a short clip). The knobs:

Best TTFA achieved: ~0.74 s (short prompt, conc 1, stock CUDA-graph config). The lever was CUDA graphs, which are already on by default.

7.2 · Streaming throughput vs concurrency

Streaming, stock server (max_running_requests=16), 48 requests per point.

concurrencyRTF (aud-s/s)req/sTTFA p50p95 latency
13.860.471.34 s3.54 s
49.201.092.40 s9.14 s
812.881.494.05 s9.88 s
1615.031.506.16 s15.11 s
3216.582.2710.92 s15.84 s
6416.012.2112.75 s19.52 s

Streaming saturates around 16–17× realtime at concurrency 16–32. Past that, aggregate RTF flattens and TTFA balloons (requests queue), so for interactive streaming keep concurrency ≤16 per GPU. Streaming caps lower than offline because SSE framing + the coarse chunking add per-request overhead and the stock server's AR batch is capped at 16.

7.3 · Offline batch throughput (dataset generation) — the important one

Non-streaming, sweep the number of in-flight requests (SGLang continuous batching). Three server configs:

concurrencystock RTF+MRR only RTF+MRR+graphs RTF+graphs req/s
14.793.634.330.52
814.3013.7713.941.72
1616.0117.7417.792.01
3219.7413.8220.012.41
4817.8322.072.57
6417.7633.203.74
9616.6135.173.91
12830.253.11

The +MRR+graphs column at concurrency 64/96/128 is the rigorous steady-state run (384 requests per point, so start-up ramp and drain are negligible); the lower concurrencies use 96 requests. Peak is at concurrency 96 — concurrency 128 regresses (queue thrash / longer per-request latency for no extra throughput).

The lesson (this is the whole game):

Peak: ~35× realtime = ~35 audio-hours generated per A100-hour, at concurrency 96 with the +MRR+graphs config, using ~68 GB VRAM and pinning the GPU at ~100% utilization (steady-state, 384-request run; concurrency 128 regresses to ~30×). Compared with a plain serialized transformers loop (~1× realtime, one clip at a time), SGLang-Omni's continuous batching + graphs is a ~35× speedup for bulk generation on the same card.

Applying the tuning: generate a config with the tts_engine stage's factory_args.server_args_overrides set to {max_running_requests:64, cuda_graph_bs:[1,2,4,8,16,24,32,48,64], cuda_graph_max_bs:64, mem_fraction_static:0.55} (and, optionally, the vocoder stage's max_batch_size:16), then launch with ninja + nvcc on PATH (trap 7). The stock YAML's typed runtime.sglang_server_args only exposes mem_fraction_static; the batch/graph knobs must go through factory_args.

8 · Recipes

(a) Lowest-latency interactive TTS

(b) Max-throughput offline dataset generation

9 · Napkin math — how long / how much to make N hours of audio

At the peak offline throughput of ~35 audio-hours per A100-hour:

These assume ~9 s mean clips and reference-less generation. Voice cloning adds a one-time reference encode per unique clip (cached), which is negligible amortized over a batch. Very long clips lower RTF slightly (longer sequences, larger KV); very short clips raise req/s but lower audio-hours/hour.

LAION · MOSS Local-Transformer 1.5 voice-acting (4.55B, 48 kHz). Serving guide for SGLang-Omni on A100. Numbers measured on 1×A100-80GB, torch 2.9.1+cu128 / sglang-omni 0.5.8 / flashinfer 0.6.1. Everything Apache-2.0 (code) / CC-BY-4.0 (docs). See the model card and the experiment overview.