Files
vllm-omni/vllm_omni/metrics/definitions.py
T

448 lines
17 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Single source of truth for vLLM-Omni Prometheus + bench CLI metric naming.
Consumed by:
- vllm_omni.metrics.prometheus (server-side /metrics pipeline families)
- vllm_omni.metrics.modality (audio families)
- vllm_omni.metrics.transfer (cross-stage transfer families)
- vllm_omni.benchmarks.metrics.metrics (bench CLI MultiModalsBenchmarkMetrics)
Naming conventions for the ``vllm_omni:*`` families exposed here:
time-bearing metrics use the ``_s`` suffix (values in seconds), counters use
``_total`` (auto-suffixed by the prometheus client), sizes use ``_bytes``.
"""
import logging
import os
from collections.abc import Mapping, Sequence
from vllm_omni.metrics.utils import resolve_int_by_sequential_keys
# vllm_omni: namespace for omni-specific Prometheus families, distinct from
# the upstream vllm:* families.
METRIC_PREFIX = "vllm_omni:"
logger = logging.getLogger(__name__)
# ============================================================================
# Bench-side stems (also used as RequestFuncOutput attribute names)
# ============================================================================
AUDIO_TTFP = "audio_ttfp"
AUDIO_DURATION = "audio_duration"
AUDIO_RTF = "audio_rtf"
AUDIO_FRAMES = "audio_frames"
AUDIO_UNDERRUN = "audio_underrun"
AUDIO_CONTINUITY_OK = "audio_continuity_ok"
AUDIO_SKIPPED_REQUESTS = "audio_skipped_requests"
# Bench-side aggregate field names. Keep these centralized so new benchmark
# metrics reuse the same vocabulary instead of inventing parallel spellings.
MEAN_AUDIO_TTFP_MS = f"mean_{AUDIO_TTFP}_ms"
MEDIAN_AUDIO_TTFP_MS = f"median_{AUDIO_TTFP}_ms"
STD_AUDIO_TTFP_MS = f"std_{AUDIO_TTFP}_ms"
PERCENTILES_AUDIO_TTFP_MS = f"percentiles_{AUDIO_TTFP}_ms"
TOTAL_AUDIO_DURATION_S = f"total_{AUDIO_DURATION}_s"
TOTAL_AUDIO_FRAMES = f"total_{AUDIO_FRAMES}"
AUDIO_THROUGHPUT = "audio_throughput"
MEAN_AUDIO_RTF = f"mean_{AUDIO_RTF}"
MEDIAN_AUDIO_RTF = f"median_{AUDIO_RTF}"
STD_AUDIO_RTF = f"std_{AUDIO_RTF}"
PERCENTILES_AUDIO_RTF = f"percentiles_{AUDIO_RTF}"
MEAN_AUDIO_DURATION_S = f"mean_{AUDIO_DURATION}_s"
MEDIAN_AUDIO_DURATION_S = f"median_{AUDIO_DURATION}_s"
STD_AUDIO_DURATION_S = f"std_{AUDIO_DURATION}_s"
PERCENTILES_AUDIO_DURATION_S = f"percentiles_{AUDIO_DURATION}_s"
MEAN_AUDIO_UNDERRUN_S = f"mean_{AUDIO_UNDERRUN}_s"
MEDIAN_AUDIO_UNDERRUN_S = f"median_{AUDIO_UNDERRUN}_s"
STD_AUDIO_UNDERRUN_S = f"std_{AUDIO_UNDERRUN}_s"
PERCENTILES_AUDIO_UNDERRUN_S = f"percentiles_{AUDIO_UNDERRUN}_s"
AUDIO_CONTINUITY_OK_RATE = f"{AUDIO_CONTINUITY_OK}_rate"
IMAGE_COUNT = "image_count"
IMAGE_GENERATION = "image_generation"
IMAGE_GENERATION_TIME_MS = f"{IMAGE_GENERATION}_time_ms"
IMAGE_PIXELS = "image_pixels"
DIFFUSION_SCHEDULER_WAITING_KEY = "scheduler_num_waiting_reqs"
TOTAL_IMAGES = "total_images"
IMAGE_THROUGHPUT = "image_throughput"
AVERAGE_PIXELS_PER_IMAGE = "average_pixels_per_image"
DENOISE_STEP_LATENCY = "denoise_step_latency"
DENOISE_STEP_LATENCY_MS = f"{DENOISE_STEP_LATENCY}_ms"
MEAN_DENOISE_STEP_LATENCY_MS = f"mean_{DENOISE_STEP_LATENCY}_ms"
MEAN_IMAGE_GENERATION_MS = f"mean_{IMAGE_GENERATION}_ms"
MEDIAN_IMAGE_GENERATION_MS = f"median_{IMAGE_GENERATION}_ms"
STD_IMAGE_GENERATION_MS = f"std_{IMAGE_GENERATION}_ms"
PERCENTILES_IMAGE_GENERATION_MS = f"percentiles_{IMAGE_GENERATION}_ms"
VIDEO_DURATION = "video_duration"
VIDEO_RTF = "video_rtf"
VIDEO_GENERATION = "video_generation"
VIDEO_GENERATION_TIME_MS = f"{VIDEO_GENERATION}_time_ms"
VIDEO_FRAMES = "video_frames"
TOTAL_VIDEO_DURATION_S = f"total_{VIDEO_DURATION}_s"
TOTAL_VIDEO_FRAMES = f"total_{VIDEO_FRAMES}"
VIDEO_THROUGHPUT = "video_throughput"
MEAN_VIDEO_RTF = f"mean_{VIDEO_RTF}"
MEDIAN_VIDEO_RTF = f"median_{VIDEO_RTF}"
STD_VIDEO_RTF = f"std_{VIDEO_RTF}"
PERCENTILES_VIDEO_RTF = f"percentiles_{VIDEO_RTF}"
MEAN_VIDEO_GENERATION_MS = f"mean_{VIDEO_GENERATION}_ms"
MEDIAN_VIDEO_GENERATION_MS = f"median_{VIDEO_GENERATION}_ms"
STD_VIDEO_GENERATION_MS = f"std_{VIDEO_GENERATION}_ms"
PERCENTILES_VIDEO_GENERATION_MS = f"percentiles_{VIDEO_GENERATION}_ms"
PEAK_MEMORY_MB = "peak_memory_mb"
MEAN_PEAK_MEMORY_MB = f"mean_{PEAK_MEMORY_MB}"
MEDIAN_PEAK_MEMORY_MB = f"median_{PEAK_MEMORY_MB}"
STD_PEAK_MEMORY_MB = f"std_{PEAK_MEMORY_MB}"
PERCENTILES_PEAK_MEMORY_MB = f"percentiles_{PEAK_MEMORY_MB}"
STAGE_DURATIONS_MEAN = "stage_durations_mean"
STAGE_DURATIONS_P50 = "stage_durations_p50"
STAGE_DURATIONS_P99 = "stage_durations_p99"
# Stage snapshot / StageBenchmarkMetrics field names.
TOTAL_OUTPUT = "total_output"
TTFTS = "ttfts"
TPOTS = "tpots"
ITLS = "itls"
VLLM_TTFTS = "vllm_ttfts"
VLLM_TPOTS = "vllm_tpots"
VLLM_ITLS = "vllm_itls"
AUDIO_TTFPS = "audio_ttfps"
AUDIO_DURATIONS = "audio_durations"
MISSING_AUDIO_DURATION_COUNT = "missing_audio_duration_count"
STAGE_GEN_TIME = "stage_gen_time"
STAGE_GEN_TIME_MS = f"{STAGE_GEN_TIME}_ms"
STAGE_GEN_TIMES_MS = f"{STAGE_GEN_TIME}s_ms"
POSTPROCESS_TIME = "postprocess_time"
POSTPROCESS_TIME_MS = f"{POSTPROCESS_TIME}_ms"
POSTPROCESS_TIMES_MS = f"{POSTPROCESS_TIME}s_ms"
OUTPUT_UNIT_COUNT = "output_unit_count"
SERVING_TIME_TO_FIRST_OUTPUT_MS = "serving_time_to_first_output_ms"
IMAGE_TIME_TO_FIRST_OUTPUT_MS = "image_time_to_first_output_ms"
SERVING_TIME_TO_FIRST_OUTPUTS_MS = "serving_time_to_first_outputs_ms"
TIME_PER_OUTPUT_UNIT_MS = "time_per_output_unit_ms"
TIME_PER_OUTPUT_UNITS_MS = "time_per_output_units_ms"
INTER_OUTPUT_LATENCY_MS = "inter_output_latency_ms"
INTER_OUTPUT_LATENCIES_MS = "inter_output_latencies_ms"
VLLM_TTFT_MS = "vllm_ttft_ms"
VLLM_TPOT_MS = "vllm_tpot_ms"
VLLM_ITL_MS = "vllm_itl_ms"
VLLM_ITLS_MS = "vllm_itls_ms"
NUM_TOKENS_IN = "num_tokens_in"
NUM_TOKENS_OUT = "num_tokens_out"
AUDIO_SAMPLE_RATE = "audio_sample_rate"
# ============================================================================
# Pipeline-level metric families (request counts + e2e latency)
# ============================================================================
NUM_REQUESTS_RUNNING = METRIC_PREFIX + "num_requests_running"
NUM_REQUESTS_WAITING = METRIC_PREFIX + "num_requests_waiting"
E2E_REQUEST_LATENCY_S = METRIC_PREFIX + "e2e_request_latency_s"
# Per-finished_reason Counter; finished_reason ∈ {stop, length, abort, ...}.
# Aborts include client disconnect / cancellation paths. Counter auto-suffixes
# ``_total`` at exposition time.
REQUESTS_SUCCESS = METRIC_PREFIX + "requests_success"
# Token counters — aggregated across all pipeline stages per request.
PROMPT_TOKENS = METRIC_PREFIX + "prompt_tokens"
GENERATION_TOKENS = METRIC_PREFIX + "generation_tokens"
# ============================================================================
# Audio family (per-stage + per-replica audio path metrics)
# ============================================================================
AUDIO_TTFP_S = METRIC_PREFIX + AUDIO_TTFP + "_s"
AUDIO_DURATION_S = METRIC_PREFIX + AUDIO_DURATION + "_s"
AUDIO_RTF_METRIC = METRIC_PREFIX + AUDIO_RTF
AUDIO_FRAMES_METRIC = METRIC_PREFIX + AUDIO_FRAMES
AUDIO_UNDERRUN_S = METRIC_PREFIX + AUDIO_UNDERRUN + "_s"
AUDIO_CONTINUITY_OK_METRIC = METRIC_PREFIX + AUDIO_CONTINUITY_OK
AUDIO_SKIPPED_REQUESTS_METRIC = METRIC_PREFIX + AUDIO_SKIPPED_REQUESTS
SPEECH_STREAM_ABORTED_METRIC = METRIC_PREFIX + "speech_stream_aborted"
SPEECH_STREAM_COMPLETED_METRIC = METRIC_PREFIX + "speech_stream_completed"
# Realtime Server VAD serving metrics.
REALTIME_VAD_ACTIVE_SESSIONS = METRIC_PREFIX + "realtime_vad_active_sessions"
REALTIME_VAD_INFERENCE_LATENCY_S = METRIC_PREFIX + "realtime_vad_inference_latency_s"
REALTIME_VAD_ENDPOINT_DELAY_S = METRIC_PREFIX + "realtime_vad_endpoint_delay_s"
REALTIME_VAD_ERRORS = METRIC_PREFIX + "realtime_vad_errors"
# ============================================================================
# Diffusion family (per-stage + per-replica diffusion timing breakdowns)
# ============================================================================
DIFFUSION_EXEC_S = METRIC_PREFIX + "diffusion_exec_s"
DIFFUSION_EXEC_PER_STEP_S = METRIC_PREFIX + "diffusion_exec_per_step_s"
DIFFUSION_PREPROCESS_S = METRIC_PREFIX + "diffusion_preprocess_s"
DIFFUSION_POSTPROCESS_S = METRIC_PREFIX + "diffusion_postprocess_s"
# ============================================================================
# Cross-stage Transfer family (per-physical-hop TX/RX/in-flight timings)
# ============================================================================
TRANSFER_SIZE_BYTES = METRIC_PREFIX + "transfer_size_bytes"
TRANSFER_TX_S = METRIC_PREFIX + "transfer_tx_s"
TRANSFER_RX_S = METRIC_PREFIX + "transfer_rx_s"
TRANSFER_IN_FLIGHT_S = METRIC_PREFIX + "transfer_in_flight_s"
# ============================================================================
# Image / diffusion service-level families
# ============================================================================
STAGE_GEN_TIME_S = METRIC_PREFIX + "stage_gen_time_s"
REQUEST_QUEUE_WAIT_S = METRIC_PREFIX + "request_queue_wait_s"
STAGE_WAITING_REQUESTS = METRIC_PREFIX + "stage_waiting_requests"
NUM_INFERENCE_STEPS = METRIC_PREFIX + "num_inference_steps"
IMAGE_COUNT_METRIC = METRIC_PREFIX + IMAGE_COUNT
IMAGE_PIXELS_METRIC = METRIC_PREFIX + IMAGE_PIXELS
PEAK_MEMORY_MB_METRIC = METRIC_PREFIX + "peak_memory_mb"
REQUESTS_FAILED = METRIC_PREFIX + "requests_failed"
KV_WAIT_S = METRIC_PREFIX + "kv_wait_s"
DIFFUSION_FORWARD_S = METRIC_PREFIX + "diffusion_forward_s"
DIFFUSION_EXEC_S = METRIC_PREFIX + "diffusion_exec_s"
DIFFUSION_EXEC_PER_STEP_S = METRIC_PREFIX + "diffusion_exec_per_step_s"
DIFFUSION_PREPROCESS_S = METRIC_PREFIX + "diffusion_preprocess_s"
DIFFUSION_POSTPROCESS_S = METRIC_PREFIX + "diffusion_postprocess_s"
VAE_DECODE_S = METRIC_PREFIX + "vae_decode_s"
DENOISE_STEP_LATENCY_S = METRIC_PREFIX + DENOISE_STEP_LATENCY + "_s"
DIFFUSION_KV_LOAD_S = METRIC_PREFIX + "diffusion_kv_load_s"
IMAGE_TTFP_S = METRIC_PREFIX + "image_ttfp_s"
STAGE_IN_QUEUE_S = METRIC_PREFIX + "stage_in_queue_s"
# ============================================================================
# Label sets
# ============================================================================
PIPELINE_LABELS = ("model_name",)
SUCCESS_LABELS = ("model_name", "finished_reason")
# Per-stage / per-replica label set used by the audio families and by the
# OmniPrometheusStatLogger wrap which relabels upstream ``engine`` into
# ``stage`` + ``replica``.
STAGE_LABELS = ("model_name", "stage", "replica")
STAGE_GEN_TIME_LABELS = ("model_name", "stage", "stage_type")
DIFFUSION_LABELS = ("model_name", "stage")
FAILED_LABELS = ("model_name", "reason")
KV_WAIT_LABELS = ("model_name", "connector_type")
# Audio continuity Counter carries an extra ``threshold_ms`` label so multiple
# threshold buckets can be tracked simultaneously. The ``_ms`` suffix names a
# numeric threshold *value* in ms, not a time-bearing metric.
AUDIO_CONTINUITY_LABELS = ("model_name", "stage", "replica", "threshold_ms")
# Audio skipped-requests Counter carries a `reason` label so the silent-loss
# path (e.g. code2wav rejecting malformed codec input) can be distinguished
# from other "200 OK + empty audio" cases.
AUDIO_SKIPPED_LABELS = ("model_name", "stage", "replica", "reason")
# Cross-stage transfer label set. Each observation is one physical hop from
# (from_stage, from_replica) to (to_stage, to_replica).
TRANSFER_LABELS = (
"model_name",
"from_stage",
"from_replica",
"to_stage",
"to_replica",
)
# ============================================================================
# Histogram buckets
# ============================================================================
# Seconds bucket for e2e / generation / TTFP-style metrics that range from
# ~10 ms to several minutes.
SECONDS_BUCKETS = (
0.05,
0.1,
0.25,
0.5,
1.0,
2.0,
5.0,
10.0,
20.0,
30.0,
60.0,
120.0,
300.0,
)
# Seconds bucket for fine-grained metrics (cross-stage transfer + audio
# underrun) that need millisecond-level resolution.
SECONDS_FAST_BUCKETS = (
0.001,
0.005,
0.01,
0.025,
0.05,
0.1,
0.25,
0.5,
1.0,
2.5,
5.0,
10.0,
60.0,
)
# RTF SLO red line is 1.0 — TTS must generate faster than playback.
RTF_BUCKETS = (
0.1,
0.25,
0.5,
0.75,
0.9,
1.0,
1.25,
1.5,
2.0,
5.0,
10.0,
)
# Bytes bucket for transfer payload size.
BYTES_BUCKETS = (
1024,
4096,
16384,
65536,
262144,
1048576,
4194304,
16777216,
67108864,
268435456,
)
# ============================================================================
# Audio defaults (shared by server-side observe and bench-side calculation)
# ============================================================================
# Most common across vllm-omni talker variants (cosyvoice3, omnivoice,
# qwen3_tts, mimo_audio). voxcpm2 uses 48000, stable_audio 44100,
# ming_flash 16000 — these models surface a rate at runtime on one of
# ``_SAMPLE_RATE_KEYS``, so DEFAULT_AUDIO_SAMPLE_RATE only applies when no
# usable positive rate is found (and as the bench PCM default when env is unset).
# Streamed OpenAI ``response_format=pcm`` is PCM_16 (s16le); sample width is fixed.
DEFAULT_AUDIO_SAMPLE_RATE = 24000
DEFAULT_AUDIO_CHANNELS = 1
DEFAULT_AUDIO_SAMPLE_WIDTH = 2 # PCM s16le bytes per sample
AUDIO_SAMPLE_RATE_ENV = "VLLM_OMNI_BENCH_AUDIO_SAMPLE_RATE"
AUDIO_CHANNELS_ENV = "VLLM_OMNI_BENCH_AUDIO_CHANNELS"
# Default underrun threshold — kept aligned with the bench-side default and
# the commonly-cited "audible gap" threshold for streaming TTS.
AUDIO_CONTINUITY_DEFAULT_THRESHOLD_S = 0.1
# ============================================================================
# Formula helpers (shared by server-side observe and bench-side calculation)
# ============================================================================
def compute_audio_rtf(audio_generation_latency_s: float, audio_duration_s: float) -> float:
"""RTF = audio_generation_latency / audio_content_duration.
SLO red line < 1 — must generate faster than content plays back to stream.
Returns 0.0 when audio_duration_s is non-positive (caller decides whether
to observe; we don't want to divide by zero or emit negative samples).
"""
if audio_duration_s <= 0:
return 0.0
return audio_generation_latency_s / audio_duration_s
def compute_audio_frames(
pcm_nbytes: int,
*,
sample_width: int = DEFAULT_AUDIO_SAMPLE_WIDTH,
channels: int = DEFAULT_AUDIO_CHANNELS,
) -> int:
"""Convert a PCM byte length to sample/frame count.
``pcm_nbytes`` is the raw byte size of interleaved PCM (e.g.
``total_pcm_bytes`` or ``len(wav_pcm_buffer)``). Defaults match the
streamed s16le mono contract
(``DEFAULT_AUDIO_SAMPLE_WIDTH``, ``DEFAULT_AUDIO_CHANNELS``);
WAV callers should pass header values.
Returns 0 when the byte count or frame width (``sample_width * channels``)
is non-positive.
"""
frame_width = sample_width * channels
if pcm_nbytes <= 0 or frame_width <= 0:
return 0
return pcm_nbytes // frame_width
# ============================================================================
# Audio sample-rate resolution
# ============================================================================
_SAMPLE_RATE_KEYS = ("output_sample_rate", "audio_sample_rate", "sample_rate", "sampling_rate", "sr")
def resolve_audio_sample_rate_or_none(
source: Sequence[Mapping[str, object] | object | None] | Mapping[str, object] | object | None,
) -> int | None:
"""Extract an explicitly configured audio sample rate, or ``None`` when absent.
``source`` may be:
- a Mapping (e.g. ``multimodal_output`` / config dict)
- an object exposing the same fields as attributes
- a Sequence of the above (``str`` / ``bytes`` excluded); the first
positive rate found across items wins
"""
if isinstance(source, Sequence) and not isinstance(source, (str, bytes, bytearray)):
for item in source:
rate = resolve_int_by_sequential_keys(item, _SAMPLE_RATE_KEYS)
if rate is not None:
return rate
return None
return resolve_int_by_sequential_keys(source, _SAMPLE_RATE_KEYS)
def resolve_audio_sample_rate(
source: Sequence[Mapping[str, object] | object | None] | Mapping[str, object] | object | None,
default: int = DEFAULT_AUDIO_SAMPLE_RATE,
) -> int:
"""Extract audio sample rate from a dict, config object, or list thereof.
Delegates to :func:`resolve_audio_sample_rate_or_none` (same key chain and
coerce rules). Returns ``default`` (``DEFAULT_AUDIO_SAMPLE_RATE`` unless
overridden) when no usable positive rate is present.
Used so /metrics ``audio_duration_s = audio_frames / sample_rate`` stays
consistent with rates surfaced on multimodal outputs and stage configs.
"""
rate = resolve_audio_sample_rate_or_none(source)
return rate if rate is not None else default
def stream_pcm_format_from_env(
*,
default_sample_rate: int = DEFAULT_AUDIO_SAMPLE_RATE,
default_channels: int = DEFAULT_AUDIO_CHANNELS,
) -> tuple[int, int]:
"""Return the sample rate and channel count for streamed raw PCM."""
sample_rate = default_sample_rate
channels = default_channels
raw_sr = os.environ.get(AUDIO_SAMPLE_RATE_ENV)
if raw_sr:
try:
sample_rate = int(raw_sr)
except ValueError:
logger.warning("Invalid %s=%r; using default %d", AUDIO_SAMPLE_RATE_ENV, raw_sr, sample_rate)
raw_ch = os.environ.get(AUDIO_CHANNELS_ENV)
if raw_ch:
try:
channels = int(raw_ch)
except ValueError:
logger.warning("Invalid %s=%r; using default %d", AUDIO_CHANNELS_ENV, raw_ch, channels)
return max(sample_rate, 1), max(channels, 1)