SGLang-Omni · Fun-ASR Serving · 2026

Encode Before You AdmitA Pre-LM Audio Encoder Service with Caching

Fun-ASR encodes the full audio context before any language-model decoding. That work used to happen inside the LM stage — serialized per request, repeated for identical audio. This change moves encoding upstream of LM admission, then arms it with batching, single-flight deduplication, and a bounded CPU LRU cache.

3.34×
WARM-CACHE THROUGHPUT VS MAIN
−49.2%
P99 LATENCY · STRICTLY UNIQUE INPUTS
+18.0pp
GPU SM BUSY · 25.3% → 43.3%
1.710%
CORPUS WER · IDENTICAL IN ALL RUNS
01

The Problem: Encoding at the Wrong Side of Admission

Fun-ASR is full-context: the whole utterance is encoded before the LM generates a single token. On current main, that encode runs after a request is admitted to the LM stage — one request at a time, inside the model runner.

CURRENT MAIN

Encode after admission

REQUEST → LM ADMIT → ENCODE → DECODE
requestLM admissionencode 1×1decode
  • Serialized encodes. Concurrent short utterances encode one by one at admission — mean SM busy sits at 25–28% under concurrency 32.
  • No dedup, no cache. Identical audio submitted N times is encoded N times.
  • Encoder competes with the LM stage for scheduling inside the runner.
THIS CHANGE

Encode before admission

REQUEST → PRE-LM ENCODE → LM ADMIT → DECODE
requestpre-LM encode · batch + cacheLM admissiondecode
  • Batched encodes of concurrently arriving requests, on a dedicated CUDA stream.
  • Single-flight + LRU cache — identical audio is encoded at most once.
  • Only complete, validated encodings are admitted; embeddings arrive attached to the request.
The GPU wasn't slow — it was idle. The encoder sat downstream of admission where it couldn't batch, so utilization and tail latency suffered long before throughput did.
02

The Design: FunAsrPreLMEncoderService

A dedicated service completes audio encoding before LM admission. Requests reach the LM stage with LM-ready embeddings already attached — the LM never touches raw audio features.

INGRESS

Requests

Concurrent audio requests. Each request's metadata now carries an audio_fingerprint and num_audio_tokens.

NEW · PRE-LM

Encoder Service

Batches concurrently arriving encodes, dedups identical in-flight audio, serves repeats from cache — on a dedicated CUDA stream.

BATCHSINGLE-FLIGHTLRU CACHECUDA STREAM
FunAsrPreLMEncoderService
GATE

LM Admission

Only complete encodings pass. Cached embeddings are validated for shape, token count, and dtype before reuse; failures never enter the LM stage.

GENERATE

LM Decode

Precomputed embeddings attach directly to the multimodal request; the original CPU feature tensor is released after encoding.

peak GPU mem −1.1 GiB
BATCHBatched encoding with per-item fallback

Concurrently arriving encoder requests encode together. If a batched encode fails, the service degrades to per-item encoding and propagates unrecoverable failures only to the affected requests.

DEDUPSingle-flight execution

Concurrent requests for identical audio share one encoder execution. In the benchmark this merged 421 of 1,088 requests — reuse that exists even with a completely cold cache.

CACHEBounded CPU LRU cache

Configurable entry and byte limits. Stores only complete encoder/adaptor outputs — deliberately no prefix reuse of partial audio windows. StageOutputCache was made thread-safe for concurrent access.

GUARDCache correctness by construction

Namespaces and keys make a stale or mismatched hit structurally impossible; every hit is re-validated before use.

namespace = model + frontend config + dtype + device type + mm-attn backend
key      = normalized audio fingerprint
validate  = embedding shape · token count · dtype before reuse
SHAREOne encoder implementation, two paths

_get_audio_feature_uncached() is extracted so the regular and pre-LM paths run the same underlying encoder code — no behavioral fork. New encoder queue, batching, latency, failure, and cache statistics ship with the service.

03

Benchmark: One H100, Concurrency 32

Full Seed-TTS English set — 1,088 samples, 666 unique audio files — withFunAudioLLM/Fun-ASR-Nano-2512-hf on one NVIDIA H100 80GB. The two passes share one fresh server: cold-start on an empty cache, then warm reusing it.

