[Bugfix] Avoid invalid TPOT metrics (#6696)

Signed-off-by: zhuhh97 <zhuhonghan@huawei.com>
Co-authored-by: amy-why-3459 <wuhaiyan17@huawei.com>
This commit is contained in:
Honghan Zhuandamy-why-3459 authored and GitHub committed 2026-08-29 11:56:37 +00:00
1 parent e338f0ece3
commit 72a1ce488b
15 files changed
+399 -65

No files matched your search

+1 -1
View File
@@ -1,5 +1,5 @@
ARG VLLM_ASCEND_IMAGE=quay.io/atlas-ci/vllm-ascend
ARG VLLM_ASCEND_TAG=v0.27.0
ARG VLLM_ASCEND_TAG=v0.28.0
FROM ${VLLM_ASCEND_IMAGE}:${VLLM_ASCEND_TAG}
# WORKDIR /vllm-workspace/vllm
+1 -1
View File
@@ -1,5 +1,5 @@
ARG VLLM_ASCEND_IMAGE=quay.io/atlas-ci/vllm-ascend
ARG VLLM_ASCEND_TAG=v0.27.0-a3
ARG VLLM_ASCEND_TAG=v0.28.0-a3
FROM ${VLLM_ASCEND_IMAGE}:${VLLM_ASCEND_TAG}
# WORKDIR /vllm-workspace/vllm
+1 -1
View File
@@ -1,5 +1,5 @@
ARG VLLM_ASCEND_IMAGE=quay.io/atlas-ci/vllm-ascend
ARG VLLM_ASCEND_TAG=v0.27.0
ARG VLLM_ASCEND_TAG=v0.28.0
FROM ${VLLM_ASCEND_IMAGE}:${VLLM_ASCEND_TAG}
ARG APP_DIR=/vllm-workspace/vllm-omni
+1 -1
View File
@@ -1,5 +1,5 @@
ARG VLLM_ASCEND_IMAGE=quay.io/atlas-ci/vllm-ascend
ARG VLLM_ASCEND_TAG=v0.27.0-a3
ARG VLLM_ASCEND_TAG=v0.28.0-a3
FROM ${VLLM_ASCEND_IMAGE}:${VLLM_ASCEND_TAG}
ARG APP_DIR=/vllm-workspace/vllm-omni
+66 -4
View File
@@ -279,17 +279,79 @@ def test_unmeasured_tpot_stays_missing_after_tokenizer_fallback():
assert metrics.request_goodput == 0.0
def test_measured_zero_itl_is_not_treated_as_missing():
def test_zero_itl_does_not_create_zero_tpot():
output = _make_output(100, output_tokens=3)
output.itl = [0.0, 0.0]
output.duplex_session_metrics = {"mean_ttft_ms": 100.0}
metrics = _calculate_test_metrics([output], {"tpot": 1.0})
assert (metrics.num_tpot_samples, metrics.num_itl_samples) == (1, 2)
assert metrics.mean_tpot_ms == 0.0
assert (metrics.num_tpot_samples, metrics.num_itl_samples) == (0, 2)
assert math.isnan(metrics.mean_tpot_ms)
assert metrics.mean_itl_ms == 0.0
assert metrics.request_goodput == 1.0
assert metrics.request_goodput == 0.0
def test_stage_output_tokens_without_client_timing_omit_tpot(capsys):
output = _make_output(100, output_tokens=5)
output.itl = []
output.text_latency = 0.0
output.ttft = 0.1
metrics, _ = calculate_metrics(
input_requests=[],
outputs=[output],
dur_s=1.0,
tokenizer=None,
selected_percentiles=[50.0, 99.0],
goodput_config_dict={},
task_type=TaskType.GENERATION,
selected_percentile_metrics=["tpot"],
max_concurrency=None,
request_rate=float("inf"),
benchmark_duration=1.0,
)
printed = capsys.readouterr().out
assert metrics.num_tpot_samples == 0
assert math.isnan(metrics.mean_tpot_ms)
assert "Time per Output Token" not in printed
assert "Mean TPOT" not in printed
def test_consistent_client_latency_can_supply_tpot_fallback():
output = _make_output(100, output_tokens=10)
output.itl = []
output.ttft = 0.1
output.text_latency = 1.0
metrics = _calculate_test_metrics([output])
assert metrics.num_tpot_samples == 1
assert metrics.mean_tpot_ms == pytest.approx(100.0)
def test_single_token_responses_do_not_report_zero_tpot(capsys):
output = _make_output(100, output_tokens=1)
metrics, _ = calculate_metrics(
input_requests=[],
outputs=[output],
dur_s=1.0,
tokenizer=None,
selected_percentiles=[50.0],
goodput_config_dict={},
task_type=TaskType.GENERATION,
selected_percentile_metrics=["tpot"],
max_concurrency=None,
request_rate=float("inf"),
benchmark_duration=1.0,
)
printed = capsys.readouterr().out
assert metrics.num_tpot_samples == 0
assert math.isnan(metrics.mean_tpot_ms)
assert "Time per Output Token" not in printed
def test_duplex_goodput_does_not_pair_measurements_from_different_requests():
+73 -2
View File
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""
Unit tests for patch.py
@@ -17,6 +17,7 @@ from vllm.benchmarks.lib.endpoint_request_func import RequestFuncInput
from vllm_omni.benchmarks.patch.patch import (
MixRequestFuncOutput,
_apply_stage0_token_timings,
async_request_openai_chat_omni_completions,
async_request_openai_realtime_duplex,
)
@@ -102,7 +103,24 @@ async def test_seed_tts_realtime_duplex_exports_per_request_metrics(monkeypatch)
received_at_s=now + 0.03,
)
self.events.add(
{"type": "response.done", "response": {"id": response_id}},
{
"type": "response.done",
"response": {
"id": response_id,
"metadata": {
"vllm_omni": {
"stage_metrics": {
"0": {
"num_tokens_out": 3,
"vllm_ttft_ms": 20.0,
"vllm_tpot_ms": 10.0,
"vllm_itls_ms": [10.0, 10.0],
}
}
}
},
},
},
received_at_s=now + 0.04,
)
@@ -143,6 +161,7 @@ async def test_seed_tts_realtime_duplex_exports_per_request_metrics(monkeypatch)
assert client.configure_kwargs["native_duplex"] is False
assert client.configure_kwargs["extra_body"] == {
"ref_audio": "data:audio/wav;base64,AAAA",
"return_stage_metrics": True,
}
assert client.sent == [
event
@@ -167,6 +186,10 @@ async def test_seed_tts_realtime_duplex_exports_per_request_metrics(monkeypatch)
assert output.audio_ttfp > output.ttft
assert output.audio_rtf > 0
assert output.latency > 0
assert output.output_tokens == 12
assert output.itl == [0.01, 0.01] * 4
assert output.text_latency == pytest.approx(output.ttft + 0.08)
assert output.tpot_measured is True
assert output.tts_turn_pcm_bytes == [b"\x00\x00" * 2400] * 4
assert output.tts_output_pcm_bytes == b"\x00\x00" * 9600
session_id = output.duplex_request_metrics[0]["session_id"]
@@ -198,6 +221,54 @@ async def test_seed_tts_realtime_duplex_exports_per_request_metrics(monkeypatch)
}
def test_stage0_token_timings_use_weighted_tpot_when_itls_are_incomplete():
output = MixRequestFuncOutput(ttft=0.1)
measured = _apply_stage0_token_timings(
output,
[
{"output_token_count": 3, "itls_ms": [], "tpot_ms": 10.0},
{"output_token_count": 2, "itls_ms": [], "tpot_ms": 40.0},
],
expected_output_tokens=5,
)
assert measured is True
assert output.itl == []
assert output.tpot_measured is True
assert (output.text_latency - output.ttft) / 4 == pytest.approx(0.02)
def test_stage0_zero_itls_fall_back_to_positive_tpot():
output = MixRequestFuncOutput(ttft=0.1)
measured = _apply_stage0_token_timings(
output,
[{"output_token_count": 3, "itls_ms": [0.0, 0.0], "tpot_ms": 25.0}],
expected_output_tokens=3,
)
assert measured is True
assert output.itl == []
assert output.tpot_measured is True
assert (output.text_latency - output.ttft) / 2 == pytest.approx(0.025)
def test_stage0_token_count_without_timing_is_not_measured():
output = MixRequestFuncOutput(ttft=0.1)
measured = _apply_stage0_token_timings(
output,
[{"output_token_count": 3, "itls_ms": [], "tpot_ms": None}],
expected_output_tokens=3,
)
assert measured is False
assert output.itl == []
assert output.text_latency == output.ttft
assert output.tpot_measured is False
def create_sse_chunk(data_dict):
"""Helper to create SSE formatted chunk"""
return f"data: {json.dumps(data_dict)}\n\n".encode()
+6 -2
View File
@@ -585,7 +585,7 @@ async def test_adapter_requires_and_forwards_exact_prepared_payload(tmp_path: Pa
@pytest.mark.asyncio
@pytest.mark.parametrize(
("request_metrics", "expected_itl", "expected_text_latency"),
("request_metrics", "expected_itl", "expected_text_latency", "expected_tpot_measured"),
[
(
[
@@ -594,6 +594,7 @@ async def test_adapter_requires_and_forwards_exact_prepared_payload(tmp_path: Pa
],
[0.01, 0.02, 0.03],
0.16,
True,
),
(
[
@@ -602,6 +603,7 @@ async def test_adapter_requires_and_forwards_exact_prepared_payload(tmp_path: Pa
],
[],
0.18,
True,
),
(
[
@@ -610,6 +612,7 @@ async def test_adapter_requires_and_forwards_exact_prepared_payload(tmp_path: Pa
],
[],
0.1,
False,
),
],
)
@@ -619,6 +622,7 @@ async def test_adapter_reports_exact_or_weighted_token_timing(
request_metrics: list[dict[str, object]],
expected_itl: list[float],
expected_text_latency: float,
expected_tpot_measured: bool,
):
case, options = _case(tmp_path), _session_options(tmp_path)
request = _request(case, options)
@@ -643,7 +647,7 @@ async def test_adapter_reports_exact_or_weighted_token_timing(
assert output.success
assert output.itl == expected_itl
assert output.text_latency == pytest.approx(expected_text_latency)
assert output.tpot_measured is True
assert output.tpot_measured is expected_tpot_measured
def test_batch_finalization_publishes_only_measured_results(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
+16
View File
@@ -1,4 +1,8 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
import json
import math
import os
import threading
from collections.abc import Callable
@@ -159,6 +163,18 @@ def benchmark_params(request):
def assert_result(result, params, num_prompt) -> None:
assert result["completed"] == num_prompt, "Request failures exist"
baseline = params.get("baseline")
hardware = result.get("Hardware")
hardware_baseline = baseline.get(hardware) if isinstance(baseline, dict) and isinstance(hardware, str) else None
if isinstance(hardware_baseline, dict) and "mean_tpot_ms" in hardware_baseline:
num_tpot_samples = result.get("num_tpot_samples")
mean_tpot_ms = result.get("mean_tpot_ms")
assert isinstance(num_tpot_samples, int) and not isinstance(num_tpot_samples, bool) and num_tpot_samples > 0, (
"TPOT baseline is configured, but no measurable TPOT samples were produced"
)
assert (
isinstance(mean_tpot_ms, int | float) and not isinstance(mean_tpot_ms, bool) and math.isfinite(mean_tpot_ms)
), "TPOT baseline is configured, but mean_tpot_ms is not finite"
expected_audio_turns = params.get("expected_duplex_audio_turns_per_session")
if expected_audio_turns is not None:
session_metrics = result.get("duplex_session_metrics")
@@ -43,7 +43,7 @@
"extra_body": {
"save_duplex_request_metrics": true
},
"percentile-metrics": "ttft,e2el,audio_ttfp,audio_rtf,audio_duration",
"percentile-metrics": "ttft,tpot,itl,e2el,audio_ttfp,audio_rtf,audio_duration",
"baseline": {
"H100": {
"request_throughput": [
@@ -515,3 +515,41 @@ def test_omni_duplex_expected_audio_turns_rejects_incomplete_session():
{"expected_duplex_audio_turns_per_session": 4},
1,
)
def test_omni_tpot_baseline_accepts_measured_finite_sample():
from tests.dfx.perf.scripts.run_benchmark import assert_result
assert_result(
{
"completed": 1,
"Hardware": "H100",
"num_tpot_samples": 1,
"mean_tpot_ms": 10.0,
},
{"baseline": {"H100": {"mean_tpot_ms": 20.0}}},
1,
)
@pytest.mark.parametrize(
("num_tpot_samples", "mean_tpot_ms", "match"),
[
(0, float("nan"), "no measurable TPOT samples"),
(1, float("nan"), "mean_tpot_ms is not finite"),
],
)
def test_omni_tpot_baseline_rejects_missing_or_nonfinite_sample(num_tpot_samples, mean_tpot_ms, match):
from tests.dfx.perf.scripts.run_benchmark import assert_result
with pytest.raises(AssertionError, match=match):
assert_result(
{
"completed": 1,
"Hardware": "H100",
"num_tpot_samples": num_tpot_samples,
"mean_tpot_ms": mean_tpot_ms,
},
{"baseline": {"H100": {"mean_tpot_ms": 20.0}}},
1,
)
@@ -4021,6 +4021,55 @@ async def test_duplex_chat_audio_stream_uses_output_audio_delta_event():
]
@pytest.mark.asyncio
async def test_duplex_chat_stage_metrics_use_latest_streaming_snapshot():
class SnapshotChatService(FakeChatService):
async def create_chat_completion(self, request, raw_request=None):
del raw_request
self.seen_request_ids.append(request.request_id)
snapshots = [
{"num_tokens_out": 1, "vllm_tpot_ms": 0.0, "vllm_itls_ms": []},
{"num_tokens_out": 2, "vllm_tpot_ms": 10.0, "vllm_itls_ms": [10.0]},
{"num_tokens_out": 3, "vllm_tpot_ms": 11.0, "vllm_itls_ms": [10.0, 12.0]},
]
async def generate():
for index, snapshot in enumerate(snapshots):
payload = {
"modality": "audio",
"choices": [{"delta": {"content": f"audio-{index}"}}],
"metrics": {"stage_metrics": {"0": snapshot}},
}
yield f"data: {json.dumps(payload)}\n\n"
return generate()
handler = OmniDuplexSessionHandler(
chat_service=SnapshotChatService(FakeEngineClient()),
config_timeout_s=0.1,
idle_timeout_s=1,
)
session = DuplexSession(
session_id="sid-chat-snapshots",
config=DuplexSessionConfig(instructions="speak"),
)
sent: list[dict[str, Any]] = []
async def send_json(data: dict[str, Any]) -> None:
sent.append(data)
await handler._run_response(session, send_json)
audio_deltas = [event for event in sent if event.get("type") == "response.output_audio.delta"]
done = next(event for event in sent if event.get("type") == "response.done")
assert all("vllm_omni" not in event for event in audio_deltas)
assert done["vllm_omni"]["stage_metrics"]["0"] == {
"num_tokens_out": 3,
"vllm_tpot_ms": 11.0,
"vllm_itls_ms": [10.0, 12.0],
}
@pytest.mark.asyncio
async def test_duplex_chat_rejects_unknown_output_modality():
handler = OmniDuplexSessionHandler(
+22 -14
View File
@@ -773,25 +773,33 @@ def calculate_metrics(
output_len = len(tokenizer(outputs[i].generated_text, add_special_tokens=False).input_ids)
actual_output_lens.append(output_len)
total_input += outputs[i].prompt_len
tpot: float | None = 0.0
tpot: float | None = None
if output_len > 1:
if outputs[i].itl:
# Use mean(ITL) directly so per-request TPOT == mean(ITL).
# The ITL list records one entry per SSE chunk; server may
# bundle multiple tokens per chunk, so len(itl)+1 != output_len.
# Using mean(itl) keeps TPOT and ITL on the same footing.
tpot = sum(outputs[i].itl) / len(outputs[i].itl)
measured_tpot = sum(outputs[i].itl) / len(outputs[i].itl)
if np.isfinite(measured_tpot) and measured_tpot > 0:
tpot = measured_tpot
elif getattr(outputs[i], "tpot_measured", True):
try:
latency_minus_ttft = outputs[i].text_latency - outputs[i].ttft
except Exception:
latency_minus_ttft = outputs[i].latency - outputs[i].ttft
tpot = latency_minus_ttft / (output_len - 1)
else:
tpot = None
text_latency = getattr(outputs[i], "text_latency", None)
ttft = getattr(outputs[i], "ttft", None)
if (
isinstance(text_latency, (int, float))
and isinstance(ttft, (int, float))
and np.isfinite(text_latency)
and np.isfinite(ttft)
and text_latency > ttft > 0
):
tpot = (text_latency - ttft) / (output_len - 1)
if tpot is not None:
tpots.append(tpot)
# Note: if output_len <= 1, we regard tpot as 0 for goodput
# TPOT is not measurable for a single-token response or when the
# client did not observe a positive decode interval. In
# particular, stage-level output token counts alone must not turn
# an unset text_latency into a negative TPOT sample.
all_tpots.append(tpot)
itls += outputs[i].itl
session_metrics = getattr(outputs[i], "duplex_session_metrics", None)
@@ -949,10 +957,10 @@ def calculate_metrics(
std_ttft_ms=np.std(ttfts or missing_duplex_value) * 1000,
median_ttft_ms=np.median(ttfts or missing_duplex_value) * 1000,
percentiles_ttft_ms=[(p, np.percentile(ttfts or missing_duplex_value, p) * 1000) for p in selected_percentiles],
mean_tpot_ms=np.mean(tpots or missing_duplex_value) * 1000,
std_tpot_ms=np.std(tpots or missing_duplex_value) * 1000,
median_tpot_ms=np.median(tpots or missing_duplex_value) * 1000,
percentiles_tpot_ms=[(p, np.percentile(tpots or missing_duplex_value, p) * 1000) for p in selected_percentiles],
mean_tpot_ms=np.mean(tpots or float("nan")) * 1000,
std_tpot_ms=np.std(tpots or float("nan")) * 1000,
median_tpot_ms=np.median(tpots or float("nan")) * 1000,
percentiles_tpot_ms=[(p, np.percentile(tpots or float("nan"), p) * 1000) for p in selected_percentiles],
mean_itl_ms=np.mean(itls or missing_duplex_value) * 1000,
std_itl_ms=np.std(itls or missing_duplex_value) * 1000,
median_itl_ms=np.median(itls or missing_duplex_value) * 1000,
+84 -28
View File
@@ -1492,6 +1492,72 @@ def _nonnegative_number(value: object) -> bool:
return isinstance(value, int | float) and not isinstance(value, bool) and np.isfinite(value) and value >= 0
def _apply_stage0_token_timings(
output: MixRequestFuncOutput,
raw_stages: list[object],
*,
expected_output_tokens: int,
) -> bool:
"""Populate aggregate token timing from complete per-response Stage 0 metrics."""
output.itl = []
output.text_latency = output.ttft
output.tpot_measured = False
stages: list[tuple[int, dict[str, object]]] = []
for stage in raw_stages:
if not isinstance(stage, dict):
return False
token_count = stage.get("output_token_count")
if not isinstance(token_count, int) or isinstance(token_count, bool) or token_count < 0:
return False
stages.append((token_count, stage))
if not stages or sum(count for count, _ in stages) != expected_output_tokens:
return False
timed = [(count - 1, stage) for count, stage in stages if count > 1]
if not timed:
return False
measured_itls: list[float] = []
exact_itls = True
for intervals, stage in timed:
values = stage.get("itls_ms")
if (
not isinstance(values, list)
or len(values) != intervals
or not all(_nonnegative_number(value) for value in values)
):
exact_itls = False
break
measured_itls.extend(float(value) / 1000.0 for value in values if isinstance(value, int | float))
if exact_itls:
if sum(measured_itls) > 0:
output.itl = measured_itls
output.text_latency = output.ttft + sum(output.itl)
output.tpot_measured = True
return True
weighted_tpot_ms = 0.0
for intervals, stage in timed:
tpot_ms = stage.get("tpot_ms")
if not _nonnegative_number(tpot_ms) or not isinstance(tpot_ms, int | float) or tpot_ms <= 0:
break
weighted_tpot_ms += float(tpot_ms) * intervals
else:
decode_intervals = sum(intervals for intervals, _ in timed)
weighted_tpot = weighted_tpot_ms / decode_intervals / 1000.0
# ``calculate_metrics`` consumes a request-shaped latency. Preserve
# the weighted per-response TPOT when projecting it onto the combined
# output token count; gaps and TTFTs between turns are intentionally
# excluded.
output.text_latency = output.ttft + weighted_tpot * (expected_output_tokens - 1)
output.tpot_measured = True
return True
return False
async def _async_request_omniinteract(
request_func_input: RequestFuncInput,
*,
@@ -1538,35 +1604,12 @@ async def _async_request_omniinteract(
output.ttft = float(session_metrics.get("mean_ttft_ms") or 0.0) / 1000.0
output.audio_ttfp = float(session_metrics.get("mean_ttfp_ms") or 0.0) / 1000.0
output.audio_rtf = float(session_metrics.get("mean_rtf") or 0.0)
stages: list[tuple[int, dict[str, object]]] = []
for request_metric in case_result.duplex_request_metrics:
stage0 = request_metric.get("stage0_tokens")
if not isinstance(stage0, dict):
continue
token_count = stage0.get("output_token_count")
if isinstance(token_count, int) and not isinstance(token_count, bool) and token_count >= 0:
stages.append((token_count, stage0))
timed = [(count - 1, stage) for count, stage in stages if count > 1]
complete = (
len(stages) == len(case_result.duplex_request_metrics)
and sum(count for count, _ in stages) == output.output_tokens
token_timing_measured = _apply_stage0_token_timings(
output,
[request_metric.get("stage0_tokens") for request_metric in case_result.duplex_request_metrics],
expected_output_tokens=output.output_tokens,
)
exact = complete and all(
isinstance(values := stage.get("itls_ms"), list)
and len(values) == intervals
and all(_nonnegative_number(value) for value in values)
for intervals, stage in timed
)
output.itl = [float(value) / 1000.0 for _, stage in timed for value in stage["itls_ms"]] if exact else []
if output.itl:
output.text_latency = output.ttft + sum(output.itl)
elif complete and timed and all(_nonnegative_number(stage.get("tpot_ms")) for _, stage in timed):
intervals = sum(count for count, _ in timed)
weighted_tpot = sum(float(stage["tpot_ms"]) * count for count, stage in timed) / intervals / 1000.0
output.text_latency = output.ttft + weighted_tpot * (output.output_tokens - 1)
else:
output.text_latency = output.ttft
output.tpot_measured = False
if not token_timing_measured:
if output.output_tokens > 1 or (output.output_tokens == 0 and output.generated_text):
logger.warning(
"OmniInteract session %s omitted complete engine token timing; standard TPOT/ITL are unavailable",
@@ -1610,6 +1653,9 @@ async def async_request_openai_realtime_duplex(
speech_extra = getattr(request_func_input, "seed_tts_speech_extra", None)
if not isinstance(speech_extra, dict):
speech_extra = {}
else:
speech_extra = dict(speech_extra)
speech_extra["return_stage_metrics"] = True
configured_turns = getattr(request_func_input, "seed_tts_turns", ())
turn_prompts = [
(
@@ -1740,6 +1786,16 @@ async def async_request_openai_realtime_duplex(
for timing in turn_timings
if isinstance((stage0 := timing.get("stage0_tokens")), dict)
)
token_timing_measured = _apply_stage0_token_timings(
output,
[timing.get("stage0_tokens") for timing in turn_timings],
expected_output_tokens=output.output_tokens,
)
if not token_timing_measured and output.output_tokens > 1:
logger.warning(
"Realtime TTS session %s omitted complete engine token timing; standard TPOT/ITL are unavailable",
session_id,
)
output.success = True
except Exception:
output.success = False
@@ -1,3 +1,6 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
from __future__ import annotations
import asyncio
@@ -43,20 +46,22 @@ class ChatFallbackProjectorMixin:
else:
await self._emit_full_response(session, result, epoch, response_id, send_json)
if session.epoch == epoch:
final_stage_metrics = session.accumulate_response_stage_metrics(None)
should_commit = self._should_commit_response_to_history(session, response_id)
committed_message = session.end_response(commit_text=should_commit)
if should_commit:
session.register_history_item(f"item_{response_id}", committed_message)
await send_json(
{
"type": "response.done",
"session_id": session.session_id,
"response_id": response_id,
"epoch": epoch,
"committed": committed_message is not None,
"playback": session.playback.as_dict(),
}
)
done_payload: dict[str, object] = {
"type": "response.done",
"session_id": session.session_id,
"response_id": response_id,
"epoch": epoch,
"committed": committed_message is not None,
"playback": session.playback.as_dict(),
}
if final_stage_metrics:
done_payload["vllm_omni"] = {"stage_metrics": final_stage_metrics}
await send_json(done_payload)
except asyncio.CancelledError:
raise
except Exception as exc:
@@ -177,6 +182,11 @@ class ChatFallbackProjectorMixin:
response_id: str,
send_json,
) -> None:
metrics = payload.get("metrics")
stage_metrics = metrics.get("stage_metrics") if isinstance(metrics, dict) else None
if isinstance(stage_metrics, dict):
session.replace_response_stage_metric_snapshots(stage_metrics)
modality = payload.get("modality")
if modality not in {None, "text", "audio"}:
await send_json(
@@ -1,3 +1,6 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
from __future__ import annotations
import copy
@@ -818,6 +821,23 @@ class DuplexSession:
return copy.deepcopy(self._response.stage_metrics)
def replace_response_stage_metric_snapshots(
self,
stage_metrics: Mapping[object, object] | None,
) -> dict[str, dict[str, object]]:
"""Merge cumulative chat snapshots by replacing each stage's latest value."""
if self.active_response_id is None or not isinstance(stage_metrics, Mapping):
return copy.deepcopy(self._response.stage_metrics)
for raw_stage_id, raw_values in stage_metrics.items():
if not isinstance(raw_values, Mapping):
continue
stage_id = str(raw_stage_id)
self._response.stage_metrics[stage_id] = copy.deepcopy(dict(raw_values))
self._response.stage_metric_tpot_weighted_ms.pop(stage_id, None)
self._response.stage_metric_tpot_weight.pop(stage_id, None)
return copy.deepcopy(self._response.stage_metrics)
def accumulate_overlap_speech(self, duration_ms: int) -> int:
self._input.overlap_speech_ms += max(0, int(duration_ms))
return self._input.overlap_speech_ms