Skip to content

Commit 84eea83

Browse files
authored
Merge pull request #3561 from modelscope/codex/realtime-l20-profile-20260830
perf(realtime): add opt-in decode profiling
2 parents 3afdf7b + 4bccc3a commit 84eea83

3 files changed

Lines changed: 90 additions & 6 deletions

File tree

docs/benchmark/realtime_ws_benchmark.md

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ CUDA_VISIBLE_DEVICES=0 python examples/industrial_data_pretraining/fun_asr_nano/
1919
--port 10095 --language 中文 \
2020
--partial-window-sec 8 --decode-interval 0.8 \
2121
--vad-device cpu --vad-ncpu 1 \
22-
--decode-batch-wait-ms 10 --decode-max-batch-size 16
22+
--decode-batch-wait-ms 10 --decode-max-batch-size 16 \
23+
--log-decode-profile
2324
```
2425

2526
Speaker diarization is disabled by default. Add `--enable-spk` only when the
@@ -81,9 +82,16 @@ stress signal, not as user-facing realtime latency.
8182
| `errors` | Connection, timeout, protocol, or client-side validation errors |
8283

8384
The script can observe only client-side timing and fields returned by the
84-
server. If you are debugging service internals, collect server logs separately
85-
for queue wait, VAD time, ASR decode time, speaker diarization time, GPU memory,
86-
and GPU utilization.
85+
server. For a performance investigation, add `--log-decode-profile` to record
86+
one structured line per engine call with the request and sample counts, audio
87+
duration range, queue-wait p50/max, and total engine latency. The underlying
88+
Fun-ASR-Nano vLLM path also logs audio-encoder and vLLM-generation time. Collect
89+
those server logs together with GPU memory/utilization and the client JSONL.
90+
91+
When comparing releases, align `partial_messages` as well as audio, clients,
92+
and service flags. A server that blocks its WebSocket event loop can appear to
93+
finish sooner simply because it processes fewer provisional decodes; that is
94+
not an engine-throughput improvement and gives users fewer live updates.
8795

8896
## Concurrency Regression Reference
8997

@@ -117,7 +125,7 @@ When publishing a realtime WebSocket benchmark or issue report, include:
117125
|----------|----------------|
118126
| Data | Audio duration, sample rate, language/domain, silence ratio or speaking pattern, and whether the same file was looped |
119127
| Load | `--clients`, `--loops`, `--chunk-ms`, paced or `--no-pace`, client ping interval/timeout, and total benchmark wall time |
120-
| Service | `serve_realtime_ws.py` command, WebSocket ping interval/timeout, `--partial-window-sec`, `--decode-interval`, `--vad-device`, `--vad-ncpu`, `--decode-batch-wait-ms`, `--decode-max-batch-size`, `--enable-spk`, language, and hotwords |
128+
| Service | `serve_realtime_ws.py` command, WebSocket ping interval/timeout, `--partial-window-sec`, `--decode-interval`, `--vad-device`, `--vad-ncpu`, `--decode-batch-wait-ms`, `--decode-max-batch-size`, `--log-decode-profile`, `--enable-spk`, language, and hotwords |
121129
| Hardware | GPU/NPU model, GPU count, memory, driver, CUDA/CANN/runtime versions, CPU model, and available RAM |
122130
| Software | `funasr`, PyTorch, torchaudio, vLLM, Python, OS, and container image if any |
123131
| Output | Summary line, JSONL artifact, server logs, and any failed client IDs |

funasr/bin/realtime_ws.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ class _BatchRequest:
4141
def __init__(self, inputs, kwargs):
4242
self.inputs = inputs
4343
self.kwargs = kwargs
44+
self.enqueued_at = time.monotonic()
4445
self.event = threading.Event()
4546
self.result = None
4647
self.error = None
@@ -64,11 +65,14 @@ def __getattr__(self, name):
6465
class RealtimeBatchingEngine:
6566
"""Serialize the shared engine while batching compatible session requests."""
6667

67-
def __init__(self, engine, batch_wait_ms=10.0, max_batch_size=16):
68+
def __init__(
69+
self, engine, batch_wait_ms=10.0, max_batch_size=16, log_profile=False
70+
):
6871
self.engine = engine
6972
self._engine = getattr(engine, "_engine", engine)
7073
self.batch_wait_s = max(0.0, float(batch_wait_ms)) / 1000.0
7174
self.max_batch_size = max(1, int(max_batch_size))
75+
self.log_profile = bool(log_profile)
7276
self.requests = queue.Queue()
7377
self.pending_request = None
7478
self.worker = threading.Thread(
@@ -186,6 +190,7 @@ def _run(self):
186190

187191
def _generate_group(self, requests):
188192
inputs = [item for request in requests for item in request.inputs]
193+
started_at = time.monotonic()
189194
try:
190195
results = self.engine.generate(inputs, **requests[0].kwargs)
191196
if len(results) != len(inputs):
@@ -211,6 +216,36 @@ def _generate_group(self, requests):
211216
request.event.set()
212217
return
213218

219+
if self.log_profile:
220+
queue_waits_ms = sorted(
221+
(started_at - request.enqueued_at) * 1000 for request in requests
222+
)
223+
midpoint = len(queue_waits_ms) // 2
224+
if len(queue_waits_ms) % 2:
225+
queue_p50_ms = queue_waits_ms[midpoint]
226+
else:
227+
queue_p50_ms = (
228+
queue_waits_ms[midpoint - 1] + queue_waits_ms[midpoint]
229+
) / 2
230+
audio_seconds = [
231+
item.shape[-1] / 16000.0
232+
for item in inputs
233+
if isinstance(item, (np.ndarray, torch.Tensor)) and item.ndim > 0
234+
]
235+
logger.info(
236+
"Realtime decode profile: requests=%d samples=%d "
237+
"audio_sec_total=%.3f audio_sec_min=%.3f audio_sec_max=%.3f "
238+
"queue_ms_p50=%.3f queue_ms_max=%.3f engine_ms=%.3f",
239+
len(requests),
240+
len(inputs),
241+
sum(audio_seconds),
242+
min(audio_seconds, default=0.0),
243+
max(audio_seconds, default=0.0),
244+
queue_p50_ms,
245+
queue_waits_ms[-1],
246+
(time.monotonic() - started_at) * 1000,
247+
)
248+
214249
offset = 0
215250
for request in requests:
216251
end = offset + len(request.inputs)
@@ -1158,6 +1193,7 @@ def load_models(args):
11581193
engine,
11591194
batch_wait_ms=getattr(args, "decode_batch_wait_ms", 10.0),
11601195
max_batch_size=getattr(args, "decode_max_batch_size", 16),
1196+
log_profile=getattr(args, "log_decode_profile", False),
11611197
)
11621198

11631199
_asr_kwargs = {}
@@ -1451,6 +1487,14 @@ def build_arg_parser():
14511487
default=16,
14521488
help="Maximum number of audio segments submitted in one batched decode.",
14531489
)
1490+
parser.add_argument(
1491+
"--log-decode-profile",
1492+
action="store_true",
1493+
help=(
1494+
"Log per-engine-batch request counts, audio durations, queue wait, "
1495+
"and engine latency for performance investigations."
1496+
),
1497+
)
14541498
parser.add_argument(
14551499
"--endpoint-mode",
14561500
choices=["server", "client"],

tests/test_realtime_ws_service.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ def test_cli_defaults_disable_speaker_and_bound_partial_window():
116116
assert args.log_session_stats_interval == 0.0
117117
assert args.decode_batch_wait_ms == 10.0
118118
assert args.decode_max_batch_size == 16
119+
assert args.log_decode_profile is False
119120
assert args.vad_device == "cpu"
120121
assert args.vad_ncpu == 1
121122

@@ -1690,6 +1691,37 @@ def generate(index, values):
16901691
assert engine.calls[0][1] == {"language": "zh"}
16911692

16921693

1694+
def test_realtime_batching_engine_logs_opt_in_decode_profile(caplog):
1695+
module = load_service_module()
1696+
1697+
class RecordingEngine:
1698+
def generate(self, inputs, **kwargs):
1699+
return [{"text": "ok"} for _ in inputs]
1700+
1701+
caplog.set_level("INFO")
1702+
batching_engine = module.RealtimeBatchingEngine(
1703+
RecordingEngine(), batch_wait_ms=0, max_batch_size=8, log_profile=True
1704+
)
1705+
1706+
result = batching_engine.generate(
1707+
[
1708+
np.zeros(16000, dtype=np.float32),
1709+
np.zeros(32000, dtype=np.float32),
1710+
]
1711+
)
1712+
1713+
assert result == [{"text": "ok"}, {"text": "ok"}]
1714+
assert "Realtime decode profile:" in caplog.text
1715+
assert "requests=1" in caplog.text
1716+
assert "samples=2" in caplog.text
1717+
assert "audio_sec_total=3.000" in caplog.text
1718+
assert "audio_sec_min=1.000" in caplog.text
1719+
assert "audio_sec_max=2.000" in caplog.text
1720+
assert "queue_ms_p50=" in caplog.text
1721+
assert "queue_ms_max=" in caplog.text
1722+
assert "engine_ms=" in caplog.text
1723+
1724+
16931725
def test_realtime_batching_engine_keeps_incompatible_options_separate():
16941726
module = load_service_module()
16951727

0 commit comments

Comments
 (0)