MetricMainNew · coldNew · warm
Throughput (samples/s)25.96234.73886.789
Wall time (s)41.90731.32012.536
Mean latency (s)1.2290.9140.362
p95 latency (s)1.8021.1140.504
p99 latency (s)3.0151.8960.800
Mean RTF0.265850.199650.07828
Corpus WER0.017100.017100.01710
Skipped requests000

// cold start: +33.8% throughput, −25.6% mean latency — before the cache has anything in it.
// warm: +234.3% (3.343×), wall time 41.9s → 12.5s. WER identical everywhere.

THROUGHPUT — SAMPLES PER SECOND
current main25.96
change · cold cache34.74 · 1.34×
change · warm cache86.79 · 3.34×
LATENCY — SECONDS, LOWER IS BETTER
main · mean / p95 / p991.23 / 1.80 / 3.02
change cold · mean / p95 / p990.91 / 1.11 / 1.90
change warm · mean / p95 / p990.36 / 0.50 / 0.80
RTF 0.266 → 0.078warm cache · same model, same H100
04

Attribution: Where the Speedup Actually Comes From

A follow-up run pins main (f916b86c) against the change (e6f49902), plus astrict no-hit control: 666 SHA-256-verified unique files, telemetry-confirmedhits=0, merged=0. This separates caching from everything else.

GPU SM BUSY — MEAN %, 100ms NVML SAMPLING
cold · main25.3%
cold · change43.3% · +18.0 pp
no-hit · main28.1%
no-hit · change43.5% · +15.4 pp

// batching alone doubles effective utilization — no cache hits required.
// exactly-idle GPU time: ~0.70s → ~0.41s; peak memory −1,099 MiB; mean power +17.6W (the GPU is finally working).

No-hit controlMainChangeΔ
Throughput (samples/s)32.17832.231+0.17%
Mean latency (s)0.9910.974−1.7%
p95 latency (s)1.5431.062−31.2%
p99 latency (s)2.1771.105−49.2%
p95 RTF0.33260.2998−9.9%
Peak GPU mem (MiB)73,062.971,963.5−1,099.4
Corpus WER0.0177150.017715identical

// 666/666 evaluated, 0 skipped, per-sample WER distribution identical.

Honest scope. With every request strictly unique, aggregate throughput is effectively neutral (+0.17%). The unconditional wins are GPU utilization (+15.4 pp SM busy) and tail latency (p99 −49.2%). Large throughput gains additionally require duplicate or repeated audio — which real workloads usually have.
05

Cache Telemetry: Reuse From an Empty Cache

Cold-start pass, 1,088 requests, cache empty at t=0. The 666 misses match the 666 unique files exactly — and on top of that, single-flight merged 421 concurrent duplicates that arrived before the first encoding of their audio had finished.

COLD-START PASS — 1,088 REQUESTS
cache misses (= unique files)666
single-flight merges421
ordinary cache hits1
38.79%of requests avoided an encoder execution — in one pass, from empty
WARM PASS — SAME SERVER, SECOND RUN
cache hits1,088 / 1,088
misses · merges · encoder executions0 · 0 · 0

// once populated, every request reuses a complete cached encoder output.
// caveat: the service's narrow hits/(hits+misses) metric read 0.15% on the cold pass
// because it excludes single-flight merges — 38.79% effective reuse is the number that matters.

06

Correctness: Nothing Moved Except the Speed

Main and the change — cold and warm alike — produced bit-identical quality on the full dataset, and incomplete or failed encodings are structurally barred from the LM stage.

1.710%
corpus WER — identical across main, cold, and warm; per-sample WER distribution identical too
0
skipped requests — all 1,088 completed in every configuration
2,125/2,125
unit tests passed on self-hosted GPU CI — batching, cache, dedup, failure handling
complete-only
cache policy — no prefix reuse of partial audio windows, ever
Failure containment: a failed batched encode degrades to per-item retries; unrecoverable failures propagate only to their own requests. The cache stores complete outputs only, namespaces prevent cross-config reuse, and every hit is shape-, token-, and dtype-validated before admission.
"The GPU wasn't slow — it was waiting in line.
Encode once, upstream, and admit only what's complete."