Merge branch 'main' into fix/omni-weight-snapshot-prefetch
This commit is contained in:
42 files changed
+2394
-243
No files matched your search
+134
-26
@@ -2,29 +2,29 @@
|
||||
The vllm bench command launches the vLLM-Omni benchmark to evaluate the performance of multimodal models.
|
||||
|
||||
## Notes
|
||||
We currently only support using the "openai-chat-omni" backend.
|
||||
We currently only support using the "openai-chat-omni" and "openai-image-edits-omni" backend.
|
||||
|
||||
## Basic Parameter Description
|
||||
You can use `vllm bench serve --omni --help=all` to get descriptions of all parameters. The commonly used parameters are described below:
|
||||
- `--omni`
|
||||
- `--omni`
|
||||
Enable Omni (multimodal) mode, supporting multimodal inputs and outputs such as images, videos, and audio.
|
||||
|
||||
- `--backend`
|
||||
- `--backend`
|
||||
Specify the backend adapter as openai-chat-omni, using OpenAI Chat compatible API behavior as the protocol. Currently only openai-chat-omni is supported.
|
||||
|
||||
- `--model`
|
||||
- `--model`
|
||||
The model identifier to load, filled according to the models supported by vLLM-Omni.
|
||||
|
||||
- `--endpoint`
|
||||
- `--endpoint`
|
||||
The API endpoint exposed externally, to which clients send their requests.
|
||||
|
||||
- `--dataset-name`
|
||||
- `--dataset-name`
|
||||
The name of the dataset used; random-mm indicates generating random multimodal inputs (images, videos, audio).
|
||||
|
||||
- `--num-prompts`
|
||||
- `--num-prompts`
|
||||
The total number of requests to send, an integer.
|
||||
|
||||
- `--max-concurrency`
|
||||
- `--max-concurrency`
|
||||
"Maximum number of concurrent requests. This can be used "
|
||||
"to help simulate an environment where a higher level component "
|
||||
"is enforcing a maximum number of concurrent requests. While the "
|
||||
@@ -34,69 +34,73 @@ You can use `vllm bench serve --omni --help=all` to get descriptions of all para
|
||||
"actual request rate may be lower than specified with --request-rate, "
|
||||
"if the server is not processing requests fast enough to keep up."
|
||||
|
||||
- `--request-rate`
|
||||
- `--request-rate`
|
||||
"Number of requests per second. If this is inf, "
|
||||
"then all the requests are sent at time 0. "
|
||||
"Otherwise, we use Poisson process or gamma distribution "
|
||||
"to synthesize the request arrival times."
|
||||
|
||||
- `--ignore-eos`
|
||||
- `--ignore-eos`
|
||||
"Set ignore_eos flag when sending the benchmark request."
|
||||
|
||||
- `--metric-percentiles`
|
||||
- `--metric-percentiles`
|
||||
Comma-separated list of percentiles for selected metrics. "
|
||||
"To report 25-th, 50-th, and 75-th percentiles, use \"25,50,75\". "
|
||||
"Default value is \"99\"."
|
||||
"Use \"--percentile-metrics\" to select metrics.
|
||||
|
||||
- `--percentile-metrics`
|
||||
- `--percentile-metrics`
|
||||
"Comma-separated list of selected metrics to report percentiles."
|
||||
"This argument specifies the metrics to report percentiles."
|
||||
'Allowed metric names are "ttft", "tpot", "itl", "e2el", "audio_ttfp", "audio_rtf", "audio_duration". '
|
||||
'Allowed metric names are "ttft", "tpot", "itl", "ttfc", "tpoc", "icl", '
|
||||
'"tpop", "e2el", "audio_ttfp", "audio_rtf", "audio_duration". '
|
||||
|
||||
- `--save-result`
|
||||
- `-print-stage`
|
||||
Print per-stage benchmark metrics for --omni serving when stage metrics are returned by the server. Disabled by default.
|
||||
|
||||
- `--save-result`
|
||||
Specify to save benchmark results to a json file
|
||||
|
||||
- `--save-detailed`
|
||||
- `--save-detailed`
|
||||
"When saving the results, whether to include per request "
|
||||
"information such as response, error, ttfs, tpots, etc."
|
||||
|
||||
- `--result-dir`
|
||||
- `--result-dir`
|
||||
"Specify directory to save benchmark json results."
|
||||
"If not specified, results are saved in the current directory."
|
||||
|
||||
- `--result-filename`
|
||||
- `--result-filename`
|
||||
"Specify the filename to save benchmark json results."
|
||||
"If not specified, results will be saved in "
|
||||
"{label}-{args.request_rate}qps-{base_model_id}-{current_dt}.json"
|
||||
|
||||
- `--random-prefix-len`
|
||||
- `--random-prefix-len`
|
||||
Number of fixed prefix tokens before the random context in a request.
|
||||
The total input length is the sum of random-prefix-len and a random
|
||||
context length sampled from [input_len * (1 - range_ratio),
|
||||
input_len * (1 + range_ratio)].Only the random and random-mm modes
|
||||
support this parameter.
|
||||
|
||||
- `--random-input-len`
|
||||
- `--random-input-len`
|
||||
Number of input tokens per request.Only the random and random-mm modes support this parameter.
|
||||
|
||||
- `--random-output-len`
|
||||
- `--random-output-len`
|
||||
Number of output tokens per request.Only the random and random-mm modes support this parameter.
|
||||
|
||||
- `--random-range-ratio`
|
||||
- `--random-range-ratio`
|
||||
Range ratio for sampling input/output length,
|
||||
used only for random sampling. Must be in the range [0, 1) to define
|
||||
a symmetric sampling range
|
||||
[length * (1 - range_ratio), length * (1 + range_ratio)].
|
||||
Only the random and random-mm modes support this parameter.
|
||||
|
||||
- `--random-mm-base-items-per-request`
|
||||
- `--random-mm-base-items-per-request`
|
||||
Base number of multimodal items per request for random-mm.
|
||||
Actual per-request count is sampled around this base using
|
||||
--random-mm-num-mm-items-range-ratio.
|
||||
Only the random-mm mode supports this parameter.
|
||||
|
||||
- `--random-mm-limit-mm-per-prompt`
|
||||
- `--random-mm-limit-mm-per-prompt`
|
||||
Per-modality hard caps for items attached per request, e.g.
|
||||
'{"image": 3, "video": 1, "audio": 1}'. The sampled per-request item
|
||||
count is clamped to the sum of these limits. When a modality
|
||||
@@ -104,7 +108,7 @@ Specify to save benchmark results to a json file
|
||||
renormalized.
|
||||
Only the random-mm mode supports this parameter.
|
||||
|
||||
- `--random-mm-num-mm-items-range-ratio`
|
||||
- `--random-mm-num-mm-items-range-ratio`
|
||||
Range ratio r in [0, 1] for sampling items per request.
|
||||
We sample uniformly from the closed integer range
|
||||
[floor(n*(1-r)), ceil(n*(1+r))]
|
||||
@@ -115,7 +119,7 @@ Specify to save benchmark results to a json file
|
||||
An error is raised if the computed min exceeds the max.
|
||||
Only the random-mm mode supports this parameter.
|
||||
|
||||
- `--random-mm-bucket-config`
|
||||
- `--random-mm-bucket-config`
|
||||
The bucket config is a dictionary mapping a multimodal item
|
||||
sampling configuration to a probability.
|
||||
Currently allows for 3 modalities: audio, images and videos.
|
||||
@@ -276,7 +280,9 @@ Generate synthetic image、video、audio inputs alongside random text prompts to
|
||||
|
||||
Notes:
|
||||
|
||||
- Works only with online benchmark via the OpenAI backend (`--backend openai-chat-omni`) and endpoint `/v1/chat/completions`.
|
||||
- Works only with online benchmark via:
|
||||
- the OpenAI chat backend (`--backend openai-chat-omni`) and endpoint `/v1/chat/completions`.
|
||||
- the OpenAI edit image backend (`--backend openai-image-edits-omni`) and endpoint `/v1/images/edits`.
|
||||
|
||||
Start the server (example):
|
||||
|
||||
@@ -357,3 +363,105 @@ How sampling works:
|
||||
This should be seen as an edge case, and if this behavior can be avoided by setting `--random-mm-limit-mm-per-prompt` to a large number. Note that this might result in errors due to engine config `--limit-mm-per-prompt`.
|
||||
- The resulting request contains synthetic image data in `multi_modal_data` (OpenAI Chat format). When `random-mm` is used with the OpenAI Chat backend, prompts remain text and MM content is attached via `multi_modal_data`.
|
||||
</details>
|
||||
|
||||
### Multi-Stage Benchmark
|
||||
|
||||
<details class="admonition abstract" markdown="1">
|
||||
<summary>Show more</summary>
|
||||
|
||||
Start the server (change the port and model):
|
||||
|
||||
```bash
|
||||
vllm serve --omni --port 29999 --model ~/Qwen3-Omni-30B-A3B-Instruct
|
||||
```
|
||||
|
||||
Then run the benchmarking script (remember to add the flag `--print-stage`):
|
||||
```bash
|
||||
vllm bench serve --omni \
|
||||
--dataset-name random \
|
||||
--port 29999 \
|
||||
--max-concurrency 1 \
|
||||
--num-warmups 2 \
|
||||
--model ~/Qwen3-Omni-30B-A3B-Instruct \
|
||||
--endpoint /v1/chat/completions \
|
||||
--backend openai-chat-omni \
|
||||
--num-prompts 3 \
|
||||
--random-input-len 2500 \
|
||||
--ignore-eos \
|
||||
--percentile-metrics ttft,tpot,itl,e2el,audio_ttfp,audio_rtf,ttfc,tpoc,icl \
|
||||
--random-output-len 900 \
|
||||
--extra_body '{"modalities": ["text","audio"]}' \
|
||||
--print-stage
|
||||
```
|
||||
|
||||
Besides the "Serving Benchmark Result", there will be "Stage Benchmark Result" below:
|
||||
```text
|
||||
============= Stage Benchmark Result =============
|
||||
=============== Stage 0 (thinker) ================
|
||||
-------------------Stage Timing-------------------
|
||||
Mean stage_gen_time (ms): 18243.16
|
||||
Median stage_gen_time (ms): 18220.83
|
||||
P99 stage_gen_time (ms): 18296.75
|
||||
================== Text Result ===================
|
||||
Stage generated tokens: 2700
|
||||
-----------Serving Time to First Token------------
|
||||
Mean Serving TTFT (ms): 816.31
|
||||
Median Serving TTFT (ms): 818.94
|
||||
P99 Serving TTFT (ms): 829.76
|
||||
-----Time per Output Token (excl. 1st token)------
|
||||
Mean TPOP (ms): 19.41
|
||||
Median TPOP (ms): 19.40
|
||||
P99 TPOP (ms): 19.47
|
||||
---------------Inter-token Latency----------------
|
||||
Mean ITL (ms): 19.41
|
||||
Median ITL (ms): 19.12
|
||||
P99 ITL (ms): 23.58
|
||||
================ Stage 1 (talker) ================
|
||||
-------------------Stage Timing-------------------
|
||||
Mean stage_gen_time (ms): 18849.64
|
||||
Median stage_gen_time (ms): 13641.15
|
||||
P99 stage_gen_time (ms): 33655.58
|
||||
============= Internal Stream Result =============
|
||||
-----------Serving Time to First Chunk------------
|
||||
Mean Serving TTFC (ms): 1029.14
|
||||
Median Serving TTFC (ms): 1027.98
|
||||
P99 Serving TTFC (ms): 1032.59
|
||||
-----Time per Output Chunk (excl. 1st chunk)------
|
||||
Mean TPOP (ms): 17.53
|
||||
Median TPOP (ms): 18.70
|
||||
P99 TPOP (ms): 18.88
|
||||
---------------Inter-chunk Latency----------------
|
||||
Mean ICL (ms): 16.27
|
||||
Median ICL (ms): 18.19
|
||||
P99 ICL (ms): 25.86
|
||||
=============== Stage 2 (code2wav) ===============
|
||||
-------------------Stage Timing-------------------
|
||||
Mean stage_gen_time (ms): 18860.74
|
||||
Median stage_gen_time (ms): 13655.84
|
||||
P99 stage_gen_time (ms): 33659.79
|
||||
================== Audio Result ==================
|
||||
Stage audio duration generated(s): 260.11
|
||||
Stage audio frames generated: 6242730
|
||||
-----------Serving Time to First Packet-----------
|
||||
Mean Serving AUDIO_TTFP (ms): 1366.16
|
||||
Median Serving AUDIO_TTFP (ms): 1367.20
|
||||
P99 Serving AUDIO_TTFP (ms): 1368.26
|
||||
-----------------Real Time Factor-----------------
|
||||
Mean AUDIO_RTF: 0.24
|
||||
Median AUDIO_RTF: 0.26
|
||||
P99 AUDIO_RTF: 0.27
|
||||
```
|
||||
Explanation:
|
||||
- stage_gen_time: Time from submitting a request to a specific stage to that stage finishing generation.
|
||||
|
||||
- Serving TTFC (Time to First Chunk): Time from the HTTP request being accepted by the serving frontend to
|
||||
the stage producing its first non-empty output chunk. Keep the name TTFT in text stage, TTFP in audio stage
|
||||
for easier mapping.
|
||||
|
||||
- TPOP (Time per Output Chunk): Average time from the first output chunk to stage completion, divided by the
|
||||
number of remaining output chunks. The TPOP abbreviation follows Qwen3.5-Omni Technical Report.
|
||||
|
||||
- ICL (Inter-Chunk Latency): Time between two consecutive output chunks produced by the same stage. Keep the
|
||||
name ITL in text stage for easier mapping.
|
||||
|
||||
</details>
|
||||
+2
-1
@@ -64,7 +64,8 @@ dev = [
|
||||
"lpips>=0.1.4", # for quantization fp8
|
||||
"orjson>=3.11.7", # to make v1/embeddings API fast
|
||||
"voxcpm>=2.0.2", # for VoxCPM2 path-locating in tests
|
||||
"torchdiffeq>=0.2.5" # for covo_audio Test
|
||||
"torchdiffeq>=0.2.5", # for covo_audio Test
|
||||
"FlagEmbedding" # for hunyuan_image3 tests
|
||||
]
|
||||
|
||||
demo = [
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
@@ -1,7 +1,7 @@
|
||||
[
|
||||
{
|
||||
"test_name": "test_hunyuan_image3_ti2i_tp4_tp4_inline",
|
||||
"description": "AR (TP=4) + DiT (TP=4) - using inline dataset and deploy-config",
|
||||
"test_name": "test_hunyuan_image3_ti2i_inline",
|
||||
"description": "AR (TP=2) + 2 * DiT (TP=2) - using inline dataset and deploy-config",
|
||||
"server_type": "vllm-omni",
|
||||
"benchmark_endpoint": "/v1/images/edits",
|
||||
"server_params": {
|
||||
@@ -21,7 +21,7 @@
|
||||
"zmq_port": 50051,
|
||||
"protocol": "rdma",
|
||||
"device_name": "",
|
||||
"memory_pool_size": 4294967296,
|
||||
"memory_pool_size": 17179869184,
|
||||
"memory_pool_device": "cpu"
|
||||
}
|
||||
}
|
||||
@@ -32,13 +32,13 @@
|
||||
"is_comprehension": true,
|
||||
"final_output": true,
|
||||
"final_output_type": "text",
|
||||
"max_num_seqs": 4,
|
||||
"max_num_seqs": 16,
|
||||
"gpu_memory_utilization": 0.95,
|
||||
"enforce_eager": false,
|
||||
"enable_prefix_caching": false,
|
||||
"max_num_batched_tokens": 32768,
|
||||
"devices": "0,1,2,3",
|
||||
"tensor_parallel_size": 4,
|
||||
"devices": "0,1",
|
||||
"tensor_parallel_size": 2,
|
||||
"hf_overrides": {
|
||||
"rope_parameters": {
|
||||
"mrope_section": [0, 32, 32],
|
||||
@@ -65,13 +65,14 @@
|
||||
"stage_id": 1,
|
||||
"max_num_seqs": 1,
|
||||
"enforce_eager": true,
|
||||
"devices": "4,5,6,7",
|
||||
"devices": "2,3",
|
||||
"distributed_executor_backend": "mp",
|
||||
"omni_kv_config": {
|
||||
"need_recv_cache": true
|
||||
},
|
||||
"parallel_config": {
|
||||
"tensor_parallel_size": 4,
|
||||
"tensor_parallel_size": 2,
|
||||
"vae_patch_parallel_size": 2,
|
||||
"enable_expert_parallel": true
|
||||
},
|
||||
"input_connectors": {
|
||||
@@ -114,8 +115,8 @@
|
||||
},
|
||||
"task": "ti2i",
|
||||
"bot-task": "think_recaption",
|
||||
"max-concurrency": 4,
|
||||
"num-prompts": 16,
|
||||
"max-concurrency": 16,
|
||||
"num-prompts": 96,
|
||||
"skip-performance-assertion": true,
|
||||
"baseline": {
|
||||
"throughput_qps": 0.1,
|
||||
|
||||
@@ -31,11 +31,6 @@
|
||||
],
|
||||
"random_input_len": 2500,
|
||||
"random_output_len": 900,
|
||||
"temperature": 0.4,
|
||||
"top_p": 0.9,
|
||||
"top_k": 1,
|
||||
"seed": 42,
|
||||
"repetition_penalty": 1.05,
|
||||
"ignore_eos": true,
|
||||
"extra_body": {
|
||||
"modalities": [
|
||||
@@ -74,11 +69,6 @@
|
||||
"random_input_len": 100,
|
||||
"random_output_len": 100,
|
||||
"random_range_ratio": 0.0,
|
||||
"temperature": 0.4,
|
||||
"top_p": 0.9,
|
||||
"top_k": 1,
|
||||
"seed": 42,
|
||||
"repetition_penalty": 1.05,
|
||||
"ignore_eos": true,
|
||||
"extra_body": {
|
||||
"modalities": [
|
||||
@@ -117,11 +107,6 @@
|
||||
"random_input_len": 100,
|
||||
"random_output_len": 100,
|
||||
"random_range_ratio": 0.0,
|
||||
"temperature": 0.4,
|
||||
"top_p": 0.9,
|
||||
"top_k": 1,
|
||||
"seed": 42,
|
||||
"repetition_penalty": 1.05,
|
||||
"ignore_eos": true,
|
||||
"extra_body": {
|
||||
"modalities": [
|
||||
@@ -162,11 +147,6 @@
|
||||
"random_input_len": 100,
|
||||
"random_output_len": 100,
|
||||
"random_range_ratio": 0.0,
|
||||
"temperature": 0.4,
|
||||
"top_p": 0.9,
|
||||
"top_k": 1,
|
||||
"seed": 42,
|
||||
"repetition_penalty": 1.05,
|
||||
"ignore_eos": true,
|
||||
"extra_body": {
|
||||
"modalities": [
|
||||
|
||||
@@ -32,8 +32,8 @@ BAGEL_MOONCAKE_CI_DEPLOY = get_deploy_config_path("ci/bagel_mooncake.yaml")
|
||||
|
||||
# Reference pixel data extracted from the known-good output image
|
||||
# Each entry contains (x, y) position and expected (R, G, B) values
|
||||
# "Generated with seed=52, num_inference_steps=15,
|
||||
# prompt='A futuristic city skyline at twilight, cyberpunk style'"
|
||||
# "Generated with seed=52, num_inference_steps=14,
|
||||
# prompt='A cute cat'"
|
||||
REFERENCE_PIXELS = [
|
||||
{"position": (100, 100), "rgb": (115, 113, 94)},
|
||||
{"position": (400, 50), "rgb": (159, 160, 144)},
|
||||
@@ -63,7 +63,7 @@ def _find_free_port() -> int:
|
||||
return port
|
||||
|
||||
|
||||
def _configure_sampling_params(omni: Omni, num_inference_steps: int = 15) -> list:
|
||||
def _configure_sampling_params(omni: Omni, num_inference_steps: int = 14) -> list:
|
||||
"""Configure sampling parameters for Bagel text2img generation.
|
||||
|
||||
Args:
|
||||
@@ -269,7 +269,6 @@ def _load_mooncake_config(host: str, rpc_port: int, http_port: int) -> str:
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.diffusion
|
||||
@pytest.mark.skip(reason="Skip failed CI issue 3977: https://github.com/vllm-project/vllm-omni/issues/3977")
|
||||
@hardware_test(res={"cuda": "H100"}, num_cards=1)
|
||||
def test_bagel_text2img_mooncake_connector(run_level):
|
||||
"""Test Bagel text2img with Mooncake connector for inter-stage communication."""
|
||||
|
||||
@@ -24,14 +24,13 @@ from tests.helpers.mark import hardware_test
|
||||
from tests.helpers.runtime import OmniRunner
|
||||
from tests.helpers.stage_config import get_deploy_config_path, modify_stage_config
|
||||
from vllm_omni.entrypoints.omni import Omni
|
||||
from vllm_omni.platforms import current_omni_platform
|
||||
|
||||
pytestmark = [pytest.mark.usefixtures("clean_gpu_memory_between_tests")]
|
||||
|
||||
BAGEL_CI_DEPLOY = get_deploy_config_path("ci/bagel.yaml")
|
||||
|
||||
# Reference pixel data extracted from the known-good output image
|
||||
# Generated with seed=52, num_inference_steps=15,
|
||||
# Generated with seed=52, num_inference_steps=14,
|
||||
# prompt='Change the grass color to red',
|
||||
# input image: 2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg
|
||||
REFERENCE_PIXELS = [
|
||||
@@ -47,23 +46,10 @@ REFERENCE_PIXELS = [
|
||||
{"position": (256, 256), "rgb": (181, 201, 221)},
|
||||
]
|
||||
|
||||
if current_omni_platform.is_rocm():
|
||||
REFERENCE_PIXELS = [
|
||||
{"position": (100, 100), "rgb": (156, 172, 217)},
|
||||
{"position": (400, 50), "rgb": (105, 144, 217)},
|
||||
{"position": (700, 100), "rgb": (118, 159, 232)},
|
||||
{"position": (150, 400), "rgb": (180, 22, 52)},
|
||||
{"position": (512, 336), "rgb": (221, 211, 194)},
|
||||
{"position": (700, 400), "rgb": (192, 10, 46)},
|
||||
{"position": (100, 600), "rgb": (102, 12, 22)},
|
||||
{"position": (400, 600), "rgb": (161, 28, 47)},
|
||||
{"position": (700, 600), "rgb": (100, 87, 94)},
|
||||
{"position": (256, 256), "rgb": (181, 201, 221)},
|
||||
]
|
||||
|
||||
# text2img reference pixels (aligned with offline `bagel/end2end.py` text2img, 15 steps)
|
||||
# "Generated with seed=52, num_inference_steps=15,
|
||||
# prompt='A futuristic city skyline at twilight, cyberpunk style'"
|
||||
# "Generated with seed=52, num_inference_steps=14,
|
||||
# prompt='A cute cat'"
|
||||
TEXT2IMG_REFERENCE_PIXELS = [
|
||||
{"position": (100, 100), "rgb": (115, 113, 94)},
|
||||
{"position": (400, 50), "rgb": (159, 160, 144)},
|
||||
@@ -77,19 +63,6 @@ TEXT2IMG_REFERENCE_PIXELS = [
|
||||
{"position": (256, 256), "rgb": (167, 156, 150)},
|
||||
]
|
||||
|
||||
if current_omni_platform.is_rocm():
|
||||
TEXT2IMG_REFERENCE_PIXELS = [
|
||||
{"position": (100, 100), "rgb": (115, 113, 94)},
|
||||
{"position": (400, 50), "rgb": (159, 160, 144)},
|
||||
{"position": (700, 100), "rgb": (164, 151, 123)},
|
||||
{"position": (150, 400), "rgb": (120, 121, 107)},
|
||||
{"position": (512, 512), "rgb": (165, 133, 127)},
|
||||
{"position": (700, 400), "rgb": (217, 130, 66)},
|
||||
{"position": (100, 700), "rgb": (191, 168, 152)},
|
||||
{"position": (400, 700), "rgb": (130, 96, 77)},
|
||||
{"position": (700, 700), "rgb": (247, 203, 140)},
|
||||
{"position": (256, 256), "rgb": (167, 156, 150)},
|
||||
]
|
||||
|
||||
PIXEL_TOLERANCE = 10
|
||||
TEXT2IMG_PIXEL_TOLERANCE = 5
|
||||
@@ -115,7 +88,7 @@ def _find_free_port() -> int:
|
||||
return port
|
||||
|
||||
|
||||
def _configure_sampling_params(omni: Omni, num_inference_steps: int = 15) -> list:
|
||||
def _configure_sampling_params(omni: Omni, num_inference_steps: int = 14) -> list:
|
||||
"""Configure sampling parameters for Bagel img2img generation.
|
||||
|
||||
Args:
|
||||
@@ -258,7 +231,6 @@ def _resolve_deploy_config(config_path: str, run_level: str) -> str:
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.diffusion
|
||||
@pytest.mark.skip(reason="Skip failed CI issue 3977: https://github.com/vllm-project/vllm-omni/issues/3977")
|
||||
@hardware_test(res={"cuda": "H100", "rocm": "MI325"})
|
||||
def test_bagel_img2img_shared_memory_connector(run_level):
|
||||
"""Test Bagel img2img with shared memory connector."""
|
||||
@@ -276,7 +248,6 @@ def test_bagel_img2img_shared_memory_connector(run_level):
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.diffusion
|
||||
@pytest.mark.skip(reason="Skip failed CI issue 3977: https://github.com/vllm-project/vllm-omni/issues/3977")
|
||||
@hardware_test(res={"cuda": "H100", "rocm": "MI325"})
|
||||
def test_bagel_text2img_shared_memory_connector(run_level):
|
||||
"""Test Bagel text2img with shared memory connector."""
|
||||
|
||||
@@ -8,7 +8,7 @@ L4-style smoke runs against an already-running server. The pytest wrappers in
|
||||
H100 / MI325) because they launch the live Omni server inside the test.
|
||||
|
||||
1. **Daily-Omni** — MCQ accuracy fields in the saved JSON (``daily_omni_accuracy``, …); by default the
|
||||
run **fails** if accuracy is strictly below **0.67** (``--min-daily-omni-accuracy`` / ``ACC_BENCH_MIN_DAILY_OMNI_ACCURACY``).
|
||||
run **fails** if accuracy is strictly below **0.69** (``--min-daily-omni-accuracy`` / ``ACC_BENCH_MIN_DAILY_OMNI_ACCURACY``).
|
||||
2. **Seed-TTS** — ``seed-tts-eval``-style metrics when ``--seed-tts-wer-eval`` is used
|
||||
(WER / SIM / UTMOS keys from :func:`compute_seed_tts_wer_metrics`).
|
||||
|
||||
@@ -308,9 +308,9 @@ def build_arg_parser() -> argparse.ArgumentParser:
|
||||
p.add_argument(
|
||||
"--min-daily-omni-accuracy",
|
||||
type=float,
|
||||
default=float((os.environ.get("ACC_BENCH_MIN_DAILY_OMNI_ACCURACY") or "0.67").strip() or "0.67"),
|
||||
default=float((os.environ.get("ACC_BENCH_MIN_DAILY_OMNI_ACCURACY") or "0.69").strip() or "0.69"),
|
||||
help="Fail when daily_omni_accuracy is strictly below this threshold (0–1). "
|
||||
"Default baseline 0.67; override with env ACC_BENCH_MIN_DAILY_OMNI_ACCURACY or pass 0 to disable the floor.",
|
||||
"Default baseline 0.69; override with env ACC_BENCH_MIN_DAILY_OMNI_ACCURACY or pass 0 to disable the floor.",
|
||||
)
|
||||
|
||||
p.add_argument(
|
||||
@@ -340,7 +340,7 @@ def build_arg_parser() -> argparse.ArgumentParser:
|
||||
p.add_argument(
|
||||
"--max-seed-tts-mean-wer",
|
||||
type=float,
|
||||
default=0.5,
|
||||
default=0.25,
|
||||
help="If set, fail when seed_tts_content_error_mean is strictly above this value.",
|
||||
)
|
||||
p.add_argument(
|
||||
|
||||
@@ -26,7 +26,7 @@ HEIGHT = 1024
|
||||
WIDTH = 1024
|
||||
PROMPT = "A brown and white dog is running on the grass."
|
||||
MEAN_THRESHOLD = 3e-2
|
||||
P99_THRESHOLD = 4e-1
|
||||
P99_THRESHOLD = 3e-1
|
||||
SSIM_THRESHOLD = 0.97
|
||||
PSNR_THRESHOLD = 30.0
|
||||
|
||||
|
||||
@@ -427,6 +427,7 @@ async def _enqueue_add_request(
|
||||
sampling_params_list=sampling_params_list,
|
||||
final_stage_id=final_stage_id,
|
||||
preprocess_ms=0.0,
|
||||
request_timestamp=time.time(),
|
||||
enqueue_ts=time.perf_counter(),
|
||||
)
|
||||
)
|
||||
@@ -984,6 +985,7 @@ async def test_handle_streaming_update_passes_prompt_text_to_stage_pool() -> Non
|
||||
sampling_params_list=[_sampling_params()],
|
||||
final_stage_id=0,
|
||||
preprocess_ms=0.0,
|
||||
request_timestamp=time.time(),
|
||||
enqueue_ts=time.perf_counter(),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -156,7 +156,7 @@ class FakeAsyncOmni:
|
||||
self.captured_prompt = None
|
||||
self._images = images or [Image.new("RGB", (64, 64), color="green")]
|
||||
|
||||
async def generate(self, prompt, request_id, sampling_params=None, sampling_params_list=None):
|
||||
async def generate(self, prompt, request_id, sampling_params=None, sampling_params_list=None, **kwargs):
|
||||
if sampling_params_list is not None:
|
||||
self.captured_sampling_params_list = sampling_params_list
|
||||
else:
|
||||
@@ -254,7 +254,7 @@ def async_omni_test_client():
|
||||
self._images = [Image.new("RGB", (64, 64), color="green")]
|
||||
self.od_config = SimpleNamespace(supports_multimodal_inputs=True)
|
||||
|
||||
async def generate(self, prompt, request_id, sampling_params=None, sampling_params_list=None):
|
||||
async def generate(self, prompt, request_id, sampling_params=None, sampling_params_list=None, **kwargs):
|
||||
if sampling_params_list is not None:
|
||||
self.captured_sampling_params_list = sampling_params_list
|
||||
else:
|
||||
@@ -318,7 +318,7 @@ def async_omni_rgba_test_client():
|
||||
self._images = [Image.new("RGBA", (64, 64), color=(0, 255, 0, 128))]
|
||||
self.od_config = SimpleNamespace(supports_multimodal_inputs=True)
|
||||
|
||||
async def generate(self, prompt, request_id, sampling_params=None, sampling_params_list=None):
|
||||
async def generate(self, prompt, request_id, sampling_params=None, sampling_params_list=None, **kwargs):
|
||||
if sampling_params_list is not None:
|
||||
self.captured_sampling_params_list = sampling_params_list
|
||||
else:
|
||||
@@ -382,7 +382,7 @@ def async_omni_stage_configs_only_client():
|
||||
self._images = [Image.new("RGB", (64, 64), color="green")]
|
||||
self.od_config = SimpleNamespace(supports_multimodal_inputs=True)
|
||||
|
||||
async def generate(self, prompt, request_id, sampling_params=None, sampling_params_list=None):
|
||||
async def generate(self, prompt, request_id, sampling_params=None, sampling_params_list=None, **kwargs):
|
||||
if sampling_params_list is not None:
|
||||
self.captured_sampling_params_list = sampling_params_list
|
||||
else:
|
||||
@@ -442,7 +442,7 @@ def streaming_image_edit_client():
|
||||
self.captured_prompt = None
|
||||
self.od_config = SimpleNamespace(supports_multimodal_inputs=True)
|
||||
|
||||
async def generate(self, prompt, request_id, sampling_params=None, sampling_params_list=None):
|
||||
async def generate(self, prompt, request_id, sampling_params=None, sampling_params_list=None, **kwargs):
|
||||
self.captured_prompt = prompt
|
||||
self.captured_sampling_params_list = sampling_params_list or [sampling_params]
|
||||
assert self.captured_sampling_params_list[0].output_kind == RequestOutputKind.DELTA
|
||||
@@ -696,7 +696,7 @@ def test_generate_images_async_omni_glm_image_sets_stage0_max_tokens():
|
||||
self._images = [Image.new("RGB", (64, 64), color="green")]
|
||||
self.od_config = SimpleNamespace(supports_multimodal_inputs=True)
|
||||
|
||||
async def generate(self, prompt, request_id, sampling_params=None, sampling_params_list=None):
|
||||
async def generate(self, prompt, request_id, sampling_params=None, sampling_params_list=None, **kwargs):
|
||||
self.captured_sampling_params_list = (
|
||||
sampling_params_list if sampling_params_list is not None else [sampling_params]
|
||||
)
|
||||
@@ -815,7 +815,9 @@ def test_image_edits_streaming_returns_ar_delta_then_image(streaming_image_edit_
|
||||
|
||||
|
||||
def test_image_edits_streaming_ar_delta_chunks_include_index(streaming_image_edit_client):
|
||||
async def generate_multi_output_delta(prompt, request_id, sampling_params=None, sampling_params_list=None):
|
||||
async def generate_multi_output_delta(
|
||||
prompt, request_id, sampling_params=None, sampling_params_list=None, **kwargs
|
||||
):
|
||||
yield MockStageResult(stage_id=0, final_output_type="text", texts=["first", "second"])
|
||||
yield MockStageResult(
|
||||
stage_id=1,
|
||||
@@ -843,7 +845,7 @@ def test_image_edits_streaming_ar_delta_chunks_include_index(streaming_image_edi
|
||||
|
||||
|
||||
def test_image_edits_streaming_errors_without_final_image(streaming_image_edit_client):
|
||||
async def generate_without_image(prompt, request_id, sampling_params=None, sampling_params_list=None):
|
||||
async def generate_without_image(prompt, request_id, sampling_params=None, sampling_params_list=None, **kwargs):
|
||||
yield MockStageResult(stage_id=0, final_output_type="text", text="recap")
|
||||
|
||||
streaming_image_edit_client.app.state.engine_client.generate = generate_without_image
|
||||
@@ -866,7 +868,7 @@ def test_image_edits_streaming_errors_without_final_image(streaming_image_edit_c
|
||||
|
||||
|
||||
def test_image_edits_streaming_errors_on_empty_final_image(streaming_image_edit_client):
|
||||
async def generate_empty_image(prompt, request_id, sampling_params=None, sampling_params_list=None):
|
||||
async def generate_empty_image(prompt, request_id, sampling_params=None, sampling_params_list=None, **kwargs):
|
||||
yield MockStageResult(stage_id=0, final_output_type="text", text="recap")
|
||||
yield MockStageResult(stage_id=1, final_output_type="image", images=[])
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ OMNI_MODEL = "Qwen/Qwen2.5-Omni-7B"
|
||||
OMNI_STAGE_CONFIG = get_deploy_config_path("ci/qwen2_5_omni_thinker_only.yaml")
|
||||
|
||||
|
||||
async def _noop(**kw):
|
||||
async def _noop(*args, **kw):
|
||||
pass
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@ def get_async_omni_instance(fake_add_request=_noop, fake_abort_request=_noop) ->
|
||||
omni._final_output_handler = lambda: None
|
||||
omni.resolve_sampling_params_list = lambda params, allow_delta_coercion: params
|
||||
omni._compute_final_stage_id = lambda output_modalities: 0
|
||||
omni._compute_final_output_stage_ids = lambda output_modalities: [0]
|
||||
omni._process_orchestrator_results = fake_process_results
|
||||
omni._log_summary_and_cleanup = lambda request_id: omni.request_states.pop(request_id, None)
|
||||
return omni
|
||||
|
||||
@@ -154,6 +154,7 @@ class TestRequestLifecycleGauges:
|
||||
obj.engine = SimpleNamespace(_running_counter=OmniRequestCounter())
|
||||
obj.prom_metrics = OmniPrometheusMetrics(model_name="lifecycle-test")
|
||||
obj.request_states = {}
|
||||
obj._consumed_metric_messages = {}
|
||||
obj.log_stats = False
|
||||
|
||||
# Simulate request lifecycle: start (counter 0→1, dict {} → {req}),
|
||||
@@ -182,6 +183,7 @@ class TestRequestLifecycleGauges:
|
||||
obj.engine = SimpleNamespace(_running_counter=OmniRequestCounter())
|
||||
obj.prom_metrics = OmniPrometheusMetrics(model_name="lifecycle-test-2")
|
||||
obj.request_states = {}
|
||||
obj._consumed_metric_messages = {}
|
||||
obj.log_stats = False
|
||||
|
||||
# Two in flight, one finalizes — running should report 1, waiting 0.
|
||||
|
||||
@@ -1638,9 +1638,6 @@ class TestBaseConfigInheritance:
|
||||
s0 = deploy.stages[0].default_sampling_params
|
||||
# CI overrides max_tokens
|
||||
assert s0["max_tokens"] == 150
|
||||
# Inherited from base
|
||||
assert s0["temperature"] == 0.4
|
||||
assert s0["seed"] == 42
|
||||
|
||||
def test_pure_inheritance_overlay(self, tmp_path):
|
||||
"""An overlay with only ``base_config`` inherits everything."""
|
||||
@@ -1960,5 +1957,3 @@ class TestSamplingConstraintsPrecedence:
|
||||
assert stages[0].yaml_extras["default_sampling_params"]["detokenize"] is True
|
||||
# Pipeline says stop_token_ids=[2150] for talker
|
||||
assert stages[1].yaml_extras["default_sampling_params"]["stop_token_ids"] == [2150]
|
||||
# Deploy temperature still flows through
|
||||
assert stages[0].yaml_extras["default_sampling_params"]["temperature"] == 0.4
|
||||
@@ -1,5 +1,6 @@
|
||||
import warnings
|
||||
from dataclasses import dataclass
|
||||
from collections import defaultdict
|
||||
from dataclasses import field, make_dataclass
|
||||
|
||||
import numpy as np
|
||||
from vllm.benchmarks.datasets import SampleRequest
|
||||
@@ -9,30 +10,172 @@ from vllm.tokenizers import TokenizerLike
|
||||
|
||||
from vllm_omni.metrics import definitions as defs
|
||||
|
||||
_PERCENTILE_ROWS_TYPE = list[tuple[float, float]] | None
|
||||
_FLOAT_LIST_TYPE = list[float]
|
||||
_INT_LIST_TYPE = list[int]
|
||||
|
||||
@dataclass
|
||||
class MultiModalsBenchmarkMetrics(BenchmarkMetrics):
|
||||
mean_audio_ttfp_ms: float = 0.0
|
||||
median_audio_ttfp_ms: float = 0.0
|
||||
std_audio_ttfp_ms: float = 0.0
|
||||
percentiles_audio_ttfp_ms: list[tuple[float, float]] = None
|
||||
total_audio_duration_s: float = 0.0
|
||||
total_audio_frames: int = 0
|
||||
audio_throughput: float = 0.0
|
||||
mean_audio_rtf: float = 0.0
|
||||
median_audio_rtf: float = 0.0
|
||||
std_audio_rtf: float = 0.0
|
||||
percentiles_audio_rtf: list[tuple[float, float]] = None
|
||||
mean_audio_duration_s: float = 0.0
|
||||
median_audio_duration_s: float = 0.0
|
||||
std_audio_duration_s: float = 0.0
|
||||
percentiles_audio_duration_s: list[tuple[float, float]] = None
|
||||
# Streaming-audio continuity (see vllm_omni/benchmarks/audio_continuity.py).
|
||||
mean_audio_underrun_s: float = 0.0
|
||||
median_audio_underrun_s: float = 0.0
|
||||
std_audio_underrun_s: float = 0.0
|
||||
percentiles_audio_underrun_s: list[tuple[float, float]] = None
|
||||
audio_continuity_ok_rate: float = 1.0
|
||||
_MULTIMODAL_BENCHMARK_FIELDS = [
|
||||
(defs.MEAN_AUDIO_TTFP_MS, float, field(default=0.0)),
|
||||
(defs.MEDIAN_AUDIO_TTFP_MS, float, field(default=0.0)),
|
||||
(defs.STD_AUDIO_TTFP_MS, float, field(default=0.0)),
|
||||
(defs.PERCENTILES_AUDIO_TTFP_MS, _PERCENTILE_ROWS_TYPE, field(default=None)),
|
||||
(defs.TOTAL_AUDIO_DURATION_S, float, field(default=0.0)),
|
||||
(defs.TOTAL_AUDIO_FRAMES, int, field(default=0)),
|
||||
(defs.AUDIO_THROUGHPUT, float, field(default=0.0)),
|
||||
(defs.MEAN_AUDIO_RTF, float, field(default=0.0)),
|
||||
(defs.MEDIAN_AUDIO_RTF, float, field(default=0.0)),
|
||||
(defs.STD_AUDIO_RTF, float, field(default=0.0)),
|
||||
(defs.PERCENTILES_AUDIO_RTF, _PERCENTILE_ROWS_TYPE, field(default=None)),
|
||||
(defs.MEAN_AUDIO_DURATION_S, float, field(default=0.0)),
|
||||
(defs.MEDIAN_AUDIO_DURATION_S, float, field(default=0.0)),
|
||||
(defs.STD_AUDIO_DURATION_S, float, field(default=0.0)),
|
||||
(defs.PERCENTILES_AUDIO_DURATION_S, _PERCENTILE_ROWS_TYPE, field(default=None)),
|
||||
(defs.MEAN_AUDIO_UNDERRUN_S, float, field(default=0.0)),
|
||||
(defs.MEDIAN_AUDIO_UNDERRUN_S, float, field(default=0.0)),
|
||||
(defs.STD_AUDIO_UNDERRUN_S, float, field(default=0.0)),
|
||||
(defs.PERCENTILES_AUDIO_UNDERRUN_S, _PERCENTILE_ROWS_TYPE, field(default=None)),
|
||||
(defs.AUDIO_CONTINUITY_OK_RATE, float, field(default=1.0)),
|
||||
(defs.TOTAL_IMAGES, int, field(default=0)),
|
||||
(defs.IMAGE_THROUGHPUT, float, field(default=0.0)),
|
||||
(defs.AVERAGE_PIXELS_PER_IMAGE, float, field(default=0.0)),
|
||||
(defs.MEAN_DENOISE_STEP_LATENCY_MS, float, field(default=0.0)),
|
||||
(defs.MEAN_IMAGE_GENERATION_MS, float, field(default=0.0)),
|
||||
(defs.MEDIAN_IMAGE_GENERATION_MS, float, field(default=0.0)),
|
||||
(defs.STD_IMAGE_GENERATION_MS, float, field(default=0.0)),
|
||||
(defs.PERCENTILES_IMAGE_GENERATION_MS, _PERCENTILE_ROWS_TYPE, field(default=None)),
|
||||
]
|
||||
|
||||
MultiModalsBenchmarkMetrics = make_dataclass(
|
||||
"MultiModalsBenchmarkMetrics",
|
||||
_MULTIMODAL_BENCHMARK_FIELDS,
|
||||
bases=(BenchmarkMetrics,),
|
||||
)
|
||||
|
||||
_STAGE_BENCHMARK_FIELDS = [
|
||||
("stage_id", int, field(default=0)),
|
||||
("stage_name", str, field(default="")),
|
||||
("final_output_type", str, field(default="")),
|
||||
(defs.TOTAL_OUTPUT, int, field(default=0)),
|
||||
(defs.TTFTS, _FLOAT_LIST_TYPE, field(default_factory=list)),
|
||||
(defs.TPOTS, _FLOAT_LIST_TYPE, field(default_factory=list)),
|
||||
(defs.ITLS, _FLOAT_LIST_TYPE, field(default_factory=list)),
|
||||
(defs.VLLM_TTFTS, _FLOAT_LIST_TYPE, field(default_factory=list)),
|
||||
(defs.VLLM_TPOTS, _FLOAT_LIST_TYPE, field(default_factory=list)),
|
||||
(defs.VLLM_ITLS, _FLOAT_LIST_TYPE, field(default_factory=list)),
|
||||
(defs.AUDIO_TTFPS, _FLOAT_LIST_TYPE, field(default_factory=list)),
|
||||
(defs.AUDIO_RTFS, _FLOAT_LIST_TYPE, field(default_factory=list)),
|
||||
(defs.AUDIO_DURATIONS, _FLOAT_LIST_TYPE, field(default_factory=list)),
|
||||
(defs.AUDIO_FRAMES, _INT_LIST_TYPE, field(default_factory=list)),
|
||||
(defs.MISSING_AUDIO_DURATION_COUNT, int, field(default=0)),
|
||||
(defs.MISSING_AUDIO_RTF_COUNT, int, field(default=0)),
|
||||
(defs.STAGE_GEN_TIMES_MS, _FLOAT_LIST_TYPE, field(default_factory=list)),
|
||||
(defs.POSTPROCESS_TIMES_MS, _FLOAT_LIST_TYPE, field(default_factory=list)),
|
||||
("output_unit_type", str, field(default="")),
|
||||
(defs.OUTPUT_UNIT_COUNT, int, field(default=0)),
|
||||
(defs.SERVING_TIME_TO_FIRST_OUTPUTS_MS, _FLOAT_LIST_TYPE, field(default_factory=list)),
|
||||
(defs.TIME_PER_OUTPUT_UNITS_MS, _FLOAT_LIST_TYPE, field(default_factory=list)),
|
||||
(defs.INTER_OUTPUT_LATENCIES_MS, _FLOAT_LIST_TYPE, field(default_factory=list)),
|
||||
]
|
||||
|
||||
StageBenchmarkMetrics = make_dataclass(
|
||||
"StageBenchmarkMetrics",
|
||||
_STAGE_BENCHMARK_FIELDS,
|
||||
namespace={"__doc__": "Aggregated metrics for one pipeline stage (for printing only)."},
|
||||
)
|
||||
|
||||
|
||||
def _percentile_rows_seconds(
|
||||
values: list[float], selected_percentiles: list[float], *, to_ms: bool
|
||||
) -> tuple[float, float, list[tuple[float, float]]]:
|
||||
arr = np.asarray(values, dtype=float)
|
||||
scale = 1000.0 if to_ms else 1.0
|
||||
mean_v = float(np.mean(arr)) * scale
|
||||
median_v = float(np.median(arr)) * scale
|
||||
rows = [(float(p), float(np.percentile(arr, p)) * scale) for p in selected_percentiles]
|
||||
return mean_v, median_v, rows
|
||||
|
||||
|
||||
def _p_label(p: float) -> str:
|
||||
pf = float(p)
|
||||
if abs(pf - round(pf)) < 1e-9:
|
||||
return str(int(round(pf)))
|
||||
return str(pf)
|
||||
|
||||
|
||||
_STREAMING_OUTPUT_UNIT_TYPES = frozenset(
|
||||
{
|
||||
"text",
|
||||
"stream",
|
||||
"audio",
|
||||
}
|
||||
)
|
||||
|
||||
_AGGREGATE_PERCENTILE_FIELD_NAMES = {
|
||||
defs.AUDIO_TTFP: (
|
||||
defs.MEAN_AUDIO_TTFP_MS,
|
||||
defs.MEDIAN_AUDIO_TTFP_MS,
|
||||
defs.PERCENTILES_AUDIO_TTFP_MS,
|
||||
),
|
||||
defs.AUDIO_RTF: (
|
||||
defs.MEAN_AUDIO_RTF,
|
||||
defs.MEDIAN_AUDIO_RTF,
|
||||
defs.PERCENTILES_AUDIO_RTF,
|
||||
),
|
||||
defs.AUDIO_DURATION: (
|
||||
defs.MEAN_AUDIO_DURATION_S,
|
||||
defs.MEDIAN_AUDIO_DURATION_S,
|
||||
defs.PERCENTILES_AUDIO_DURATION_S,
|
||||
),
|
||||
defs.AUDIO_UNDERRUN: (
|
||||
defs.MEAN_AUDIO_UNDERRUN_S,
|
||||
defs.MEDIAN_AUDIO_UNDERRUN_S,
|
||||
defs.PERCENTILES_AUDIO_UNDERRUN_S,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def _wants_text_ttft(metrics: list[str]) -> bool:
|
||||
return "ttft" in metrics
|
||||
|
||||
|
||||
def _wants_text_tpot(metrics: list[str]) -> bool:
|
||||
return "tpot" in metrics or "tpop" in metrics
|
||||
|
||||
|
||||
def _wants_text_itl(metrics: list[str]) -> bool:
|
||||
return "itl" in metrics
|
||||
|
||||
|
||||
def _wants_stream_ttfc(metrics: list[str]) -> bool:
|
||||
return "ttfc" in metrics
|
||||
|
||||
|
||||
def _wants_stream_tpoc(metrics: list[str]) -> bool:
|
||||
return "tpoc" in metrics or "tpop" in metrics
|
||||
|
||||
|
||||
def _wants_stream_icl(metrics: list[str]) -> bool:
|
||||
return "icl" in metrics
|
||||
|
||||
|
||||
def _stage_modality_flags(
|
||||
final_output_type: str,
|
||||
output_unit_type: str,
|
||||
) -> tuple[bool, bool, bool, bool, bool]:
|
||||
is_text_stage = final_output_type == "text" or output_unit_type == "text"
|
||||
is_audio_stage = final_output_type == "audio" or output_unit_type == "audio"
|
||||
is_image_stage = final_output_type in {"image", "images"} or output_unit_type == "image"
|
||||
is_video_stage = final_output_type in {"video", "videos"} or output_unit_type == "video"
|
||||
is_internal_stream_stage = (
|
||||
output_unit_type in _STREAMING_OUTPUT_UNIT_TYPES and not is_text_stage and not is_audio_stage
|
||||
)
|
||||
return (
|
||||
is_text_stage,
|
||||
is_audio_stage,
|
||||
is_image_stage,
|
||||
is_video_stage,
|
||||
is_internal_stream_stage,
|
||||
)
|
||||
|
||||
|
||||
def print_metrics(
|
||||
@@ -43,6 +186,10 @@ def print_metrics(
|
||||
benchmark_duration,
|
||||
goodput_config_dict,
|
||||
metrics: MultiModalsBenchmarkMetrics,
|
||||
*,
|
||||
outputs: list[RequestFuncOutput] | None = None,
|
||||
selected_percentiles: list[float] | None = None,
|
||||
print_stage: bool = False,
|
||||
):
|
||||
print("{s:{c}^{n}}".format(s=" Serving Benchmark Result ", n=50, c="="))
|
||||
print("{:<40} {:<10}".format("Successful requests:", metrics.completed))
|
||||
@@ -61,7 +208,19 @@ def print_metrics(
|
||||
process_one_metric("e2el", metrics)
|
||||
print_text_metrics(task_type, selected_percentile_metrics, metrics)
|
||||
if task_type == TaskType.GENERATION:
|
||||
print_audio_metrics(selected_percentile_metrics, metrics)
|
||||
if _has_audio_output(metrics):
|
||||
print_audio_metrics(selected_percentile_metrics, metrics)
|
||||
if _has_image_output(metrics):
|
||||
print_image_metrics(selected_percentiles or [], metrics)
|
||||
if print_stage and outputs and selected_percentiles is not None:
|
||||
print("\n{s:{c}^{n}}".format(s=" Stage Benchmark Result ", n=50, c="="))
|
||||
for sm in _build_stage_metrics_from_outputs(outputs):
|
||||
print_stage_metrics(
|
||||
task_type,
|
||||
selected_percentile_metrics,
|
||||
selected_percentiles,
|
||||
sm,
|
||||
)
|
||||
print("=" * 50)
|
||||
|
||||
|
||||
@@ -79,22 +238,36 @@ def print_text_metrics(task_type, selected_percentile_metrics, metrics: MultiMod
|
||||
# No text tokens generated (e.g. pure TTS speech endpoint): per-token
|
||||
# latency metrics (ttft/tpot/itl) are undefined, so skip them.
|
||||
has_text_output = metrics.total_output > 0
|
||||
for metric in selected_percentile_metrics:
|
||||
if metric == "e2el":
|
||||
continue
|
||||
if not metric.startswith("audio") and has_text_output:
|
||||
process_one_metric(metric, metrics)
|
||||
if has_text_output:
|
||||
if _wants_text_ttft(selected_percentile_metrics):
|
||||
process_one_metric("ttft", metrics)
|
||||
if _wants_text_tpot(selected_percentile_metrics):
|
||||
process_one_metric("tpot", metrics)
|
||||
if _wants_text_itl(selected_percentile_metrics):
|
||||
process_one_metric("itl", metrics)
|
||||
|
||||
|
||||
def _has_audio_output(metrics: MultiModalsBenchmarkMetrics) -> bool:
|
||||
return bool(
|
||||
getattr(metrics, defs.TOTAL_AUDIO_DURATION_S, 0.0) > 0 or getattr(metrics, defs.TOTAL_AUDIO_FRAMES, 0) > 0
|
||||
)
|
||||
|
||||
|
||||
def _has_image_output(metrics: MultiModalsBenchmarkMetrics) -> bool:
|
||||
return int(getattr(metrics, defs.TOTAL_IMAGES, 0) or 0) > 0
|
||||
|
||||
|
||||
def print_audio_metrics(selected_percentile_metrics, metrics: MultiModalsBenchmarkMetrics):
|
||||
print("{s:{c}^{n}}".format(s=" Audio Result ", n=50, c="="))
|
||||
print("{:<40} {:<10.2f}".format("Total audio duration generated(s):", metrics.total_audio_duration_s))
|
||||
print("{:<40} {:<10}".format("Total audio frames generated:", metrics.total_audio_frames))
|
||||
print("{:<40} {:<10.2f}".format("Audio throughput(audio duration/s):", metrics.audio_throughput))
|
||||
print(
|
||||
"{:<40} {:<10.2f}".format("Total audio duration generated(s):", getattr(metrics, defs.TOTAL_AUDIO_DURATION_S))
|
||||
)
|
||||
print("{:<40} {:<10}".format("Total audio frames generated:", getattr(metrics, defs.TOTAL_AUDIO_FRAMES)))
|
||||
print("{:<40} {:<10.2f}".format("Audio throughput(audio duration/s):", getattr(metrics, defs.AUDIO_THROUGHPUT)))
|
||||
print(
|
||||
"{:<40} {:<10.2%}".format(
|
||||
"Streaming continuity OK rate:",
|
||||
metrics.audio_continuity_ok_rate,
|
||||
getattr(metrics, defs.AUDIO_CONTINUITY_OK_RATE),
|
||||
)
|
||||
)
|
||||
for metric in selected_percentile_metrics:
|
||||
@@ -102,6 +275,37 @@ def print_audio_metrics(selected_percentile_metrics, metrics: MultiModalsBenchma
|
||||
process_one_metric(metric, metrics)
|
||||
|
||||
|
||||
def print_image_metrics(selected_percentiles: list[float], metrics: MultiModalsBenchmarkMetrics):
|
||||
print("{s:{c}^{n}}".format(s=" Image Result ", n=50, c="="))
|
||||
print("{:<40} {:<10}".format("Total images generated:", getattr(metrics, defs.TOTAL_IMAGES)))
|
||||
print("{:<40} {:<10.2f}".format("Image throughput (img/s):", getattr(metrics, defs.IMAGE_THROUGHPUT)))
|
||||
print(
|
||||
"{:<40} {:<10.2f}".format(
|
||||
"Average pixels per image:",
|
||||
getattr(metrics, defs.AVERAGE_PIXELS_PER_IMAGE),
|
||||
)
|
||||
)
|
||||
if getattr(metrics, defs.MEAN_DENOISE_STEP_LATENCY_MS) > 0:
|
||||
print(
|
||||
"{:<40} {:<10.2f}".format(
|
||||
"Mean denoise step latency (ms):",
|
||||
getattr(metrics, defs.MEAN_DENOISE_STEP_LATENCY_MS),
|
||||
)
|
||||
)
|
||||
if getattr(metrics, defs.MEAN_IMAGE_GENERATION_MS) > 0:
|
||||
print("-----------------Image Generation-----------------")
|
||||
print("{:<40} {:<10.2f}".format("Mean IMAGE_GENERATION (ms):", getattr(metrics, defs.MEAN_IMAGE_GENERATION_MS)))
|
||||
print(
|
||||
"{:<40} {:<10.2f}".format(
|
||||
"Median IMAGE_GENERATION (ms):",
|
||||
getattr(metrics, defs.MEDIAN_IMAGE_GENERATION_MS),
|
||||
)
|
||||
)
|
||||
for p, value in getattr(metrics, defs.PERCENTILES_IMAGE_GENERATION_MS) or []:
|
||||
if p in selected_percentiles:
|
||||
print("{:<40} {:<10.2f}".format(f"P{_p_label(p)} IMAGE_GENERATION (ms):", value))
|
||||
|
||||
|
||||
def process_one_metric(
|
||||
metric_attribute_name: str,
|
||||
metrics: MultiModalsBenchmarkMetrics,
|
||||
@@ -131,15 +335,16 @@ def process_one_metric(
|
||||
elif is_audio_rtf:
|
||||
suffix = ""
|
||||
unit_suffix = ""
|
||||
mean_attr_name = f"mean_{metric_attribute_name}{suffix}"
|
||||
field_names = _AGGREGATE_PERCENTILE_FIELD_NAMES.get(metric_attribute_name)
|
||||
mean_attr_name = field_names[0] if field_names else f"mean_{metric_attribute_name}{suffix}"
|
||||
mean_value = getattr(metrics, mean_attr_name, 0.0)
|
||||
print(f"{f'Mean {metric_attribute_name.upper()}{unit_suffix}:':<40} {mean_value:<10.2f}")
|
||||
|
||||
median_attr_name = f"median_{metric_attribute_name}{suffix}"
|
||||
median_attr_name = field_names[1] if field_names else f"median_{metric_attribute_name}{suffix}"
|
||||
median_value = getattr(metrics, median_attr_name, 0.0)
|
||||
print(f"{f'Median {metric_attribute_name.upper()}{unit_suffix}:':<40} {median_value:<10.2f}")
|
||||
|
||||
percentiles_attr_name = f"percentiles_{metric_attribute_name}{suffix}"
|
||||
percentiles_attr_name = field_names[2] if field_names else f"percentiles_{metric_attribute_name}{suffix}"
|
||||
percentiles = getattr(metrics, percentiles_attr_name, [])
|
||||
|
||||
for percentile, value in percentiles:
|
||||
@@ -148,6 +353,356 @@ def process_one_metric(
|
||||
print(f"{label:<40} {value:<10.2f}")
|
||||
|
||||
|
||||
def _print_percentile_metric(
|
||||
header: str,
|
||||
label: str,
|
||||
values: list[float],
|
||||
selected_percentiles: list[float],
|
||||
*,
|
||||
values_are_ms: bool = False,
|
||||
to_ms: bool = True,
|
||||
) -> None:
|
||||
if not values:
|
||||
return
|
||||
values_s = [v / 1000.0 for v in values] if values_are_ms else values
|
||||
mean_v, med_v, pcts = _percentile_rows_seconds(values_s, selected_percentiles, to_ms=to_ms)
|
||||
unit_suffix = " (ms)" if to_ms else ""
|
||||
print("{s:{c}^{n}}".format(s=header, n=50, c="-"))
|
||||
print(f"{f'Mean {label}{unit_suffix}:':<40} {mean_v:<10.2f}")
|
||||
print(f"{f'Median {label}{unit_suffix}:':<40} {med_v:<10.2f}")
|
||||
for p, v in pcts:
|
||||
p_str = _p_label(p)
|
||||
print(f"{f'P{p_str} {label}{unit_suffix}:':<40} {v:<10.2f}")
|
||||
|
||||
|
||||
def _print_stage_timing(sm: StageBenchmarkMetrics, selected_percentiles: list[float]) -> None:
|
||||
_print_percentile_metric(
|
||||
"Stage Timing",
|
||||
"stage_gen_time",
|
||||
getattr(sm, defs.STAGE_GEN_TIMES_MS),
|
||||
selected_percentiles,
|
||||
values_are_ms=True,
|
||||
to_ms=True,
|
||||
)
|
||||
postprocess_times_ms = getattr(sm, defs.POSTPROCESS_TIMES_MS)
|
||||
if postprocess_times_ms and any(v > 0 for v in postprocess_times_ms):
|
||||
_print_percentile_metric(
|
||||
"Stage Postprocess",
|
||||
"postprocess",
|
||||
postprocess_times_ms,
|
||||
selected_percentiles,
|
||||
values_are_ms=True,
|
||||
to_ms=True,
|
||||
)
|
||||
|
||||
|
||||
def _print_text_stage_metrics(
|
||||
task_type: TaskType,
|
||||
selected_percentile_metrics: list[str],
|
||||
selected_percentiles: list[float],
|
||||
sm: StageBenchmarkMetrics,
|
||||
) -> None:
|
||||
print("{s:{c}^{n}}".format(s=" Text Result ", n=50, c="="))
|
||||
print("{:<40} {:<10}".format("Stage generated tokens:", getattr(sm, defs.TOTAL_OUTPUT)))
|
||||
|
||||
if task_type != TaskType.GENERATION:
|
||||
return
|
||||
if _wants_text_ttft(selected_percentile_metrics):
|
||||
_print_percentile_metric(
|
||||
"Serving Time to First Token",
|
||||
"Serving TTFT",
|
||||
getattr(sm, defs.TTFTS),
|
||||
selected_percentiles,
|
||||
)
|
||||
_print_percentile_metric("Time to First Token", "TTFT", getattr(sm, defs.VLLM_TTFTS), selected_percentiles)
|
||||
if _wants_text_tpot(selected_percentile_metrics):
|
||||
_print_percentile_metric(
|
||||
"Time per Output Token (excl. 1st token)",
|
||||
"TPOT",
|
||||
getattr(sm, defs.VLLM_TPOTS),
|
||||
selected_percentiles,
|
||||
)
|
||||
if _wants_text_itl(selected_percentile_metrics):
|
||||
_print_percentile_metric("Inter-token Latency", "ITL", getattr(sm, defs.VLLM_ITLS), selected_percentiles)
|
||||
|
||||
|
||||
def _print_audio_stage_metrics(
|
||||
selected_percentile_metrics: list[str],
|
||||
selected_percentiles: list[float],
|
||||
sm: StageBenchmarkMetrics,
|
||||
) -> None:
|
||||
total_duration = float(sum(getattr(sm, defs.AUDIO_DURATIONS)))
|
||||
total_frames = int(sum(getattr(sm, defs.AUDIO_FRAMES)))
|
||||
print("{s:{c}^{n}}".format(s=" Audio Result ", n=50, c="="))
|
||||
print("{:<40} {:<10.2f}".format("Stage audio duration generated(s):", total_duration))
|
||||
print("{:<40} {:<10}".format("Stage audio frames generated:", total_frames))
|
||||
if defs.AUDIO_TTFP in selected_percentile_metrics:
|
||||
_print_percentile_metric(
|
||||
"Serving Time to First Packet",
|
||||
"Serving AUDIO_TTFP",
|
||||
getattr(sm, defs.AUDIO_TTFPS),
|
||||
selected_percentiles,
|
||||
)
|
||||
if defs.AUDIO_RTF in selected_percentile_metrics:
|
||||
audio_rtfs = getattr(sm, defs.AUDIO_RTFS)
|
||||
if audio_rtfs:
|
||||
_print_percentile_metric("Real Time Factor", "AUDIO_RTF", audio_rtfs, selected_percentiles, to_ms=False)
|
||||
elif getattr(sm, defs.MISSING_AUDIO_RTF_COUNT) > 0:
|
||||
print("{:<40} {:<10}".format("AUDIO_RTF skipped:", "missing stage-local audio duration/sample rate"))
|
||||
if defs.AUDIO_DURATION in selected_percentile_metrics:
|
||||
audio_durations = getattr(sm, defs.AUDIO_DURATIONS)
|
||||
if audio_durations:
|
||||
_print_percentile_metric(
|
||||
"Audio Duration",
|
||||
"AUDIO_DURATION",
|
||||
audio_durations,
|
||||
selected_percentiles,
|
||||
to_ms=False,
|
||||
)
|
||||
elif getattr(sm, defs.MISSING_AUDIO_DURATION_COUNT) > 0:
|
||||
print("{:<40} {:<10}".format("AUDIO_DURATION skipped:", "missing stage-local audio duration/sample rate"))
|
||||
|
||||
|
||||
def _print_image_stage_metrics(
|
||||
selected_percentiles: list[float],
|
||||
sm: StageBenchmarkMetrics,
|
||||
) -> None:
|
||||
image_num = getattr(sm, defs.OUTPUT_UNIT_COUNT) if getattr(sm, "output_unit_type") == "image" else 0
|
||||
print("{s:{c}^{n}}".format(s=" Image Result ", n=50, c="="))
|
||||
print("{:<40} {:<10}".format("Total images generated:", image_num))
|
||||
_print_percentile_metric(
|
||||
"Image Generation",
|
||||
"IMAGE_GENERATION",
|
||||
getattr(sm, defs.STAGE_GEN_TIMES_MS),
|
||||
selected_percentiles,
|
||||
values_are_ms=True,
|
||||
to_ms=True,
|
||||
)
|
||||
|
||||
|
||||
def _print_internal_stream_stage_metrics(
|
||||
selected_percentile_metrics: list[str],
|
||||
selected_percentiles: list[float],
|
||||
sm: StageBenchmarkMetrics,
|
||||
) -> None:
|
||||
print("{s:{c}^{n}}".format(s=" Internal Stream Result ", n=50, c="="))
|
||||
serving_time_to_first_outputs_ms = getattr(sm, defs.SERVING_TIME_TO_FIRST_OUTPUTS_MS)
|
||||
if _wants_stream_ttfc(selected_percentile_metrics) and serving_time_to_first_outputs_ms:
|
||||
_print_percentile_metric(
|
||||
"Serving Time to First Chunk",
|
||||
"Serving TTFC",
|
||||
serving_time_to_first_outputs_ms,
|
||||
selected_percentiles,
|
||||
values_are_ms=True,
|
||||
to_ms=True,
|
||||
)
|
||||
time_per_output_units_ms = getattr(sm, defs.TIME_PER_OUTPUT_UNITS_MS)
|
||||
if _wants_stream_tpoc(selected_percentile_metrics) and time_per_output_units_ms:
|
||||
_print_percentile_metric(
|
||||
"Time per Output Chunk (excl. 1st chunk)",
|
||||
"TPOP",
|
||||
time_per_output_units_ms,
|
||||
selected_percentiles,
|
||||
values_are_ms=True,
|
||||
to_ms=True,
|
||||
)
|
||||
inter_output_latencies_ms = getattr(sm, defs.INTER_OUTPUT_LATENCIES_MS)
|
||||
if _wants_stream_icl(selected_percentile_metrics) and inter_output_latencies_ms:
|
||||
_print_percentile_metric(
|
||||
"Inter-chunk Latency",
|
||||
"ICL",
|
||||
inter_output_latencies_ms,
|
||||
selected_percentiles,
|
||||
values_are_ms=True,
|
||||
to_ms=True,
|
||||
)
|
||||
|
||||
|
||||
def print_stage_metrics(
|
||||
task_type: TaskType,
|
||||
selected_percentile_metrics: list[str],
|
||||
selected_percentiles: list[float],
|
||||
sm: StageBenchmarkMetrics,
|
||||
):
|
||||
title = f" Stage {getattr(sm, 'stage_id')} ({getattr(sm, 'stage_name')}) "
|
||||
(
|
||||
is_text_stage,
|
||||
is_audio_stage,
|
||||
is_image_stage,
|
||||
is_video_stage,
|
||||
is_internal_stream_stage,
|
||||
) = _stage_modality_flags(getattr(sm, "final_output_type"), getattr(sm, "output_unit_type"))
|
||||
|
||||
print("{s:{c}^{n}}".format(s=title, n=50, c="="))
|
||||
if is_image_stage:
|
||||
_print_image_stage_metrics(selected_percentiles, sm)
|
||||
return
|
||||
|
||||
if is_video_stage:
|
||||
return
|
||||
|
||||
_print_stage_timing(sm, selected_percentiles)
|
||||
if is_text_stage:
|
||||
_print_text_stage_metrics(task_type, selected_percentile_metrics, selected_percentiles, sm)
|
||||
elif is_audio_stage:
|
||||
_print_audio_stage_metrics(
|
||||
selected_percentile_metrics,
|
||||
selected_percentiles,
|
||||
sm,
|
||||
)
|
||||
elif is_internal_stream_stage:
|
||||
_print_internal_stream_stage_metrics(selected_percentile_metrics, selected_percentiles, sm)
|
||||
|
||||
|
||||
def _build_stage_metrics_from_outputs(
|
||||
outputs: list[RequestFuncOutput],
|
||||
) -> list[StageBenchmarkMetrics]:
|
||||
"""Aggregate per ``stage_id`` using ``stage_metrics`` snapshots from the client."""
|
||||
buckets: dict[str, list[tuple[RequestFuncOutput, dict]]] = defaultdict(list)
|
||||
for out in outputs:
|
||||
if not getattr(out, "success", False):
|
||||
continue
|
||||
smap = getattr(out, "stage_metrics", None) or {}
|
||||
if not isinstance(smap, dict) or not smap:
|
||||
continue
|
||||
for sid, info in smap.items():
|
||||
if isinstance(info, dict):
|
||||
buckets[str(sid)].append((out, info))
|
||||
|
||||
result: list[StageBenchmarkMetrics] = []
|
||||
for sid in sorted(buckets.keys(), key=lambda x: int(x)):
|
||||
rows = buckets[sid]
|
||||
stage_id_int = int(sid)
|
||||
stage_name = str((rows[0][1] or {}).get("stage_name") or f"stage_{stage_id_int}")
|
||||
final_output_type = str((rows[0][1] or {}).get("final_output_type") or "unknown")
|
||||
output_unit_types = [
|
||||
str((info or {}).get("output_unit_type") or "") for _, info in rows if (info or {}).get("output_unit_type")
|
||||
]
|
||||
output_unit_type = output_unit_types[0] if output_unit_types else "other"
|
||||
|
||||
is_text_stage, is_audio_stage, _, _, is_internal_stream_stage = _stage_modality_flags(
|
||||
final_output_type, output_unit_type
|
||||
)
|
||||
total_output = 0
|
||||
|
||||
ttfts: list[float] = []
|
||||
tpots: list[float] = []
|
||||
itls: list[float] = []
|
||||
vllm_ttfts: list[float] = []
|
||||
vllm_tpots: list[float] = []
|
||||
vllm_itls: list[float] = []
|
||||
stage_gen_times_ms = [float((info or {}).get(defs.STAGE_GEN_TIME_MS) or 0.0) for _, info in rows]
|
||||
postprocess_times_ms = [float((info or {}).get(defs.POSTPROCESS_TIME_MS) or 0.0) for _, info in rows]
|
||||
output_unit_count = sum(int((info or {}).get(defs.OUTPUT_UNIT_COUNT) or 0) for _, info in rows)
|
||||
inter_output_latencies_ms: list[float] = []
|
||||
serving_time_to_first_outputs_ms: list[float] = []
|
||||
time_per_output_units_ms: list[float] = []
|
||||
if is_text_stage or is_internal_stream_stage:
|
||||
serving_time_to_first_outputs_ms = [
|
||||
float((info or {}).get(defs.SERVING_TIME_TO_FIRST_OUTPUT_MS) or 0.0)
|
||||
for _, info in rows
|
||||
if (info or {}).get(defs.SERVING_TIME_TO_FIRST_OUTPUT_MS) is not None
|
||||
]
|
||||
time_per_output_units_ms = [
|
||||
float((info or {}).get(defs.TIME_PER_OUTPUT_UNIT_MS) or 0.0)
|
||||
for _, info in rows
|
||||
if int((info or {}).get(defs.OUTPUT_UNIT_COUNT) or 0) > 1
|
||||
and (info or {}).get(defs.TIME_PER_OUTPUT_UNIT_MS) is not None
|
||||
]
|
||||
for _, info in rows:
|
||||
values = (info or {}).get(defs.INTER_OUTPUT_LATENCIES_MS)
|
||||
if isinstance(values, list):
|
||||
inter_output_latencies_ms.extend(float(v or 0.0) for v in values)
|
||||
elif (info or {}).get(defs.INTER_OUTPUT_LATENCY_MS) is not None:
|
||||
inter_output_latencies_ms.append(float((info or {}).get(defs.INTER_OUTPUT_LATENCY_MS) or 0.0))
|
||||
|
||||
if is_text_stage:
|
||||
for _, info in rows:
|
||||
ttft_ms = float((info or {}).get(defs.SERVING_TIME_TO_FIRST_OUTPUT_MS) or 0.0)
|
||||
if ttft_ms > 0:
|
||||
ttfts.append(ttft_ms / 1000.0)
|
||||
tpot_ms = float((info or {}).get(defs.TIME_PER_OUTPUT_UNIT_MS) or 0.0)
|
||||
if int((info or {}).get(defs.OUTPUT_UNIT_COUNT) or 0) > 1 and tpot_ms > 0:
|
||||
tpots.append(tpot_ms / 1000.0)
|
||||
values = (info or {}).get(defs.INTER_OUTPUT_LATENCIES_MS)
|
||||
if isinstance(values, list):
|
||||
itls.extend(float(v or 0.0) / 1000.0 for v in values)
|
||||
elif (info or {}).get(defs.INTER_OUTPUT_LATENCY_MS) is not None:
|
||||
itls.append(float((info or {}).get(defs.INTER_OUTPUT_LATENCY_MS) or 0.0) / 1000.0)
|
||||
vllm_ttft_ms = float((info or {}).get(defs.VLLM_TTFT_MS) or 0.0)
|
||||
if vllm_ttft_ms > 0:
|
||||
vllm_ttfts.append(vllm_ttft_ms / 1000.0)
|
||||
vllm_tpot_ms = float((info or {}).get(defs.VLLM_TPOT_MS) or 0.0)
|
||||
if vllm_tpot_ms > 0:
|
||||
vllm_tpots.append(vllm_tpot_ms / 1000.0)
|
||||
vllm_values = (info or {}).get(defs.VLLM_ITLS_MS)
|
||||
if isinstance(vllm_values, list):
|
||||
vllm_itls.extend(float(v or 0.0) / 1000.0 for v in vllm_values)
|
||||
elif (info or {}).get(defs.VLLM_ITL_MS) is not None:
|
||||
vllm_itls.append(float((info or {}).get(defs.VLLM_ITL_MS) or 0.0) / 1000.0)
|
||||
total_output = sum(int((info or {}).get(defs.NUM_TOKENS_OUT) or 0) for _, info in rows)
|
||||
|
||||
audio_ttfps: list[float] = []
|
||||
audio_rtfs: list[float] = []
|
||||
audio_durations: list[float] = []
|
||||
audio_frames: list[int] = []
|
||||
missing_audio_duration_count = 0
|
||||
missing_audio_rtf_count = 0
|
||||
if is_audio_stage:
|
||||
for _, info in rows:
|
||||
audio_ttfp_ms = float((info or {}).get(defs.SERVING_TIME_TO_FIRST_OUTPUT_MS) or 0.0)
|
||||
if audio_ttfp_ms > 0:
|
||||
audio_ttfps.append(audio_ttfp_ms / 1000.0)
|
||||
|
||||
frame_count = int((info or {}).get(defs.AUDIO_FRAMES) or 0)
|
||||
if frame_count <= 0 and (info or {}).get("output_unit_type") == "audio":
|
||||
frame_count = int((info or {}).get(defs.OUTPUT_UNIT_COUNT) or 0)
|
||||
audio_frames.append(frame_count)
|
||||
|
||||
duration_s = float((info or {}).get(f"{defs.AUDIO_DURATION}_s") or 0.0)
|
||||
if duration_s > 0:
|
||||
audio_durations.append(duration_s)
|
||||
else:
|
||||
missing_audio_duration_count += 1
|
||||
|
||||
rtf = float((info or {}).get(defs.AUDIO_RTF) or 0.0)
|
||||
if rtf > 0:
|
||||
audio_rtfs.append(rtf)
|
||||
else:
|
||||
missing_audio_rtf_count += 1
|
||||
|
||||
result.append(
|
||||
StageBenchmarkMetrics(
|
||||
**{
|
||||
"stage_id": stage_id_int,
|
||||
"stage_name": stage_name,
|
||||
"final_output_type": final_output_type,
|
||||
defs.TOTAL_OUTPUT: total_output,
|
||||
defs.TTFTS: ttfts,
|
||||
defs.TPOTS: tpots,
|
||||
defs.ITLS: itls,
|
||||
defs.VLLM_TTFTS: vllm_ttfts,
|
||||
defs.VLLM_TPOTS: vllm_tpots,
|
||||
defs.VLLM_ITLS: vllm_itls,
|
||||
defs.AUDIO_TTFPS: audio_ttfps,
|
||||
defs.AUDIO_RTFS: audio_rtfs,
|
||||
defs.AUDIO_DURATIONS: audio_durations,
|
||||
defs.AUDIO_FRAMES: audio_frames,
|
||||
defs.MISSING_AUDIO_DURATION_COUNT: missing_audio_duration_count,
|
||||
defs.MISSING_AUDIO_RTF_COUNT: missing_audio_rtf_count,
|
||||
defs.STAGE_GEN_TIMES_MS: stage_gen_times_ms,
|
||||
defs.POSTPROCESS_TIMES_MS: postprocess_times_ms,
|
||||
"output_unit_type": output_unit_type,
|
||||
defs.OUTPUT_UNIT_COUNT: output_unit_count,
|
||||
defs.SERVING_TIME_TO_FIRST_OUTPUTS_MS: serving_time_to_first_outputs_ms,
|
||||
defs.TIME_PER_OUTPUT_UNITS_MS: time_per_output_units_ms,
|
||||
defs.INTER_OUTPUT_LATENCIES_MS: inter_output_latencies_ms,
|
||||
}
|
||||
)
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def calculate_metrics(
|
||||
input_requests: list[SampleRequest],
|
||||
outputs: list[RequestFuncOutput],
|
||||
@@ -160,6 +715,7 @@ def calculate_metrics(
|
||||
max_concurrency,
|
||||
request_rate,
|
||||
benchmark_duration,
|
||||
print_stage: bool = False,
|
||||
) -> tuple[BenchmarkMetrics, list[int]]:
|
||||
"""Calculate the metrics for the benchmark.
|
||||
|
||||
@@ -187,6 +743,10 @@ def calculate_metrics(
|
||||
audio_rtfs: list[float] = []
|
||||
audio_duration: list[float] = []
|
||||
audio_frames: list[int] = []
|
||||
image_generation_times_ms: list[float] = []
|
||||
denoise_step_latencies_ms: list[float] = []
|
||||
total_images = 0
|
||||
total_image_pixels = 0
|
||||
audio_underruns: list[float] = []
|
||||
audio_continuity_ok: list[bool] = []
|
||||
input_audio_duration = 0.0
|
||||
@@ -222,6 +782,15 @@ def calculate_metrics(
|
||||
audio_rtfs.append(getattr(outputs[i], defs.AUDIO_RTF, 0.0))
|
||||
audio_duration.append(getattr(outputs[i], defs.AUDIO_DURATION, 0.0))
|
||||
audio_frames.append(getattr(outputs[i], defs.AUDIO_FRAMES, 0.0))
|
||||
image_count = int(getattr(outputs[i], defs.IMAGE_COUNT, 0) or 0)
|
||||
total_images += image_count
|
||||
image_generation_time_ms = float(getattr(outputs[i], defs.IMAGE_GENERATION_TIME_MS, 0.0) or 0.0)
|
||||
if image_generation_time_ms > 0:
|
||||
image_generation_times_ms.append(image_generation_time_ms)
|
||||
total_image_pixels += int(getattr(outputs[i], defs.IMAGE_PIXELS, 0) or 0)
|
||||
denoise_step_latency_ms = float(getattr(outputs[i], defs.DENOISE_STEP_LATENCY_MS, 0.0) or 0.0)
|
||||
if denoise_step_latency_ms > 0:
|
||||
denoise_step_latencies_ms.append(denoise_step_latency_ms)
|
||||
audio_underruns.append(getattr(outputs[i], f"{defs.AUDIO_UNDERRUN}_s", 0.0))
|
||||
audio_continuity_ok.append(bool(getattr(outputs[i], defs.AUDIO_CONTINUITY_OK, True)))
|
||||
e2els.append(outputs[i].latency)
|
||||
@@ -336,26 +905,6 @@ def calculate_metrics(
|
||||
std_ttft_ms=np.std(ttfts or 0) * 1000,
|
||||
median_ttft_ms=np.median(ttfts or 0) * 1000,
|
||||
percentiles_ttft_ms=[(p, np.percentile(ttfts or 0, p) * 1000) for p in selected_percentiles],
|
||||
mean_audio_ttfp_ms=np.mean(audio_ttfps or 0) * 1000,
|
||||
std_audio_ttfp_ms=np.std(audio_ttfps or 0) * 1000,
|
||||
median_audio_ttfp_ms=np.median(audio_ttfps or 0) * 1000,
|
||||
percentiles_audio_ttfp_ms=[(p, np.percentile(audio_ttfps or 0, p) * 1000) for p in selected_percentiles],
|
||||
mean_audio_duration_s=np.mean(audio_duration or 0),
|
||||
std_audio_duration_s=np.std(audio_duration or 0),
|
||||
median_audio_duration_s=np.median(audio_duration or 0),
|
||||
percentiles_audio_duration_s=[(p, np.percentile(audio_duration or 0, p)) for p in selected_percentiles],
|
||||
total_audio_duration_s=sum(audio_duration),
|
||||
total_audio_frames=sum(audio_frames),
|
||||
audio_throughput=sum(audio_duration) / dur_s,
|
||||
mean_audio_rtf=np.mean(audio_rtfs or 0),
|
||||
std_audio_rtf=np.std(audio_rtfs or 0),
|
||||
median_audio_rtf=np.median(audio_rtfs or 0),
|
||||
percentiles_audio_rtf=[(p, np.percentile(audio_rtfs or 0, p)) for p in selected_percentiles],
|
||||
mean_audio_underrun_s=np.mean(audio_underruns or 0),
|
||||
std_audio_underrun_s=np.std(audio_underruns or 0),
|
||||
median_audio_underrun_s=np.median(audio_underruns or 0),
|
||||
percentiles_audio_underrun_s=[(p, np.percentile(audio_underruns or 0, p)) for p in selected_percentiles],
|
||||
audio_continuity_ok_rate=(sum(audio_continuity_ok) / len(audio_continuity_ok)) if audio_continuity_ok else 1.0,
|
||||
mean_tpot_ms=np.mean(tpots or 0) * 1000,
|
||||
std_tpot_ms=np.std(tpots or 0) * 1000,
|
||||
median_tpot_ms=np.median(tpots or 0) * 1000,
|
||||
@@ -371,6 +920,46 @@ def calculate_metrics(
|
||||
max_output_tokens_per_s=max_output_tokens_per_s,
|
||||
max_concurrent_requests=max_concurrent_requests,
|
||||
rtfx=input_audio_duration / dur_s,
|
||||
**{
|
||||
defs.MEAN_AUDIO_TTFP_MS: np.mean(audio_ttfps or 0) * 1000,
|
||||
defs.STD_AUDIO_TTFP_MS: np.std(audio_ttfps or 0) * 1000,
|
||||
defs.MEDIAN_AUDIO_TTFP_MS: np.median(audio_ttfps or 0) * 1000,
|
||||
defs.PERCENTILES_AUDIO_TTFP_MS: [
|
||||
(p, np.percentile(audio_ttfps or 0, p) * 1000) for p in selected_percentiles
|
||||
],
|
||||
defs.MEAN_AUDIO_DURATION_S: np.mean(audio_duration or 0),
|
||||
defs.STD_AUDIO_DURATION_S: np.std(audio_duration or 0),
|
||||
defs.MEDIAN_AUDIO_DURATION_S: np.median(audio_duration or 0),
|
||||
defs.PERCENTILES_AUDIO_DURATION_S: [
|
||||
(p, np.percentile(audio_duration or 0, p)) for p in selected_percentiles
|
||||
],
|
||||
defs.TOTAL_AUDIO_DURATION_S: sum(audio_duration),
|
||||
defs.TOTAL_AUDIO_FRAMES: sum(audio_frames),
|
||||
defs.AUDIO_THROUGHPUT: sum(audio_duration) / dur_s,
|
||||
defs.MEAN_AUDIO_RTF: np.mean(audio_rtfs or 0),
|
||||
defs.STD_AUDIO_RTF: np.std(audio_rtfs or 0),
|
||||
defs.MEDIAN_AUDIO_RTF: np.median(audio_rtfs or 0),
|
||||
defs.PERCENTILES_AUDIO_RTF: [(p, np.percentile(audio_rtfs or 0, p)) for p in selected_percentiles],
|
||||
defs.TOTAL_IMAGES: total_images,
|
||||
defs.IMAGE_THROUGHPUT: total_images / dur_s,
|
||||
defs.AVERAGE_PIXELS_PER_IMAGE: (total_image_pixels / total_images) if total_images > 0 else 0.0,
|
||||
defs.MEAN_DENOISE_STEP_LATENCY_MS: np.mean(denoise_step_latencies_ms or 0),
|
||||
defs.MEAN_IMAGE_GENERATION_MS: np.mean(image_generation_times_ms or 0),
|
||||
defs.STD_IMAGE_GENERATION_MS: np.std(image_generation_times_ms or 0),
|
||||
defs.MEDIAN_IMAGE_GENERATION_MS: np.median(image_generation_times_ms or 0),
|
||||
defs.PERCENTILES_IMAGE_GENERATION_MS: [
|
||||
(p, np.percentile(image_generation_times_ms or 0, p)) for p in selected_percentiles
|
||||
],
|
||||
defs.MEAN_AUDIO_UNDERRUN_S: np.mean(audio_underruns or 0),
|
||||
defs.STD_AUDIO_UNDERRUN_S: np.std(audio_underruns or 0),
|
||||
defs.MEDIAN_AUDIO_UNDERRUN_S: np.median(audio_underruns or 0),
|
||||
defs.PERCENTILES_AUDIO_UNDERRUN_S: [
|
||||
(p, np.percentile(audio_underruns or 0, p)) for p in selected_percentiles
|
||||
],
|
||||
defs.AUDIO_CONTINUITY_OK_RATE: (
|
||||
(sum(audio_continuity_ok) / len(audio_continuity_ok)) if audio_continuity_ok else 1.0
|
||||
),
|
||||
},
|
||||
)
|
||||
print_metrics(
|
||||
task_type,
|
||||
@@ -380,5 +969,8 @@ def calculate_metrics(
|
||||
benchmark_duration,
|
||||
goodput_config_dict,
|
||||
metrics,
|
||||
outputs=outputs,
|
||||
selected_percentiles=selected_percentiles,
|
||||
print_stage=print_stage,
|
||||
)
|
||||
return metrics, actual_output_lens
|
||||
@@ -2,6 +2,7 @@ import asyncio
|
||||
import contextlib
|
||||
import io
|
||||
import json
|
||||
import mimetypes
|
||||
import os
|
||||
import random
|
||||
import ssl
|
||||
@@ -12,7 +13,7 @@ import wave
|
||||
from collections.abc import Iterable
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Literal
|
||||
from typing import Any, Literal
|
||||
|
||||
import aiohttp
|
||||
import pybase64 as base64
|
||||
@@ -48,9 +49,41 @@ from vllm_omni.benchmarks.data_modules.seed_tts_dataset import (
|
||||
)
|
||||
from vllm_omni.benchmarks.data_modules.sound_effect_dataset import SoundEffectDataset
|
||||
from vllm_omni.benchmarks.data_modules.ttsd_dataset import TTSDDataset
|
||||
from vllm_omni.metrics import definitions as defs
|
||||
|
||||
_AUDIO_CONTINUITY_THRESHOLD_ENV = "VLLM_OMNI_BENCH_AUDIO_CONTINUITY_THRESHOLD_S"
|
||||
_DEFAULT_AUDIO_CONTINUITY_THRESHOLD_S = 0.1
|
||||
RETURN_STAGE_METRICS_FIELD = "return_stage_metrics"
|
||||
_IMAGE_STAGE_METRICS_BACKENDS = frozenset({"openai-image-edits-omni"})
|
||||
_PRINT_STAGE = False
|
||||
|
||||
|
||||
def maybe_enable_stage_metrics(extra_body: dict[str, Any] | None, *, enabled: bool) -> dict[str, Any] | None:
|
||||
"""Return extra_body with stage-metric opt-in when benchmark metrics need it."""
|
||||
if not enabled:
|
||||
return extra_body
|
||||
body = dict(extra_body or {})
|
||||
body.setdefault(RETURN_STAGE_METRICS_FIELD, True)
|
||||
return body
|
||||
|
||||
|
||||
def should_request_stage_metrics(args: Any) -> bool:
|
||||
"""Whether this benchmark run needs server-side stage metrics in responses."""
|
||||
if getattr(args, "print_stage", False):
|
||||
return True
|
||||
|
||||
backend = getattr(args, "backend", None)
|
||||
if backend in _IMAGE_STAGE_METRICS_BACKENDS:
|
||||
return True
|
||||
|
||||
extra_body = getattr(args, "extra_body", None) or {}
|
||||
modalities = extra_body.get("modalities") if isinstance(extra_body, dict) else None
|
||||
return backend == "openai-chat-omni" and "image" in (modalities or [])
|
||||
|
||||
|
||||
def set_print_stage(enabled: bool) -> None:
|
||||
"""Set whether this benchmark run prints the stage benchmark section."""
|
||||
global _PRINT_STAGE
|
||||
_PRINT_STAGE = bool(enabled)
|
||||
|
||||
|
||||
def _audio_continuity_threshold_s() -> float:
|
||||
@@ -62,7 +95,7 @@ def _audio_continuity_threshold_s() -> float:
|
||||
"""
|
||||
raw = os.environ.get(_AUDIO_CONTINUITY_THRESHOLD_ENV)
|
||||
if not raw:
|
||||
return _DEFAULT_AUDIO_CONTINUITY_THRESHOLD_S
|
||||
return defs.AUDIO_CONTINUITY_DEFAULT_THRESHOLD_S
|
||||
try:
|
||||
value = float(raw)
|
||||
except ValueError:
|
||||
@@ -70,9 +103,9 @@ def _audio_continuity_threshold_s() -> float:
|
||||
"Invalid %s=%r; using default %.3fs",
|
||||
_AUDIO_CONTINUITY_THRESHOLD_ENV,
|
||||
raw,
|
||||
_DEFAULT_AUDIO_CONTINUITY_THRESHOLD_S,
|
||||
defs.AUDIO_CONTINUITY_DEFAULT_THRESHOLD_S,
|
||||
)
|
||||
return _DEFAULT_AUDIO_CONTINUITY_THRESHOLD_S
|
||||
return defs.AUDIO_CONTINUITY_DEFAULT_THRESHOLD_S
|
||||
return max(value, 0.0)
|
||||
|
||||
|
||||
@@ -352,6 +385,10 @@ class MixRequestFuncOutput(RequestFuncOutput):
|
||||
audio_duration: float = 0.0
|
||||
audio_frames: int = 0
|
||||
audio_rtf: float = 0.0
|
||||
image_count: int = 0
|
||||
image_generation_time_ms: float = 0.0
|
||||
image_pixels: int = 0
|
||||
denoise_step_latency_ms: float = 0.0
|
||||
text_latency: float = 0.0
|
||||
#: Worst-case streaming-audio underrun (wall-clock seconds the player
|
||||
#: would have been starved). Populated by the audio-speech backend; ``0.0``
|
||||
@@ -366,6 +403,200 @@ class MixRequestFuncOutput(RequestFuncOutput):
|
||||
#: Raw PCM s16le mono at 24 kHz for Seed-TTS WER: from ``/v1/audio/speech`` stream or
|
||||
#: resampled export after ``openai-chat-omni`` audio deltas.
|
||||
tts_output_pcm_bytes: bytes | None = None
|
||||
#: Per-stage snapshot from orchestrator ``metrics["stage_metrics"]`` (merged across SSE chunks).
|
||||
stage_metrics: dict[str, dict] | None = None
|
||||
stage_id: int | None = None
|
||||
final_output_type: str | None = None
|
||||
|
||||
|
||||
_IMAGE_EDITS_EXTRA_BODY_FORM_FIELDS = (
|
||||
"negative_prompt",
|
||||
"num_inference_steps",
|
||||
"guidance_scale",
|
||||
"strength",
|
||||
"true_cfg_scale",
|
||||
"seed",
|
||||
"generator_device",
|
||||
"lora",
|
||||
"layers",
|
||||
"resolution",
|
||||
"bot_task",
|
||||
"sys_type",
|
||||
"system_prompt",
|
||||
RETURN_STAGE_METRICS_FIELD,
|
||||
)
|
||||
|
||||
|
||||
def _guess_mime_type(path: str) -> str:
|
||||
mime, _ = mimetypes.guess_type(path)
|
||||
return mime or "application/octet-stream"
|
||||
|
||||
|
||||
def _iter_image_edit_inputs(value: Any) -> Iterable[Any]:
|
||||
"""Yield image references from benchmark multimodal content."""
|
||||
if value is None:
|
||||
return
|
||||
if isinstance(value, list):
|
||||
for item in value:
|
||||
yield from _iter_image_edit_inputs(item)
|
||||
return
|
||||
if not isinstance(value, dict):
|
||||
yield value
|
||||
return
|
||||
|
||||
content_type = value.get("type")
|
||||
if content_type == "image_url":
|
||||
image_url = value.get("image_url")
|
||||
if isinstance(image_url, dict):
|
||||
url = image_url.get("url")
|
||||
if url:
|
||||
yield url
|
||||
elif image_url:
|
||||
yield image_url
|
||||
return
|
||||
|
||||
for key in ("image", "images"):
|
||||
if key in value:
|
||||
yield from _iter_image_edit_inputs(value[key])
|
||||
|
||||
|
||||
def _add_image_edit_input_to_form(form: aiohttp.FormData, image_input: Any) -> None:
|
||||
if isinstance(image_input, dict) and "bytes" in image_input:
|
||||
form.add_field(
|
||||
"image",
|
||||
image_input["bytes"],
|
||||
filename="benchmark.png",
|
||||
content_type="image/png",
|
||||
)
|
||||
return
|
||||
|
||||
if isinstance(image_input, str):
|
||||
if image_input.startswith(("data:image", "http://", "https://")):
|
||||
form.add_field("url", image_input)
|
||||
return
|
||||
local_path = image_input.removeprefix("file://")
|
||||
if os.path.exists(local_path):
|
||||
with open(local_path, "rb") as f:
|
||||
image_bytes = f.read()
|
||||
form.add_field(
|
||||
"image",
|
||||
image_bytes,
|
||||
filename=os.path.basename(local_path),
|
||||
content_type=_guess_mime_type(local_path),
|
||||
)
|
||||
return
|
||||
|
||||
raise ValueError(f"Unsupported image edit input: {type(image_input).__name__}")
|
||||
|
||||
|
||||
def _add_image_edit_extra_body_to_form(form: aiohttp.FormData, extra_body: dict[str, Any]) -> None:
|
||||
for key in _IMAGE_EDITS_EXTRA_BODY_FORM_FIELDS:
|
||||
value = extra_body.get(key)
|
||||
if value is None:
|
||||
continue
|
||||
if isinstance(value, (dict, list)):
|
||||
form.add_field(key, json.dumps(value))
|
||||
else:
|
||||
form.add_field(key, str(value))
|
||||
|
||||
|
||||
def _coerce_positive_int(value: Any) -> int | None:
|
||||
try:
|
||||
coerced = int(value)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
return coerced if coerced > 0 else None
|
||||
|
||||
|
||||
def _extract_output_tokens_from_metrics(metrics: dict[str, Any]) -> int | None:
|
||||
top_level_tokens = _coerce_positive_int(metrics.get(defs.NUM_TOKENS_OUT))
|
||||
if top_level_tokens is not None:
|
||||
return top_level_tokens
|
||||
|
||||
stage_snapshot = metrics.get("stage_metrics")
|
||||
if not isinstance(stage_snapshot, dict):
|
||||
return None
|
||||
|
||||
fallback_tokens: list[int] = []
|
||||
for info in stage_snapshot.values():
|
||||
if not isinstance(info, dict):
|
||||
continue
|
||||
num_tokens_out = _coerce_positive_int(info.get(defs.NUM_TOKENS_OUT))
|
||||
if num_tokens_out is None:
|
||||
continue
|
||||
if info.get("final_output_type") == "text" or info.get("output_unit_type") == "token":
|
||||
return num_tokens_out
|
||||
fallback_tokens.append(num_tokens_out)
|
||||
return max(fallback_tokens, default=None)
|
||||
|
||||
|
||||
def _update_output_stage_metrics_from_payload(
|
||||
output: MixRequestFuncOutput,
|
||||
data: dict[str, Any],
|
||||
*,
|
||||
update_output_tokens: bool = True,
|
||||
) -> None:
|
||||
metrics = data.get("metrics")
|
||||
if not isinstance(metrics, dict):
|
||||
return
|
||||
if update_output_tokens:
|
||||
if (num_tokens_out := _extract_output_tokens_from_metrics(metrics)) is not None:
|
||||
output.output_tokens = num_tokens_out
|
||||
if isinstance(sid := metrics.get("stage_id"), int):
|
||||
output.stage_id = sid
|
||||
if isinstance(final_output_type := metrics.get("final_output_type"), str):
|
||||
output.final_output_type = final_output_type
|
||||
stage_snapshot = metrics.get("stage_metrics")
|
||||
if isinstance(stage_snapshot, dict):
|
||||
if output.stage_metrics is None:
|
||||
output.stage_metrics = {}
|
||||
output.stage_metrics.update(stage_snapshot)
|
||||
|
||||
|
||||
def _image_metrics_from_stage_metrics(metrics: dict[str, Any] | None) -> tuple[int, float, int, float]:
|
||||
if not isinstance(metrics, dict):
|
||||
return 0, 0.0, 0, 0.0
|
||||
stage_snapshot = metrics.get("stage_metrics")
|
||||
if not isinstance(stage_snapshot, dict):
|
||||
return 0, 0.0, 0, 0.0
|
||||
image_count = 0
|
||||
image_generation_ms = 0.0
|
||||
image_pixels = 0
|
||||
denoise_step_latency_ms = 0.0
|
||||
for info in stage_snapshot.values():
|
||||
if not isinstance(info, dict):
|
||||
continue
|
||||
final_output_type = info.get("final_output_type")
|
||||
output_unit_type = info.get("output_unit_type")
|
||||
if final_output_type not in {"image", "images"} and output_unit_type != "image":
|
||||
continue
|
||||
image_count += int(info.get(defs.OUTPUT_UNIT_COUNT) or 0)
|
||||
image_generation_ms += float(info.get(defs.STAGE_GEN_TIME_MS) or 0.0)
|
||||
image_pixels += int(info.get(defs.IMAGE_PIXELS) or 0)
|
||||
denoise_step_latency_ms = max(
|
||||
denoise_step_latency_ms,
|
||||
float(info.get(defs.DENOISE_STEP_LATENCY_MS) or 0.0),
|
||||
)
|
||||
return image_count, image_generation_ms, image_pixels, denoise_step_latency_ms
|
||||
|
||||
|
||||
def _image_generation_ms_from_content(content: Any) -> float:
|
||||
if not isinstance(content, list):
|
||||
return 0.0
|
||||
for item in content:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
stage_durations = item.get("stage_durations")
|
||||
if not isinstance(stage_durations, dict):
|
||||
continue
|
||||
gen_values = [
|
||||
float(value)
|
||||
for key, value in stage_durations.items()
|
||||
if str(key).endswith("_gen_ms") and isinstance(value, (int, float))
|
||||
]
|
||||
if gen_values:
|
||||
return max(gen_values)
|
||||
return 0.0
|
||||
|
||||
|
||||
async def async_request_openai_chat_omni_completions(
|
||||
@@ -448,6 +679,13 @@ async def async_request_openai_chat_omni_completions(
|
||||
output.output_tokens = 0
|
||||
output.error = ""
|
||||
output.success = False
|
||||
output.stage_metrics = {}
|
||||
output.stage_id = None
|
||||
output.final_output_type = None
|
||||
output.image_count = 0
|
||||
output.image_generation_time_ms = 0.0
|
||||
output.image_pixels = 0
|
||||
output.denoise_step_latency_ms = 0.0
|
||||
try:
|
||||
async with session.post(url=api_url, json=payload, headers=headers) as response:
|
||||
if response.status == 200:
|
||||
@@ -519,9 +757,27 @@ async def async_request_openai_chat_omni_completions(
|
||||
logger.warning("Failed to parse wav audio chunk: %s", ex)
|
||||
else:
|
||||
audio_bytes_buffer.extend(audio_bytes)
|
||||
elif modality == "image":
|
||||
output.image_count += 1
|
||||
content_image_ms = _image_generation_ms_from_content(content)
|
||||
if content_image_ms > 0:
|
||||
output.image_generation_time_ms += content_image_ms
|
||||
|
||||
if metrics := data.get("metrics"):
|
||||
output.output_tokens = metrics.get("num_tokens_out", 0)
|
||||
_update_output_stage_metrics_from_payload(output, data)
|
||||
(
|
||||
metrics_image_count,
|
||||
metrics_image_ms,
|
||||
metrics_image_pixels,
|
||||
metrics_denoise_step_ms,
|
||||
) = _image_metrics_from_stage_metrics(data.get("metrics"))
|
||||
if metrics_image_count > output.image_count:
|
||||
output.image_count = metrics_image_count
|
||||
if metrics_image_ms > output.image_generation_time_ms:
|
||||
output.image_generation_time_ms = metrics_image_ms
|
||||
if metrics_image_pixels > output.image_pixels:
|
||||
output.image_pixels = metrics_image_pixels
|
||||
if metrics_denoise_step_ms > output.denoise_step_latency_ms:
|
||||
output.denoise_step_latency_ms = metrics_denoise_step_ms
|
||||
|
||||
if usage := data.get("usage"):
|
||||
if (pt := usage.get("prompt_tokens")) is not None:
|
||||
@@ -610,6 +866,138 @@ async def async_request_openai_chat_omni_completions(
|
||||
return output
|
||||
|
||||
|
||||
async def async_request_openai_image_edits_omni(
|
||||
request_func_input: RequestFuncInput,
|
||||
session: aiohttp.ClientSession,
|
||||
pbar: tqdm | None = None,
|
||||
) -> MixRequestFuncOutput:
|
||||
"""Streaming request to /v1/images/edits for multi-stage image-edit benchmarks."""
|
||||
api_url = request_func_input.api_url
|
||||
_validate_api_url(api_url, "OpenAI Image Edits API", "images/edits")
|
||||
|
||||
extra_body = dict(request_func_input.extra_body or {})
|
||||
model = request_func_input.model_name if request_func_input.model_name else request_func_input.model
|
||||
output = MixRequestFuncOutput()
|
||||
output.prompt_len = request_func_input.prompt_len
|
||||
output.itl = []
|
||||
output.stage_metrics = {}
|
||||
output.output_tokens = 0
|
||||
output.image_count = 0
|
||||
output.image_generation_time_ms = 0.0
|
||||
output.image_pixels = 0
|
||||
output.denoise_step_latency_ms = 0.0
|
||||
|
||||
form = aiohttp.FormData()
|
||||
form.add_field("model", model)
|
||||
form.add_field("prompt", request_func_input.prompt)
|
||||
form.add_field("response_format", "b64_json")
|
||||
form.add_field("output_format", str(extra_body.get("output_format", "png")))
|
||||
form.add_field("stream", "true")
|
||||
|
||||
size = extra_body.get("size")
|
||||
if size is None:
|
||||
width, height = extra_body.get("width"), extra_body.get("height")
|
||||
size = f"{width}x{height}" if width is not None and height is not None else "auto"
|
||||
form.add_field("size", str(size))
|
||||
|
||||
_add_image_edit_extra_body_to_form(form, extra_body)
|
||||
|
||||
try:
|
||||
image_inputs = list(_iter_image_edit_inputs(request_func_input.multi_modal_content))
|
||||
if not image_inputs:
|
||||
raise ValueError(
|
||||
"openai-image-edits-omni requires image multimodal content. "
|
||||
"For synthetic inputs, use --dataset-name random-mm with an image bucket."
|
||||
)
|
||||
for image_input in image_inputs:
|
||||
_add_image_edit_input_to_form(form, image_input)
|
||||
except Exception:
|
||||
output.success = False
|
||||
output.error = traceback.format_exc()
|
||||
if pbar:
|
||||
pbar.update(1)
|
||||
return output
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {os.environ.get('OPENAI_API_KEY')}",
|
||||
}
|
||||
_update_headers_common(headers, request_func_input)
|
||||
|
||||
st = time.perf_counter()
|
||||
output.start_time = st
|
||||
timestamp = st
|
||||
most_recent_text_timestamp = st
|
||||
generated_text = ""
|
||||
try:
|
||||
async with session.post(url=api_url, data=form, headers=headers) as response:
|
||||
if response.status == 200:
|
||||
handler = StreamedResponseHandler()
|
||||
async for chunk_bytes in response.content.iter_any():
|
||||
if not chunk_bytes:
|
||||
continue
|
||||
for message in handler.add_chunk(chunk_bytes):
|
||||
if type(message) is bytes:
|
||||
message = message.decode("utf-8")
|
||||
if message.startswith(":"):
|
||||
continue
|
||||
chunk = message.removeprefix("data: ")
|
||||
if chunk == "[DONE]":
|
||||
continue
|
||||
|
||||
timestamp = time.perf_counter()
|
||||
data = json.loads(chunk)
|
||||
_update_output_stage_metrics_from_payload(
|
||||
output,
|
||||
data,
|
||||
update_output_tokens=(data.get("type") == "ar_delta"),
|
||||
)
|
||||
|
||||
chunk_type = data.get("type")
|
||||
if chunk_type == "ar_delta":
|
||||
if output.ttft == 0.0:
|
||||
output.ttft = timestamp - st
|
||||
else:
|
||||
output.itl.append(timestamp - most_recent_text_timestamp)
|
||||
delta = data.get("delta") or ""
|
||||
generated_text += delta
|
||||
most_recent_text_timestamp = timestamp
|
||||
output.text_latency = timestamp - st
|
||||
elif chunk_type == "image":
|
||||
image_data = data.get("data")
|
||||
output.image_count += len(image_data) if isinstance(image_data, list) else 1
|
||||
content_image_ms = _image_generation_ms_from_content(data.get("data"))
|
||||
if content_image_ms > 0:
|
||||
output.image_generation_time_ms += content_image_ms
|
||||
(
|
||||
metrics_image_count,
|
||||
metrics_image_ms,
|
||||
metrics_image_pixels,
|
||||
metrics_denoise_step_ms,
|
||||
) = _image_metrics_from_stage_metrics(data.get("metrics"))
|
||||
if metrics_image_count > output.image_count:
|
||||
output.image_count = metrics_image_count
|
||||
if metrics_image_ms > output.image_generation_time_ms:
|
||||
output.image_generation_time_ms = metrics_image_ms
|
||||
if metrics_image_pixels > output.image_pixels:
|
||||
output.image_pixels = metrics_image_pixels
|
||||
if metrics_denoise_step_ms > output.denoise_step_latency_ms:
|
||||
output.denoise_step_latency_ms = metrics_denoise_step_ms
|
||||
output.latency = timestamp - st
|
||||
output.generated_text = generated_text
|
||||
output.success = True
|
||||
else:
|
||||
output.error = f"HTTP {response.status}: {await response.text()}"
|
||||
output.success = False
|
||||
except Exception:
|
||||
output.success = False
|
||||
output.error = traceback.format_exc()
|
||||
logger.error(f"ERROR: send image edit request failed, reason is: {output.error}")
|
||||
|
||||
if pbar:
|
||||
pbar.update(1)
|
||||
return output
|
||||
|
||||
|
||||
async def async_request_openai_audio_speech(
|
||||
request_func_input: RequestFuncInput, session: aiohttp.ClientSession, pbar: tqdm | None = None
|
||||
) -> MixRequestFuncOutput:
|
||||
@@ -727,6 +1115,10 @@ ASYNC_REQUEST_FUNCS["openai-audio-speech"] = async_request_openai_audio_speech
|
||||
if "openai-audio-speech" not in OPENAI_COMPATIBLE_BACKENDS:
|
||||
OPENAI_COMPATIBLE_BACKENDS.append("openai-audio-speech")
|
||||
|
||||
ASYNC_REQUEST_FUNCS["openai-image-edits-omni"] = async_request_openai_image_edits_omni
|
||||
if "openai-image-edits-omni" not in OPENAI_COMPATIBLE_BACKENDS:
|
||||
OPENAI_COMPATIBLE_BACKENDS.append("openai-image-edits-omni")
|
||||
|
||||
# Daily-Omni backend for audio-visual reasoning benchmark
|
||||
# Reuses openai-chat-omni completions for video+text understanding
|
||||
ASYNC_REQUEST_FUNCS["daily-omni"] = async_request_openai_chat_omni_completions
|
||||
@@ -739,7 +1131,10 @@ from vllm.benchmarks import serve
|
||||
from vllm.benchmarks.lib.ready_checker import wait_for_endpoint
|
||||
from vllm.benchmarks.serve import TaskType, calculate_metrics_for_embeddings, get_request
|
||||
|
||||
from vllm_omni.benchmarks.metrics.metrics import MultiModalsBenchmarkMetrics, calculate_metrics
|
||||
from vllm_omni.benchmarks.metrics.metrics import (
|
||||
MultiModalsBenchmarkMetrics,
|
||||
calculate_metrics,
|
||||
)
|
||||
|
||||
# ruff: noqa: E402
|
||||
|
||||
@@ -996,6 +1391,7 @@ async def benchmark(
|
||||
max_concurrency=max_concurrency,
|
||||
request_rate=request_rate,
|
||||
benchmark_duration=benchmark_duration,
|
||||
print_stage=_PRINT_STAGE,
|
||||
)
|
||||
else:
|
||||
metrics = calculate_metrics_for_embeddings(
|
||||
@@ -1016,9 +1412,13 @@ async def benchmark(
|
||||
"request_goodput": metrics.request_goodput if goodput_config_dict else None,
|
||||
"output_throughput": metrics.output_throughput,
|
||||
"total_token_throughput": metrics.total_token_throughput,
|
||||
"total_audio_duration_s": metrics.total_audio_duration_s,
|
||||
"total_audio_frames": metrics.total_audio_frames,
|
||||
"audio_throughput": metrics.audio_throughput,
|
||||
defs.TOTAL_AUDIO_DURATION_S: getattr(metrics, defs.TOTAL_AUDIO_DURATION_S),
|
||||
defs.TOTAL_AUDIO_FRAMES: getattr(metrics, defs.TOTAL_AUDIO_FRAMES),
|
||||
defs.AUDIO_THROUGHPUT: getattr(metrics, defs.AUDIO_THROUGHPUT),
|
||||
defs.TOTAL_IMAGES: getattr(metrics, defs.TOTAL_IMAGES),
|
||||
defs.IMAGE_THROUGHPUT: getattr(metrics, defs.IMAGE_THROUGHPUT),
|
||||
defs.AVERAGE_PIXELS_PER_IMAGE: getattr(metrics, defs.AVERAGE_PIXELS_PER_IMAGE),
|
||||
defs.MEAN_DENOISE_STEP_LATENCY_MS: getattr(metrics, defs.MEAN_DENOISE_STEP_LATENCY_MS),
|
||||
"input_lens": [output.prompt_len for output in outputs],
|
||||
"output_lens": actual_output_lens,
|
||||
"ttfts": [output.ttft for output in outputs],
|
||||
@@ -1074,21 +1474,34 @@ async def benchmark(
|
||||
if rps_change_events:
|
||||
result["rps_change_events"] = rps_change_events
|
||||
|
||||
result_percentile_metrics: list[str] = []
|
||||
if "ttft" in selected_percentile_metrics:
|
||||
result_percentile_metrics.append("ttft")
|
||||
if "tpot" in selected_percentile_metrics or "tpop" in selected_percentile_metrics:
|
||||
result_percentile_metrics.append("tpot")
|
||||
if "itl" in selected_percentile_metrics:
|
||||
result_percentile_metrics.append("itl")
|
||||
if "e2el" in selected_percentile_metrics:
|
||||
result_percentile_metrics.append("e2el")
|
||||
for metric in selected_percentile_metrics:
|
||||
if metric.startswith("audio") and metric not in result_percentile_metrics:
|
||||
result_percentile_metrics.append(metric)
|
||||
|
||||
def process_one_metric(
|
||||
# E.g., "ttft"
|
||||
metric_attribute_name: str,
|
||||
):
|
||||
# This function prints and adds statistics of the specified
|
||||
# metric.
|
||||
if metric_attribute_name not in selected_percentile_metrics:
|
||||
if metric_attribute_name not in result_percentile_metrics:
|
||||
return
|
||||
# No text tokens generated (e.g. pure TTS speech endpoint): per-token
|
||||
# latency metrics (ttft/tpot/itl) are undefined, so skip them.
|
||||
is_text_token_metric = not (metric_attribute_name == "e2el" or metric_attribute_name.startswith("audio"))
|
||||
if is_text_token_metric and getattr(metrics, "total_output", 0) == 0:
|
||||
return
|
||||
is_audio_rtf = metric_attribute_name == "audio_rtf"
|
||||
is_audio_duration_or_underrun = metric_attribute_name in ("audio_duration", "audio_underrun")
|
||||
is_audio_rtf = metric_attribute_name == defs.AUDIO_RTF
|
||||
is_audio_duration_or_underrun = metric_attribute_name in (defs.AUDIO_DURATION, defs.AUDIO_UNDERRUN)
|
||||
|
||||
suffix = "_ms"
|
||||
if is_audio_duration_or_underrun:
|
||||
@@ -1107,9 +1520,10 @@ async def benchmark(
|
||||
result[f"p{p_word}_{metric_attribute_name}{suffix}"] = value
|
||||
|
||||
if task_type == TaskType.GENERATION:
|
||||
for metric in selected_percentile_metrics:
|
||||
for metric in result_percentile_metrics:
|
||||
process_one_metric(metric)
|
||||
else:
|
||||
result_percentile_metrics.append("e2el")
|
||||
process_one_metric("e2el")
|
||||
|
||||
if profile:
|
||||
|
||||
@@ -9,6 +9,11 @@ from vllm.benchmarks.serve import main_async
|
||||
# This monkey-patches vllm.benchmarks.datasets.get_samples before it's used
|
||||
# Must be imported before any vllm.benchmarks module usage
|
||||
import vllm_omni.benchmarks.patch.patch # noqa: F401
|
||||
from vllm_omni.benchmarks.patch.patch import (
|
||||
maybe_enable_stage_metrics,
|
||||
set_print_stage,
|
||||
should_request_stage_metrics,
|
||||
)
|
||||
|
||||
|
||||
def main(args: argparse.Namespace) -> dict[str, Any]:
|
||||
@@ -18,4 +23,9 @@ def main(args: argparse.Namespace) -> dict[str, Any]:
|
||||
os.environ["SEED_TTS_WER_SAVE_ITEMS"] = "1"
|
||||
if getattr(args, "daily_omni_save_eval_items", False):
|
||||
os.environ["DAILY_OMNI_SAVE_EVAL_ITEMS"] = "1"
|
||||
set_print_stage(getattr(args, "print_stage", False))
|
||||
args.extra_body = maybe_enable_stage_metrics(
|
||||
getattr(args, "extra_body", None),
|
||||
enabled=should_request_stage_metrics(args),
|
||||
)
|
||||
return asyncio.run(main_async(args))
|
||||
@@ -31,12 +31,8 @@ stages:
|
||||
enable_prefix_caching: false
|
||||
devices: "0"
|
||||
default_sampling_params:
|
||||
temperature: 0.4
|
||||
top_p: 0.9
|
||||
top_k: 1
|
||||
temperature: 0.0
|
||||
max_tokens: 2048
|
||||
seed: 42
|
||||
repetition_penalty: 1.05
|
||||
|
||||
- stage_id: 1
|
||||
max_num_batched_tokens: 32768
|
||||
|
||||
@@ -52,6 +52,7 @@ class InlineStageDiffusionClient(StageClientBase):
|
||||
self.replica_id = metadata.replica_id
|
||||
self.final_output = metadata.final_output
|
||||
self.final_output_type = metadata.final_output_type
|
||||
self.model_stage = getattr(metadata, "model_stage", None)
|
||||
self.default_sampling_params = metadata.default_sampling_params
|
||||
self.requires_multimodal_data = metadata.requires_multimodal_data
|
||||
self.custom_process_input_func = metadata.custom_process_input_func
|
||||
|
||||
@@ -777,7 +777,9 @@ class BagelPipeline(nn.Module, SupportsComponentDiscovery, DiffusionPipelineProf
|
||||
if torch.is_tensor(v):
|
||||
generation_input[k] = v.to(self.device)
|
||||
|
||||
self._regen_init_noise_on_device(generation_input, req.sampling_params.seed)
|
||||
# NOTE: For now we disable device specific noise regeneration so that e2e tests can run
|
||||
# on both CUDA and ROCm. Context: https://github.com/vllm-project/vllm-omni/pull/4081
|
||||
# self._regen_init_noise_on_device(generation_input, req.sampling_params.seed)
|
||||
|
||||
# text cfg
|
||||
generation_input_cfg_text = self.bagel.prepare_vae_latent_cfg(
|
||||
|
||||
@@ -865,7 +865,9 @@ class HunyuanImage3Pipeline(
|
||||
output, sections = out["output"], out["sections"]
|
||||
|
||||
# 4. Encode conditional images
|
||||
if batch_cond_image_info is not None and len(batch_cond_image_info[0]) > 0:
|
||||
# Skip encoding if AR KV reuse is enabled
|
||||
has_ar_kv = kwargs.get("ar_kv_data")
|
||||
if batch_cond_image_info is not None and len(batch_cond_image_info[0]) > 0 and not has_ar_kv:
|
||||
cond_vae_images, cond_timestep, cond_vit_images = self._encode_cond_image(
|
||||
batch_cond_image_info, cfg_factor[mode]
|
||||
)
|
||||
@@ -1459,6 +1461,7 @@ class HunyuanImage3Pipeline(
|
||||
num_inference_steps=num_inference_steps,
|
||||
guidance_scale=guidance_scale,
|
||||
batch_cond_image_info=batch_cond_image_info,
|
||||
**ar_kv_kwargs,
|
||||
)
|
||||
|
||||
model_inputs.update(ar_kv_kwargs)
|
||||
|
||||
@@ -119,6 +119,7 @@ class StageDiffusionClient(StageClientBase):
|
||||
self.replica_id = metadata.replica_id
|
||||
self.final_output = metadata.final_output
|
||||
self.final_output_type = metadata.final_output_type
|
||||
self.model_stage = getattr(metadata, "model_stage", None)
|
||||
self.default_sampling_params = metadata.default_sampling_params
|
||||
self.requires_multimodal_data = getattr(metadata, "requires_multimodal_data", False)
|
||||
self.custom_process_input_func = getattr(metadata, "custom_process_input_func", None)
|
||||
|
||||
@@ -123,6 +123,7 @@ class StageRuntimeInfo:
|
||||
final_output: bool
|
||||
final_output_type: FinalOutputModalityType | None
|
||||
stage_type: str
|
||||
model_stage: str | None = None
|
||||
|
||||
|
||||
# ============================================================================
|
||||
@@ -1247,6 +1248,7 @@ class AsyncOmniEngine:
|
||||
final_output=first_client.final_output,
|
||||
final_output_type=first_client.final_output_type,
|
||||
stage_type=first_client.stage_type,
|
||||
model_stage=getattr(first_client, "model_stage", None),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1616,6 +1618,7 @@ class AsyncOmniEngine:
|
||||
prompt_text: str | None = None,
|
||||
sampling_params_list: Sequence[Any] | None = None,
|
||||
final_stage_id: int = 0,
|
||||
final_output_stage_ids: Sequence[int] | None = None,
|
||||
arrival_time: float | None = None,
|
||||
lora_request: Any = None,
|
||||
tokenization_kwargs: dict[str, Any] | None = None,
|
||||
@@ -1628,6 +1631,7 @@ class AsyncOmniEngine:
|
||||
message_type: Literal["add_request", "streaming_update"] = "add_request",
|
||||
) -> StageSubmissionMessage:
|
||||
"""Build an add_request message after stage-0 preprocessing."""
|
||||
request_timestamp = float(arrival_time) if arrival_time is not None else time.time()
|
||||
effective_sampling_params_list: list[OmniSamplingParams] = (
|
||||
list(cast(Sequence[OmniSamplingParams], sampling_params_list))
|
||||
if sampling_params_list is not None
|
||||
@@ -1713,7 +1717,9 @@ class AsyncOmniEngine:
|
||||
output_prompt_text=output_prompt_text,
|
||||
sampling_params_list=effective_sampling_params_list,
|
||||
final_stage_id=final_stage_id,
|
||||
final_output_stage_ids=list(final_output_stage_ids) if final_output_stage_ids is not None else None,
|
||||
preprocess_ms=_preprocess_ms,
|
||||
request_timestamp=request_timestamp,
|
||||
enqueue_ts=time.perf_counter(),
|
||||
)
|
||||
|
||||
@@ -2186,6 +2192,7 @@ class AsyncOmniEngine:
|
||||
prompt_text: str | None = None,
|
||||
sampling_params_list: Sequence[Any] | None = None,
|
||||
final_stage_id: int = 0,
|
||||
final_output_stage_ids: Sequence[int] | None = None,
|
||||
arrival_time: float | None = None,
|
||||
lora_request: Any = None,
|
||||
tokenization_kwargs: dict[str, Any] | None = None,
|
||||
@@ -2209,6 +2216,7 @@ class AsyncOmniEngine:
|
||||
prompt_text=prompt_text,
|
||||
sampling_params_list=sampling_params_list,
|
||||
final_stage_id=final_stage_id,
|
||||
final_output_stage_ids=final_output_stage_ids,
|
||||
arrival_time=arrival_time,
|
||||
lora_request=lora_request,
|
||||
tokenization_kwargs=tokenization_kwargs,
|
||||
@@ -2236,6 +2244,7 @@ class AsyncOmniEngine:
|
||||
prompt_text: str | None = None,
|
||||
sampling_params_list: Sequence[Any] | None = None,
|
||||
final_stage_id: int = 0,
|
||||
final_output_stage_ids: Sequence[int] | None = None,
|
||||
arrival_time: float | None = None,
|
||||
lora_request: Any = None,
|
||||
tokenization_kwargs: dict[str, Any] | None = None,
|
||||
@@ -2253,6 +2262,7 @@ class AsyncOmniEngine:
|
||||
prompt_text=prompt_text,
|
||||
sampling_params_list=sampling_params_list,
|
||||
final_stage_id=final_stage_id,
|
||||
final_output_stage_ids=final_output_stage_ids,
|
||||
arrival_time=arrival_time,
|
||||
lora_request=lora_request,
|
||||
tokenization_kwargs=tokenization_kwargs,
|
||||
@@ -2270,6 +2280,7 @@ class AsyncOmniEngine:
|
||||
prompt_text: str | None = None,
|
||||
sampling_params_list: Sequence[Any] | None = None,
|
||||
final_stage_id: int = 0,
|
||||
final_output_stage_ids: Sequence[int] | None = None,
|
||||
arrival_time: float | None = None,
|
||||
*,
|
||||
resumable: bool = True,
|
||||
@@ -2281,6 +2292,7 @@ class AsyncOmniEngine:
|
||||
prompt_text=prompt_text,
|
||||
sampling_params_list=sampling_params_list,
|
||||
final_stage_id=final_stage_id,
|
||||
final_output_stage_ids=final_output_stage_ids,
|
||||
arrival_time=arrival_time,
|
||||
resumable=resumable,
|
||||
message_type="streaming_update",
|
||||
@@ -2294,6 +2306,7 @@ class AsyncOmniEngine:
|
||||
prompt_text: str | None = None,
|
||||
sampling_params_list: Sequence[Any] | None = None,
|
||||
final_stage_id: int = 0,
|
||||
final_output_stage_ids: Sequence[int] | None = None,
|
||||
arrival_time: float | None = None,
|
||||
*,
|
||||
resumable: bool = True,
|
||||
@@ -2305,6 +2318,7 @@ class AsyncOmniEngine:
|
||||
prompt_text=prompt_text,
|
||||
sampling_params_list=sampling_params_list,
|
||||
final_stage_id=final_stage_id,
|
||||
final_output_stage_ids=final_output_stage_ids,
|
||||
arrival_time=arrival_time,
|
||||
resumable=resumable,
|
||||
)
|
||||
|
||||
@@ -24,7 +24,9 @@ class StageSubmissionMessage(EngineQueueMessage, kw_only=True):
|
||||
sampling_params_list: list[OmniSamplingParams]
|
||||
final_stage_id: int
|
||||
preprocess_ms: float
|
||||
request_timestamp: float
|
||||
enqueue_ts: float
|
||||
final_output_stage_ids: list[int] | None = None
|
||||
|
||||
|
||||
class AddCompanionRequestMessage(EngineQueueMessage, kw_only=True):
|
||||
|
||||
@@ -126,6 +126,11 @@ class OrchestratorRequestState:
|
||||
prompt: Any = None
|
||||
sampling_params_list: list[Any] = field(default_factory=list)
|
||||
final_stage_id: int = -1
|
||||
final_output_stage_ids: set[int] = field(default_factory=set)
|
||||
finished_final_output_stage_ids: set[int] = field(default_factory=set)
|
||||
|
||||
# Wall-clock timestamp when the client-facing engine request was accepted.
|
||||
request_timestamp: float = 0.0
|
||||
|
||||
# Metrics: timestamp when request was submitted to each stage.
|
||||
stage_submit_ts: dict[int, float] = field(default_factory=dict)
|
||||
@@ -451,6 +456,7 @@ class Orchestrator:
|
||||
if not sampling_params_list:
|
||||
raise ValueError(f"Missing sampling params for stage 0. Got {len(sampling_params_list)} stage params.")
|
||||
final_stage_id = msg.final_stage_id
|
||||
final_output_stage_ids = set(msg.final_output_stage_ids or [final_stage_id])
|
||||
|
||||
logger.debug(
|
||||
"[Orchestrator] _handle_add_request: stage=%s req=%s "
|
||||
@@ -469,6 +475,8 @@ class Orchestrator:
|
||||
prompt=original_prompt,
|
||||
sampling_params_list=sampling_params_list,
|
||||
final_stage_id=final_stage_id,
|
||||
final_output_stage_ids=final_output_stage_ids,
|
||||
request_timestamp=float(msg.request_timestamp or _time.time()),
|
||||
mm_features=getattr(prompt, "mm_features", None),
|
||||
)
|
||||
self.request_states[request_id] = req_state
|
||||
@@ -512,7 +520,9 @@ class Orchestrator:
|
||||
output_prompt_text=msg.output_prompt_text,
|
||||
sampling_params_list=msg.sampling_params_list,
|
||||
final_stage_id=msg.final_stage_id,
|
||||
final_output_stage_ids=msg.final_output_stage_ids,
|
||||
preprocess_ms=msg.preprocess_ms,
|
||||
request_timestamp=msg.request_timestamp,
|
||||
enqueue_ts=msg.enqueue_ts,
|
||||
)
|
||||
await self._handle_add_request(fallback_msg)
|
||||
@@ -558,6 +568,8 @@ class Orchestrator:
|
||||
prompt=companion_prompt,
|
||||
sampling_params_list=sampling_params_list,
|
||||
final_stage_id=0,
|
||||
final_output_stage_ids={0},
|
||||
request_timestamp=parent_state.request_timestamp,
|
||||
)
|
||||
self.request_states[companion_id] = companion_state
|
||||
companion_state.stage_submit_ts[0] = _time.time()
|
||||
@@ -665,6 +677,7 @@ class Orchestrator:
|
||||
if output is None:
|
||||
continue
|
||||
|
||||
pool.record_output_timestamps([output])
|
||||
await self._handle_processed_outputs(stage_id, replica_id, [output])
|
||||
idle = False
|
||||
else:
|
||||
@@ -775,7 +788,9 @@ class Orchestrator:
|
||||
stage_metrics = pool.build_stage_metrics(
|
||||
[output],
|
||||
submit_ts=req_state.stage_submit_ts.get(stage_id, _time.time()),
|
||||
request_timestamp=req_state.request_timestamp,
|
||||
replica_id=replica_id,
|
||||
sampling_params=req_state.sampling_params_list[stage_id],
|
||||
)
|
||||
stage_metrics.pipeline_timings = dict(req_state.pipeline_timings)
|
||||
|
||||
@@ -852,6 +867,12 @@ class Orchestrator:
|
||||
await self._cleanup_request_ids([req_id])
|
||||
return
|
||||
|
||||
request_finished = False
|
||||
if finished and self.stage_pools[stage_id].final_output:
|
||||
req_state.finished_final_output_stage_ids.add(stage_id)
|
||||
final_output_stage_ids = req_state.final_output_stage_ids or {req_state.final_stage_id}
|
||||
request_finished = final_output_stage_ids.issubset(req_state.finished_final_output_stage_ids)
|
||||
|
||||
if self.stage_pools[stage_id].final_output:
|
||||
await self.output_async_queue.put(
|
||||
OutputMessage(
|
||||
@@ -860,7 +881,7 @@ class Orchestrator:
|
||||
replica_id=replica_id,
|
||||
engine_outputs=output,
|
||||
metrics=stage_metrics,
|
||||
finished=finished and stage_id == req_state.final_stage_id,
|
||||
finished=request_finished,
|
||||
stage_submit_ts=submit_ts,
|
||||
)
|
||||
)
|
||||
@@ -915,7 +936,7 @@ class Orchestrator:
|
||||
is_final_update=True,
|
||||
)
|
||||
|
||||
if finished and stage_id == req_state.final_stage_id:
|
||||
if request_finished:
|
||||
await self._cleanup_request_ids([req_id, *self._cfg_tracker.cleanup_parent(req_id)])
|
||||
|
||||
def _next_stage_already_submitted(self, stage_id: int, req_state: OrchestratorRequestState) -> bool:
|
||||
@@ -1341,7 +1362,12 @@ class Orchestrator:
|
||||
resumable=downstream_resumable,
|
||||
)
|
||||
request.external_req_id = request.request_id
|
||||
await next_pool.submit_initial(request_id, req_state, request, prompt_text=None)
|
||||
await next_pool.submit_initial(
|
||||
request_id,
|
||||
req_state,
|
||||
request,
|
||||
prompt_text=None,
|
||||
)
|
||||
|
||||
# async_chunk pre-submit fires per stage edge (N-1 -> N). Source
|
||||
# replica is stage 0's bound replica (single-replica thinker in
|
||||
|
||||
@@ -13,7 +13,7 @@ from vllm.v1.engine.output_processor import (
|
||||
RequestState,
|
||||
)
|
||||
from vllm.v1.engine.parallel_sampling import ParentRequest
|
||||
from vllm.v1.metrics.stats import IterationStats
|
||||
from vllm.v1.metrics.stats import IterationStats, RequestStateStats
|
||||
|
||||
from vllm_omni.data_entry_keys import unflatten_payload
|
||||
from vllm_omni.engine.output_modality import DRAINABLE_MODALITIES
|
||||
@@ -35,13 +35,21 @@ class OmniRequestState(RequestState):
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
arrival_time = kwargs.get("arrival_time")
|
||||
if arrival_time is None and len(args) > 12:
|
||||
arrival_time = args[12]
|
||||
super().__init__(*args, **kwargs)
|
||||
self.native_text_stats = RequestStateStats(arrival_time=float(arrival_time or 0.0))
|
||||
# Omni-specific: multimodal output accumulation
|
||||
# NOTE: Keys in mm_accumulated matter, because they dictate which
|
||||
# outputs are drained (i.e., only drain modality keys, don't drain
|
||||
# hidden states).
|
||||
self.mm_accumulated: dict[str, Any] = {}
|
||||
|
||||
def apply_streaming_update(self, update) -> None:
|
||||
super().apply_streaming_update(update)
|
||||
self.native_text_stats.arrival_time = update.arrival_time
|
||||
|
||||
@staticmethod
|
||||
def _to_cpu(x):
|
||||
"""Try to convert to CPU tensor if needed."""
|
||||
@@ -318,6 +326,32 @@ class MultimodalOutputProcessor(VLLMOutputProcessor):
|
||||
tracing_enabled=tracing_enabled,
|
||||
)
|
||||
self.engine_core_output_type = engine_core_output_type
|
||||
self._native_text_metrics_by_request: dict[str, dict[str, Any]] = {}
|
||||
|
||||
def _native_text_metric_record(self, request_id: str) -> dict[str, Any]:
|
||||
return self._native_text_metrics_by_request.setdefault(
|
||||
request_id,
|
||||
{
|
||||
"vllm_ttft_ms": 0.0,
|
||||
"vllm_tpot_ms": 0.0,
|
||||
"vllm_itl_ms": 0.0,
|
||||
"vllm_itls_ms": [],
|
||||
},
|
||||
)
|
||||
|
||||
def pop_native_text_metrics(self, request_id: str) -> dict[str, Any]:
|
||||
return self._native_text_metrics_by_request.pop(request_id, {})
|
||||
|
||||
def abort_requests(self, request_ids, internal: bool) -> list[str]:
|
||||
request_ids = list(request_ids)
|
||||
for request_id in request_ids:
|
||||
if internal:
|
||||
req_state = self.request_states.get(request_id)
|
||||
if req_state is not None:
|
||||
self._native_text_metrics_by_request.pop(req_state.external_req_id, None)
|
||||
else:
|
||||
self._native_text_metrics_by_request.pop(request_id, None)
|
||||
return super().abort_requests(request_ids, internal)
|
||||
|
||||
def add_request(
|
||||
self,
|
||||
@@ -371,6 +405,7 @@ class MultimodalOutputProcessor(VLLMOutputProcessor):
|
||||
|
||||
external_req_id = getattr(req_state, "external_req_id", None)
|
||||
if external_req_id is not None:
|
||||
self._native_text_metrics_by_request.pop(external_req_id, None)
|
||||
request_ids = self.external_req_ids.get(external_req_id)
|
||||
if request_ids is not None:
|
||||
self.external_req_ids[external_req_id] = [rid for rid in request_ids if rid != request_id]
|
||||
@@ -402,3 +437,73 @@ class MultimodalOutputProcessor(VLLMOutputProcessor):
|
||||
engine_core_timestamp=engine_core_timestamp,
|
||||
iteration_stats=iteration_stats,
|
||||
)
|
||||
|
||||
def _update_stats_from_output(
|
||||
self,
|
||||
req_state: RequestState,
|
||||
engine_core_output: EngineCoreOutput,
|
||||
engine_core_timestamp: float | None,
|
||||
iteration_stats: IterationStats | None,
|
||||
):
|
||||
was_prefilling = req_state.is_prefilling
|
||||
native_stats = req_state.native_text_stats if isinstance(req_state, OmniRequestState) else None
|
||||
previous_last_token_ts = native_stats.last_token_ts if native_stats is not None else 0.0
|
||||
|
||||
super()._update_stats_from_output(
|
||||
req_state,
|
||||
engine_core_output,
|
||||
engine_core_timestamp,
|
||||
None,
|
||||
)
|
||||
|
||||
if iteration_stats is None or engine_core_timestamp is None or native_stats is None:
|
||||
return
|
||||
|
||||
iteration_stats.update_from_output(
|
||||
engine_core_output,
|
||||
engine_core_timestamp,
|
||||
was_prefilling,
|
||||
native_stats,
|
||||
self.lora_states,
|
||||
req_state.lora_name,
|
||||
)
|
||||
record = self._native_text_metric_record(req_state.external_req_id)
|
||||
if was_prefilling:
|
||||
record["vllm_ttft_ms"] = max(float(native_stats.first_token_latency) * 1000.0, 0.0)
|
||||
return
|
||||
|
||||
if previous_last_token_ts > 0:
|
||||
itl_ms = max((engine_core_timestamp - previous_last_token_ts) * 1000.0, 0.0)
|
||||
itls_ms = record.setdefault("vllm_itls_ms", [])
|
||||
itls_ms.append(itl_ms)
|
||||
record["vllm_itl_ms"] = sum(itls_ms) / float(len(itls_ms))
|
||||
|
||||
def _update_stats_from_finished(
|
||||
self,
|
||||
req_state: RequestState,
|
||||
finish_reason: FinishReason | None,
|
||||
iteration_stats: IterationStats | None,
|
||||
):
|
||||
super()._update_stats_from_finished(req_state, finish_reason, None)
|
||||
|
||||
native_stats = req_state.native_text_stats if isinstance(req_state, OmniRequestState) else None
|
||||
if iteration_stats is None or native_stats is None or finish_reason is None:
|
||||
return
|
||||
|
||||
iteration_stats.update_from_finished_request(
|
||||
finish_reason=finish_reason,
|
||||
request_id=req_state.external_req_id,
|
||||
num_prompt_tokens=req_state.prompt_len,
|
||||
max_tokens_param=req_state.max_tokens_param,
|
||||
req_stats=native_stats,
|
||||
num_cached_tokens=req_state.num_cached_tokens,
|
||||
)
|
||||
if not iteration_stats.finished_requests:
|
||||
return
|
||||
|
||||
finished_request = iteration_stats.finished_requests[-1]
|
||||
if finished_request.request_id != req_state.external_req_id:
|
||||
return
|
||||
self._native_text_metric_record(req_state.external_req_id)["vllm_tpot_ms"] = (
|
||||
float(finished_request.mean_time_per_output_token) * 1000.0
|
||||
)
|
||||
@@ -428,6 +428,8 @@ def extract_stage_metadata(stage_config: Any) -> StageMetadata:
|
||||
_mod, _fn = _ckf_path.rsplit(".", 1)
|
||||
cfg_kv_collect_func = getattr(importlib.import_module(_mod), _fn)
|
||||
|
||||
model_stage = getattr(engine_args, "model_stage", None)
|
||||
|
||||
if stage_type == "diffusion":
|
||||
return StageMetadata(
|
||||
stage_id=stage_id,
|
||||
@@ -440,12 +442,11 @@ def extract_stage_metadata(stage_config: Any) -> StageMetadata:
|
||||
final_output_type=final_output_type,
|
||||
default_sampling_params=default_sampling_params,
|
||||
custom_process_input_func=custom_process_input_func,
|
||||
model_stage=None,
|
||||
model_stage=model_stage,
|
||||
runtime_cfg=runtime_cfg,
|
||||
cfg_kv_collect_func=cfg_kv_collect_func,
|
||||
)
|
||||
|
||||
model_stage = getattr(engine_args, "model_stage", None)
|
||||
engine_output_type = getattr(engine_args, "engine_output_type", None)
|
||||
is_comprehension = getattr(stage_config, "is_comprehension", False)
|
||||
requires_multimodal_data = getattr(runtime_cfg, "requires_multimodal_data", False)
|
||||
|
||||
@@ -24,6 +24,7 @@ from vllm_omni.engine.stage_client import (
|
||||
StagePoolLLMClient,
|
||||
)
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
from vllm_omni.metrics import definitions as defs
|
||||
from vllm_omni.metrics.stats import StageRequestStats as StageRequestMetrics
|
||||
from vllm_omni.metrics.stats import StageStats
|
||||
from vllm_omni.metrics.utils import count_tokens_from_outputs
|
||||
@@ -96,6 +97,10 @@ class StagePool:
|
||||
self._next_replica_id = 0
|
||||
self._request_bindings: dict[str, int] = {}
|
||||
self._replica_metrics: list[_ReplicaMetrics] = [_ReplicaMetrics() for _ in self.clients]
|
||||
self._output_timestamps_by_request: dict[str, list[float]] = {}
|
||||
self._non_empty_first_output_timestamps_by_request: dict[str, float] = {}
|
||||
self._audio_frames_by_request: dict[str, int] = {}
|
||||
self._audio_sample_rate_by_request: dict[str, int] = {}
|
||||
|
||||
# Distributed-mode state. Populated by add_client / remove_client.
|
||||
self._addr_to_replica_id: dict[str, int] = {}
|
||||
@@ -428,6 +433,10 @@ class StagePool:
|
||||
"""Drop the route binding for *request_id* in this stage."""
|
||||
self._request_bindings.pop(request_id, None)
|
||||
self._affinity.pop(request_id, None)
|
||||
self._output_timestamps_by_request.pop(str(request_id), None)
|
||||
self._non_empty_first_output_timestamps_by_request.pop(str(request_id), None)
|
||||
self._audio_frames_by_request.pop(str(request_id), None)
|
||||
self._audio_sample_rate_by_request.pop(str(request_id), None)
|
||||
|
||||
def release_bindings(self, request_ids: list[str]) -> None:
|
||||
"""Drop route bindings for the given request ids in this stage."""
|
||||
@@ -485,13 +494,66 @@ class StagePool:
|
||||
request_outputs: list[Any],
|
||||
*,
|
||||
submit_ts: float,
|
||||
request_timestamp: float,
|
||||
replica_id: int,
|
||||
sampling_params: Any | None = None,
|
||||
) -> StageRequestMetrics:
|
||||
"""Build stage metrics for outputs produced on one replica."""
|
||||
now = _time.time()
|
||||
stage_gen_time_ms = (now - submit_ts) * 1000.0
|
||||
|
||||
request_id = str(getattr(request_outputs[0], "request_id", "")) if request_outputs else ""
|
||||
output_timestamps = self._output_timestamps_by_request.pop(request_id, []) if request_id else []
|
||||
non_empty_first_output_ts = (
|
||||
self._non_empty_first_output_timestamps_by_request.pop(request_id, None) if request_id else None
|
||||
)
|
||||
num_tokens_out = count_tokens_from_outputs(request_outputs)
|
||||
output_unit_type = self._infer_output_unit_type(request_outputs, token_count=num_tokens_out)
|
||||
output_unit_count = self._count_output_units(
|
||||
request_outputs,
|
||||
unit_type=output_unit_type,
|
||||
fallback_token_count=num_tokens_out,
|
||||
)
|
||||
native_text_metrics = {}
|
||||
if request_id:
|
||||
pop_native_text_metrics = getattr(self.output_processor, "pop_native_text_metrics", None)
|
||||
if callable(pop_native_text_metrics):
|
||||
native_text_metrics = pop_native_text_metrics(request_id)
|
||||
current_audio_frames, current_audio_sample_rate, _ = self._collect_audio_metrics(request_outputs)
|
||||
accumulated_audio_frames = self._audio_frames_by_request.pop(request_id, 0) if request_id else 0
|
||||
accumulated_audio_sample_rate = self._audio_sample_rate_by_request.pop(request_id, 0) if request_id else 0
|
||||
audio_generated_frames = max(accumulated_audio_frames, current_audio_frames)
|
||||
audio_sample_rate = accumulated_audio_sample_rate or current_audio_sample_rate
|
||||
audio_duration_s = (
|
||||
float(audio_generated_frames) / float(audio_sample_rate)
|
||||
if audio_generated_frames > 0 and audio_sample_rate > 0
|
||||
else 0.0
|
||||
)
|
||||
image_pixels = self._count_image_pixels(request_outputs) if output_unit_type == "image" else 0
|
||||
num_inference_steps = self._coerce_int_scalar(getattr(sampling_params, "num_inference_steps", None))
|
||||
denoise_step_latency_ms = (
|
||||
defs.compute_denoise_step_latency(stage_gen_time_ms, num_inference_steps)
|
||||
if output_unit_type == "image"
|
||||
else 0.0
|
||||
)
|
||||
has_output_timestamps = bool(output_timestamps)
|
||||
first_ts = output_timestamps[0] if has_output_timestamps else now
|
||||
serving_time_to_first_output_ms = (
|
||||
max((non_empty_first_output_ts - request_timestamp) * 1000.0, 0.0)
|
||||
if non_empty_first_output_ts is not None
|
||||
else 0.0
|
||||
)
|
||||
remaining_ms = max((now - first_ts) * 1000.0, 0.0)
|
||||
if output_unit_count > 1 and self._is_streaming_output_unit_type(output_unit_type):
|
||||
time_per_output_unit_ms = remaining_ms / float(output_unit_count - 1)
|
||||
else:
|
||||
time_per_output_unit_ms = 0.0
|
||||
inter_output_latencies_ms = [
|
||||
max((cur_ts - prev_ts) * 1000.0, 0.0) for prev_ts, cur_ts in zip(output_timestamps, output_timestamps[1:])
|
||||
]
|
||||
inter_output_latency_ms = (
|
||||
sum(inter_output_latencies_ms) / float(len(inter_output_latencies_ms)) if inter_output_latencies_ms else 0.0
|
||||
)
|
||||
num_tokens_in = 0
|
||||
if self.stage_id == 0:
|
||||
for ro in request_outputs:
|
||||
@@ -504,6 +566,7 @@ class StagePool:
|
||||
batch_id = metrics.batch_seq
|
||||
metrics.agg_total_tokens += num_tokens_out
|
||||
metrics.agg_total_gen_time_ms += stage_gen_time_ms
|
||||
audio_rtf = defs.compute_audio_rtf(stage_gen_time_ms / 1000.0, audio_duration_s)
|
||||
|
||||
return StageRequestMetrics(
|
||||
num_tokens_in=num_tokens_in,
|
||||
@@ -519,8 +582,321 @@ class StagePool:
|
||||
total_token=metrics.agg_total_tokens,
|
||||
total_gen_time_ms=metrics.agg_total_gen_time_ms,
|
||||
),
|
||||
audio_generated_frames=audio_generated_frames,
|
||||
audio_sample_rate=audio_sample_rate,
|
||||
audio_duration_s=audio_duration_s,
|
||||
audio_rtf=audio_rtf,
|
||||
image_pixels=image_pixels,
|
||||
denoise_step_latency_ms=denoise_step_latency_ms,
|
||||
output_unit_type=output_unit_type,
|
||||
output_unit_count=output_unit_count,
|
||||
serving_time_to_first_output_ms=serving_time_to_first_output_ms,
|
||||
time_per_output_unit_ms=time_per_output_unit_ms,
|
||||
inter_output_latency_ms=inter_output_latency_ms,
|
||||
inter_output_latencies_ms=inter_output_latencies_ms,
|
||||
vllm_ttft_ms=float(native_text_metrics.get("vllm_ttft_ms") or 0.0),
|
||||
vllm_tpot_ms=float(native_text_metrics.get("vllm_tpot_ms") or 0.0),
|
||||
vllm_itl_ms=float(native_text_metrics.get("vllm_itl_ms") or 0.0),
|
||||
vllm_itls_ms=list(native_text_metrics.get("vllm_itls_ms") or []),
|
||||
)
|
||||
|
||||
def _infer_output_unit_type(self, request_outputs: list[Any], *, token_count: int) -> str:
|
||||
final_output_type = getattr(self.stage_client, "final_output_type", None)
|
||||
|
||||
if self._has_image_output(request_outputs) or final_output_type in {"image", "images"}:
|
||||
return "image"
|
||||
if self._has_video_output(request_outputs) or final_output_type in {"video", "videos"}:
|
||||
return "video"
|
||||
if self._has_audio_output(request_outputs) or final_output_type == "audio":
|
||||
return "audio"
|
||||
if self._has_trajectory_latent_output(request_outputs) or self._has_latent_output(request_outputs):
|
||||
return "latent"
|
||||
if final_output_type in {"latent", "latents"}:
|
||||
return "latent"
|
||||
if final_output_type == "text":
|
||||
return "text"
|
||||
if token_count > 0:
|
||||
return "stream"
|
||||
return "other"
|
||||
|
||||
@staticmethod
|
||||
def _is_streaming_output_unit_type(unit_type: str) -> bool:
|
||||
return unit_type in {"text", "stream"}
|
||||
|
||||
def _count_output_units(
|
||||
self,
|
||||
request_outputs: list[Any],
|
||||
*,
|
||||
unit_type: str,
|
||||
fallback_token_count: int,
|
||||
) -> int:
|
||||
if unit_type == "audio":
|
||||
total_frames, _, _ = self._collect_audio_metrics(request_outputs)
|
||||
if total_frames > 0:
|
||||
return total_frames
|
||||
if unit_type == "image":
|
||||
total_images = sum(self._count_images(ro) for ro in request_outputs)
|
||||
if total_images > 0:
|
||||
return total_images
|
||||
if unit_type == "video":
|
||||
total_videos = sum(self._count_videos(ro) for ro in request_outputs)
|
||||
if total_videos > 0:
|
||||
return total_videos
|
||||
if unit_type == "latent":
|
||||
total_latents = sum(
|
||||
self._count_value_units(getattr(ro, "trajectory_latents", None))
|
||||
+ self._count_value_units(getattr(ro, "latents", None))
|
||||
for ro in request_outputs
|
||||
)
|
||||
if total_latents > 0:
|
||||
return total_latents
|
||||
return int(fallback_token_count)
|
||||
|
||||
def _has_audio_output(self, request_outputs: list[Any]) -> bool:
|
||||
for ro in request_outputs:
|
||||
for mm_output in self._iter_multimodal_outputs(ro):
|
||||
if isinstance(mm_output, dict) and mm_output.get("audio") is not None:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _collect_audio_metrics(self, request_outputs: list[Any]) -> tuple[int, int, float]:
|
||||
total_frames = 0
|
||||
sample_rate = 0
|
||||
for ro in request_outputs:
|
||||
for mm_output in self._iter_multimodal_outputs(ro):
|
||||
if not isinstance(mm_output, dict):
|
||||
continue
|
||||
if sample_rate <= 0:
|
||||
sample_rate = self._infer_audio_sample_rate(mm_output)
|
||||
audio_output = mm_output.get("audio")
|
||||
if audio_output is None:
|
||||
continue
|
||||
items = audio_output if isinstance(audio_output, list) else [audio_output]
|
||||
for item in items:
|
||||
total_frames += self._count_audio_frames(item)
|
||||
duration_s = (float(total_frames) / float(sample_rate)) if total_frames > 0 and sample_rate > 0 else 0.0
|
||||
return int(total_frames), int(sample_rate), duration_s
|
||||
|
||||
@staticmethod
|
||||
def _count_audio_frames(audio_output: Any) -> int:
|
||||
shape = getattr(audio_output, "shape", None)
|
||||
if shape is not None and len(shape) > 0:
|
||||
# Audio chunks are concatenated on dim=-1 in the output processor,
|
||||
# so the frame/sample axis is the last dim (e.g. [channels, frames]).
|
||||
# Keep this aligned with serving_chat.py: audio tensors are consumed
|
||||
# as (T,), (C, T), or (B, C, T). Flattening would corrupt
|
||||
# multi-channel audio.
|
||||
return int(shape[-1])
|
||||
try:
|
||||
return len(audio_output)
|
||||
except TypeError:
|
||||
return 1
|
||||
|
||||
def _infer_audio_sample_rate(self, mm_output: dict[str, Any]) -> int:
|
||||
for key in ("audio_sample_rate", "sample_rate", "sampling_rate", "sr"):
|
||||
rate = self._coerce_int_scalar(mm_output.get(key))
|
||||
if rate > 0:
|
||||
return rate
|
||||
for attr in ("audio_sample_rate", "sample_rate", "sampling_rate", "output_sample_rate"):
|
||||
rate = self._coerce_int_scalar(getattr(self.stage_client, attr, None))
|
||||
if rate > 0:
|
||||
return rate
|
||||
rate = self._coerce_int_scalar(getattr(self._stage_vllm_config, attr, None))
|
||||
if rate > 0:
|
||||
return rate
|
||||
return 0
|
||||
|
||||
@classmethod
|
||||
def _coerce_int_scalar(cls, value: Any) -> int:
|
||||
if value is None:
|
||||
return 0
|
||||
if isinstance(value, (list, tuple)):
|
||||
for item in value:
|
||||
coerced = cls._coerce_int_scalar(item)
|
||||
if coerced > 0:
|
||||
return coerced
|
||||
return 0
|
||||
item = getattr(value, "item", None)
|
||||
if callable(item):
|
||||
try:
|
||||
return int(item())
|
||||
except Exception:
|
||||
return 0
|
||||
try:
|
||||
return int(value)
|
||||
except (TypeError, ValueError):
|
||||
return 0
|
||||
|
||||
def _has_image_output(self, request_outputs: list[Any]) -> bool:
|
||||
for ro in request_outputs:
|
||||
if self._count_images(ro) > 0:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _has_video_output(self, request_outputs: list[Any]) -> bool:
|
||||
for ro in request_outputs:
|
||||
if self._count_videos(ro) > 0:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _has_trajectory_latent_output(self, request_outputs: list[Any]) -> bool:
|
||||
return any(self._is_non_empty_value(getattr(ro, "trajectory_latents", None)) for ro in request_outputs)
|
||||
|
||||
def _has_latent_output(self, request_outputs: list[Any]) -> bool:
|
||||
return any(self._is_non_empty_value(getattr(ro, "latents", None)) for ro in request_outputs)
|
||||
|
||||
def _count_images(self, request_output: Any) -> int:
|
||||
total_images = self._count_value_units(getattr(request_output, "images", None))
|
||||
for mm_output in self._iter_multimodal_outputs(request_output):
|
||||
if isinstance(mm_output, dict):
|
||||
total_images += self._count_value_units(mm_output.get("image"))
|
||||
total_images += self._count_value_units(mm_output.get("images"))
|
||||
return total_images
|
||||
|
||||
def _count_image_pixels(self, request_outputs: list[Any]) -> int:
|
||||
total_pixels = 0
|
||||
for ro in request_outputs:
|
||||
total_pixels += self._count_image_value_pixels(getattr(ro, "images", None))
|
||||
for mm_output in self._iter_multimodal_outputs(ro):
|
||||
if isinstance(mm_output, dict):
|
||||
total_pixels += self._count_image_value_pixels(mm_output.get("image"))
|
||||
total_pixels += self._count_image_value_pixels(mm_output.get("images"))
|
||||
return total_pixels
|
||||
|
||||
@classmethod
|
||||
def _count_image_value_pixels(cls, value: Any) -> int:
|
||||
if value is None:
|
||||
return 0
|
||||
if isinstance(value, (list, tuple)):
|
||||
return sum(cls._count_image_value_pixels(item) for item in value)
|
||||
|
||||
size = getattr(value, "size", None)
|
||||
if isinstance(size, tuple) and len(size) >= 2:
|
||||
try:
|
||||
return int(size[0]) * int(size[1])
|
||||
except (TypeError, ValueError):
|
||||
return 0
|
||||
|
||||
shape = getattr(value, "shape", None)
|
||||
if shape is None or len(shape) < 2:
|
||||
return 0
|
||||
dims = [int(dim) for dim in shape]
|
||||
if len(dims) >= 4:
|
||||
return dims[0] * dims[-2] * dims[-1]
|
||||
if len(dims) == 3 and dims[0] in (1, 3, 4):
|
||||
return dims[1] * dims[2]
|
||||
if len(dims) == 3 and dims[-1] in (1, 3, 4):
|
||||
return dims[0] * dims[1]
|
||||
return dims[-2] * dims[-1]
|
||||
|
||||
def _count_videos(self, request_output: Any) -> int:
|
||||
total_videos = self._count_video_units(getattr(request_output, "video", None))
|
||||
total_videos += self._count_video_units(getattr(request_output, "videos", None))
|
||||
for mm_output in self._iter_multimodal_outputs(request_output):
|
||||
if isinstance(mm_output, dict):
|
||||
total_videos += self._count_video_units(mm_output.get("video"))
|
||||
total_videos += self._count_video_units(mm_output.get("videos"))
|
||||
return total_videos
|
||||
|
||||
def has_non_empty_output(self, request_output: Any) -> bool:
|
||||
return self._has_non_empty_output(request_output)
|
||||
|
||||
def _has_non_empty_output(self, request_output: Any) -> bool:
|
||||
final_output_type = getattr(request_output, "final_output_type", None)
|
||||
if final_output_type is None:
|
||||
final_output_type = getattr(self.stage_client, "final_output_type", None)
|
||||
if final_output_type == "text":
|
||||
return any(bool(getattr(output, "text", "")) for output in getattr(request_output, "outputs", None) or [])
|
||||
|
||||
if getattr(request_output, "images", None):
|
||||
return True
|
||||
if getattr(request_output, "video", None) or getattr(request_output, "videos", None):
|
||||
return True
|
||||
if getattr(request_output, "trajectory_latents", None) is not None:
|
||||
return True
|
||||
custom_output = getattr(request_output, "_custom_output", None)
|
||||
if isinstance(custom_output, dict) and custom_output:
|
||||
return True
|
||||
for mm_output in self._iter_multimodal_outputs(request_output):
|
||||
if isinstance(mm_output, dict) and any(self._is_non_empty_value(value) for value in mm_output.values()):
|
||||
return True
|
||||
for output in getattr(request_output, "outputs", None) or []:
|
||||
if getattr(output, "text", None):
|
||||
return True
|
||||
token_ids = getattr(output, "token_ids", None)
|
||||
if self._is_non_empty_value(token_ids):
|
||||
return True
|
||||
cumulative_token_ids = getattr(output, "cumulative_token_ids", None)
|
||||
if self._is_non_empty_value(cumulative_token_ids):
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _is_non_empty_value(value: Any) -> bool:
|
||||
if value is None:
|
||||
return False
|
||||
shape = getattr(value, "shape", None)
|
||||
if shape is not None:
|
||||
return all(int(dim) > 0 for dim in shape)
|
||||
try:
|
||||
return len(value) > 0
|
||||
except TypeError:
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _count_value_units(value: Any) -> int:
|
||||
if value is None:
|
||||
return 0
|
||||
shape = getattr(value, "shape", None)
|
||||
if shape is not None:
|
||||
return int(shape[0]) if len(shape) > 0 and int(shape[0]) > 0 else 0
|
||||
try:
|
||||
return len(value)
|
||||
except TypeError:
|
||||
return 1
|
||||
|
||||
@staticmethod
|
||||
def _count_video_units(value: Any) -> int:
|
||||
if value is None:
|
||||
return 0
|
||||
if isinstance(value, (list, tuple)):
|
||||
return len(value)
|
||||
shape = getattr(value, "shape", None)
|
||||
if shape is not None:
|
||||
return 1 if all(int(dim) > 0 for dim in shape) else 0
|
||||
try:
|
||||
return len(value)
|
||||
except TypeError:
|
||||
return 1
|
||||
|
||||
def record_output_timestamps(self, request_outputs: list[Any], *, output_ts: float | None = None) -> None:
|
||||
"""Record all output timestamps and the first non-empty output timestamp."""
|
||||
output_ts = _time.time() if output_ts is None else output_ts
|
||||
for request_output in request_outputs:
|
||||
request_id = getattr(request_output, "request_id", None)
|
||||
if request_id is None:
|
||||
continue
|
||||
rid = str(request_id)
|
||||
self._output_timestamps_by_request.setdefault(rid, []).append(output_ts)
|
||||
if self._has_non_empty_output(request_output):
|
||||
self._non_empty_first_output_timestamps_by_request.setdefault(rid, output_ts)
|
||||
audio_frames, audio_sample_rate, _ = self._collect_audio_metrics([request_output])
|
||||
if audio_frames > 0:
|
||||
self._audio_frames_by_request[rid] = self._audio_frames_by_request.get(rid, 0) + audio_frames
|
||||
if self._audio_sample_rate_by_request.get(rid, 0) <= 0 and audio_sample_rate > 0:
|
||||
self._audio_sample_rate_by_request[rid] = audio_sample_rate
|
||||
|
||||
def _iter_multimodal_outputs(self, request_output: Any) -> list[dict[str, Any]]:
|
||||
multimodal_outputs: list[dict[str, Any]] = []
|
||||
outer_mm = getattr(request_output, "multimodal_output", None)
|
||||
if isinstance(outer_mm, dict) and outer_mm:
|
||||
multimodal_outputs.append(outer_mm)
|
||||
for output in getattr(request_output, "outputs", None) or []:
|
||||
inner_mm = getattr(output, "multimodal_output", None)
|
||||
if isinstance(inner_mm, dict) and inner_mm:
|
||||
multimodal_outputs.append(inner_mm)
|
||||
return multimodal_outputs
|
||||
|
||||
# ---- Stage-local admission ----
|
||||
|
||||
async def submit_initial(
|
||||
@@ -658,11 +1034,16 @@ class StagePool:
|
||||
return []
|
||||
client = cast(StagePoolLLMClient, raw_client)
|
||||
processor = self.output_processor
|
||||
iteration_stats = IterationStats()
|
||||
processed = processor.process_outputs(
|
||||
raw_outputs.outputs,
|
||||
raw_outputs.timestamp,
|
||||
iteration_stats,
|
||||
)
|
||||
# Use the same wall-clock source as OrchestratorRequestState.stage_submit_ts.
|
||||
# EngineCoreOutputs.timestamp may use a different clock base, which would
|
||||
# make TTFO negative and get clamped to 0.
|
||||
self.record_output_timestamps(processed.request_outputs, output_ts=_time.time())
|
||||
|
||||
if processed.reqs_to_abort:
|
||||
await client.abort_requests_async(processed.reqs_to_abort)
|
||||
|
||||
@@ -261,6 +261,7 @@ class AsyncOmni(EngineClient, OmniBase):
|
||||
data_parallel_rank: int | None = None,
|
||||
reasoning_ended: bool | None = None,
|
||||
reasoning_parser_kwargs: dict[str, Any] | None = None,
|
||||
arrival_time: float | None = None,
|
||||
) -> AsyncGenerator[OmniRequestOutput, None]:
|
||||
"""Generate outputs for the given prompt(s) asynchronously.
|
||||
|
||||
@@ -334,11 +335,12 @@ class AsyncOmni(EngineClient, OmniBase):
|
||||
)
|
||||
|
||||
# Track per-request metrics
|
||||
wall_start_ts = time.time()
|
||||
wall_start_ts = float(arrival_time) if arrival_time is not None else time.time()
|
||||
req_start_ts: dict[str, float] = {}
|
||||
|
||||
# Determine the final stage for E2E stats
|
||||
final_stage_id_for_e2e = self._compute_final_stage_id(output_modalities)
|
||||
final_output_stage_ids = self._compute_final_output_stage_ids(output_modalities) or [final_stage_id_for_e2e]
|
||||
|
||||
metrics = OrchestratorMetrics(
|
||||
self.num_stages,
|
||||
@@ -372,6 +374,8 @@ class AsyncOmni(EngineClient, OmniBase):
|
||||
input_stream=prompt,
|
||||
sampling_params_list=req_sp_list,
|
||||
final_stage_id=final_stage_id_for_e2e,
|
||||
final_output_stage_ids=final_output_stage_ids,
|
||||
arrival_time=wall_start_ts,
|
||||
)
|
||||
else:
|
||||
await self.engine.add_request_async(
|
||||
@@ -379,6 +383,8 @@ class AsyncOmni(EngineClient, OmniBase):
|
||||
prompt=prompt,
|
||||
sampling_params_list=req_sp_list,
|
||||
final_stage_id=final_stage_id_for_e2e,
|
||||
final_output_stage_ids=final_output_stage_ids,
|
||||
arrival_time=wall_start_ts,
|
||||
)
|
||||
submit_ts = time.time()
|
||||
req_state.metrics.stage_first_ts[0] = submit_ts
|
||||
@@ -421,6 +427,8 @@ class AsyncOmni(EngineClient, OmniBase):
|
||||
input_stream: AsyncGenerator[StreamingInput, None],
|
||||
sampling_params_list: Sequence[OmniSamplingParams],
|
||||
final_stage_id: int,
|
||||
final_output_stage_ids: Sequence[int],
|
||||
arrival_time: float,
|
||||
) -> asyncio.Task:
|
||||
"""Submit a streaming input generator as incremental stage-0 updates."""
|
||||
if not sampling_params_list:
|
||||
@@ -456,6 +464,8 @@ class AsyncOmni(EngineClient, OmniBase):
|
||||
prompt_text=prompt_text,
|
||||
sampling_params_list=chunk_sampling_params_list,
|
||||
final_stage_id=final_stage_id,
|
||||
final_output_stage_ids=final_output_stage_ids,
|
||||
arrival_time=arrival_time,
|
||||
resumable=True,
|
||||
)
|
||||
has_submitted_first_chunk = True
|
||||
@@ -466,6 +476,8 @@ class AsyncOmni(EngineClient, OmniBase):
|
||||
prompt_text=prompt_text,
|
||||
sampling_params_list=chunk_sampling_params_list,
|
||||
final_stage_id=final_stage_id,
|
||||
final_output_stage_ids=final_output_stage_ids,
|
||||
arrival_time=arrival_time,
|
||||
resumable=True,
|
||||
)
|
||||
except (asyncio.CancelledError, GeneratorExit):
|
||||
@@ -492,6 +504,8 @@ class AsyncOmni(EngineClient, OmniBase):
|
||||
prompt_text=None,
|
||||
sampling_params_list=final_sampling_params_list,
|
||||
final_stage_id=final_stage_id,
|
||||
final_output_stage_ids=final_output_stage_ids,
|
||||
arrival_time=arrival_time,
|
||||
resumable=False,
|
||||
)
|
||||
else:
|
||||
@@ -501,6 +515,8 @@ class AsyncOmni(EngineClient, OmniBase):
|
||||
prompt_text=None,
|
||||
sampling_params_list=final_sampling_params_list,
|
||||
final_stage_id=final_stage_id,
|
||||
final_output_stage_ids=final_output_stage_ids,
|
||||
arrival_time=arrival_time,
|
||||
resumable=False,
|
||||
)
|
||||
|
||||
|
||||
@@ -131,6 +131,31 @@ def add_seed_tts_cli_args(parser: argparse.ArgumentParser) -> None:
|
||||
)
|
||||
|
||||
|
||||
def add_omni_benchmark_cli_args(parser: argparse.ArgumentParser) -> None:
|
||||
"""Add vLLM-Omni specific serving benchmark options."""
|
||||
group = parser.add_argument_group("vLLM-Omni Multi-stage Benchmark Options")
|
||||
group.add_argument(
|
||||
"--print-stage",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=(
|
||||
"Print per-stage benchmark metrics for --omni serving when stage metrics are returned by the server. "
|
||||
"Disabled by default. The latency sections follow --percentile-metrics by modality: "
|
||||
"ttft/tpot/itl control text stages, ttfc/tpoc/icl control internal stream stages, "
|
||||
"and tpop controls both text TPOP and internal stream TPOP."
|
||||
),
|
||||
)
|
||||
group.add_argument(
|
||||
"--image-edits-bot-task",
|
||||
type=str,
|
||||
default="think",
|
||||
help=(
|
||||
"Default bot_task form field for --backend openai-image-edits-omni "
|
||||
'(/v1/images/edits). Use --extra-body \'{"bot_task":"..."}\' to override per run.'
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
_OMNI_BENCH_DATASET_CHOICES = (
|
||||
"daily-omni",
|
||||
"seed-tts",
|
||||
@@ -173,16 +198,39 @@ class OmniBenchmarkServingSubcommand(OmniBenchmarkSubcommandBase):
|
||||
# Add Daily-Omni specific arguments
|
||||
add_daily_omni_cli_args(parser)
|
||||
add_seed_tts_cli_args(parser)
|
||||
add_omni_benchmark_cli_args(parser)
|
||||
|
||||
for action in parser._actions:
|
||||
if action.dest == "dataset_name" and action.choices is not None:
|
||||
extra = [
|
||||
c
|
||||
for c in (
|
||||
"daily-omni",
|
||||
"seed-tts",
|
||||
"seed-tts-text",
|
||||
"seed-tts-design",
|
||||
"ttsd",
|
||||
"sound-effect",
|
||||
)
|
||||
if c not in action.choices
|
||||
]
|
||||
if extra:
|
||||
action.choices = list(action.choices) + extra
|
||||
if action.dest == "backend" and action.choices is not None:
|
||||
extra = [c for c in ("openai-image-edits-omni",) if c not in action.choices]
|
||||
if extra:
|
||||
action.choices = list(action.choices) + extra
|
||||
_extend_omni_dataset_name_choices(parser)
|
||||
|
||||
# Update help messages for omni-specific features
|
||||
for action in parser._actions:
|
||||
if action.dest == "percentile_metrics":
|
||||
action.help = (
|
||||
"Comma-separated list of selected metrics to report percentiles."
|
||||
"This argument specifies the metrics to report percentiles."
|
||||
'Allowed metric names are "ttft", "tpot", "itl", "e2el", "audio_ttfp", "audio_rtf". '
|
||||
"Comma-separated list of selected metrics to report percentiles. "
|
||||
'For text metrics, "ttft", "tpot", and "itl" affect the global benchmark and text '
|
||||
'stage metrics. "tpop" also requests text TPOT/TPOP globally and per stage, and internal '
|
||||
'stream TPOP. "ttfc", "tpoc", and "icl" only affect internal stream stage metrics. '
|
||||
'Audio metrics include "audio_ttfp", "audio_rtf", "audio_duration", and "audio_underrun".'
|
||||
)
|
||||
if action.dest == "random_mm_limit_mm_per_prompt":
|
||||
action.help = (
|
||||
@@ -216,4 +264,9 @@ class OmniBenchmarkServingSubcommand(OmniBenchmarkSubcommandBase):
|
||||
os.environ["SEED_TTS_WER_EVAL"] = "1"
|
||||
if getattr(args, "seed_tts_wer_save_items", False):
|
||||
os.environ["SEED_TTS_WER_SAVE_ITEMS"] = "1"
|
||||
image_edits_bot_task = getattr(args, "image_edits_bot_task", None)
|
||||
if image_edits_bot_task is not None:
|
||||
extra_body = dict(getattr(args, "extra_body", None) or {})
|
||||
extra_body.setdefault("bot_task", image_edits_bot_task)
|
||||
args.extra_body = extra_body
|
||||
main(args)
|
||||
@@ -121,6 +121,7 @@ class Omni(OmniBase):
|
||||
for req_id, prompt in zip(request_ids, request_prompts):
|
||||
prompt_modalities = prompt.get("modalities", None) if isinstance(prompt, dict) else None
|
||||
final_stage_id = self._compute_final_stage_id(prompt_modalities)
|
||||
final_output_stage_ids = self._compute_final_output_stage_ids(prompt_modalities) or [final_stage_id]
|
||||
req_final_stage_ids[req_id] = final_stage_id
|
||||
|
||||
metrics = OrchestratorMetrics(
|
||||
@@ -145,6 +146,7 @@ class Omni(OmniBase):
|
||||
prompt=prompt,
|
||||
sampling_params_list=req_sp_list,
|
||||
final_stage_id=final_stage_id,
|
||||
final_output_stage_ids=final_output_stage_ids,
|
||||
)
|
||||
submit_ts = time.time()
|
||||
req_state.metrics.stage_first_ts[0] = submit_ts
|
||||
|
||||
@@ -22,7 +22,7 @@ from vllm_omni.entrypoints.pd_utils import PDDisaggregationMixin
|
||||
from vllm_omni.entrypoints.utils import coerce_param_message_types, get_final_stage_id_for_e2e
|
||||
from vllm_omni.metrics.modality import OmniModalityMetrics, observe_modality_at_finalize
|
||||
from vllm_omni.metrics.prometheus import OmniPrometheusMetrics
|
||||
from vllm_omni.metrics.stats import OrchestratorAggregator as OrchestratorMetrics
|
||||
from vllm_omni.metrics.stats import OrchestratorAggregator
|
||||
from vllm_omni.metrics.transfer import OmniTransferMetrics
|
||||
from vllm_omni.model_executor.model_loader.weight_utils import download_weights_from_hf_specific
|
||||
from vllm_omni.outputs import OmniRequestOutput
|
||||
@@ -189,6 +189,7 @@ class OmniBase(PDDisaggregationMixin):
|
||||
self.async_chunk = bool(getattr(self.engine, "async_chunk", False))
|
||||
|
||||
self.request_states: dict[str, ClientRequestState] = {}
|
||||
self._consumed_metric_messages: dict[str, set[int]] = {}
|
||||
self.prom_metrics = OmniPrometheusMetrics(model_name=model, log_stats=log_stats)
|
||||
self.mod_metrics = OmniModalityMetrics(model_name=model, log_stats=log_stats)
|
||||
|
||||
@@ -308,6 +309,7 @@ class OmniBase(PDDisaggregationMixin):
|
||||
)
|
||||
finally:
|
||||
self.request_states.pop(request_id, None)
|
||||
self._consumed_metric_messages.pop(request_id, None)
|
||||
# Republish gauges so any stale value left by the per-stage
|
||||
# publish in _process_single_result (which runs while the request
|
||||
# is still in self.request_states) is corrected after the pop.
|
||||
@@ -326,6 +328,17 @@ class OmniBase(PDDisaggregationMixin):
|
||||
self._stage_meta_list,
|
||||
)
|
||||
|
||||
def _compute_final_output_stage_ids(self, output_modalities: list[str] | None) -> list[int]:
|
||||
requested_modalities = output_modalities or self.output_modalities
|
||||
requested_modalities = [m for m in requested_modalities if m in self.output_modalities]
|
||||
if not requested_modalities:
|
||||
requested_modalities = self.output_modalities
|
||||
return [
|
||||
sid
|
||||
for sid, stage in enumerate(self._stage_meta_list)
|
||||
if getattr(stage, "final_output", False) and stage.final_output_type in requested_modalities
|
||||
]
|
||||
|
||||
def _process_stage_metrics_message(self, msg: StageMetricsMessage) -> None:
|
||||
req_id = msg.request_id
|
||||
req_state = self.request_states.get(req_id)
|
||||
@@ -333,7 +346,8 @@ class OmniBase(PDDisaggregationMixin):
|
||||
return
|
||||
_m = msg.metrics
|
||||
stage_id = msg.stage_id
|
||||
req_state.metrics.on_stage_metrics(stage_id, req_id, _m)
|
||||
stage_meta = self.engine.get_stage_metadata(stage_id)
|
||||
req_state.metrics.on_stage_metrics(stage_id, req_id, _m, stage_meta.final_output_type)
|
||||
submit_ts = msg.stage_submit_ts
|
||||
now = time.time()
|
||||
if req_state.metrics.stage_first_ts[stage_id] is None:
|
||||
@@ -378,6 +392,20 @@ class OmniBase(PDDisaggregationMixin):
|
||||
|
||||
req_state.stage_id = stage_id
|
||||
|
||||
if msg.metrics is not None and not msg.finished and req_state.metrics is not None:
|
||||
stage_meta = self.engine.get_stage_metadata(stage_id)
|
||||
output_type = getattr(msg.engine_outputs, "final_output_type", stage_meta.final_output_type)
|
||||
msg_id = id(msg)
|
||||
consumed = self._consumed_metric_messages.setdefault(req_id, set())
|
||||
if msg_id not in consumed:
|
||||
req_state.metrics.on_stage_metrics(stage_id, req_id, msg.metrics, output_type)
|
||||
submit_ts = msg.stage_submit_ts
|
||||
now = time.time()
|
||||
if req_state.metrics.stage_first_ts[stage_id] is None:
|
||||
req_state.metrics.stage_first_ts[stage_id] = submit_ts if submit_ts is not None else now
|
||||
req_state.metrics.stage_last_ts[stage_id] = max(req_state.metrics.stage_last_ts[stage_id] or 0.0, now)
|
||||
consumed.add(msg_id)
|
||||
|
||||
return False, req_id, stage_id, req_state
|
||||
|
||||
def _check_engine_output_error(
|
||||
@@ -415,7 +443,7 @@ class OmniBase(PDDisaggregationMixin):
|
||||
self,
|
||||
result: OutputMessage,
|
||||
stage_id: int,
|
||||
metrics: OrchestratorMetrics,
|
||||
metrics: OrchestratorAggregator,
|
||||
req_start_ts: dict[str, float],
|
||||
wall_start_ts: float,
|
||||
final_stage_id_for_e2e: int,
|
||||
@@ -457,10 +485,11 @@ class OmniBase(PDDisaggregationMixin):
|
||||
metrics.stage_last_ts[stage_id] = max(metrics.stage_last_ts[stage_id] or 0.0, now)
|
||||
|
||||
_m = result.metrics
|
||||
if finished and _m is not None:
|
||||
metrics.on_stage_metrics(stage_id, req_id, _m)
|
||||
|
||||
stage_meta = self.engine.get_stage_metadata(stage_id)
|
||||
output_type = getattr(engine_outputs, "final_output_type", stage_meta.final_output_type)
|
||||
if finished and _m is not None:
|
||||
metrics.on_stage_metrics(stage_id, req_id, _m, output_type)
|
||||
|
||||
if not stage_meta.final_output:
|
||||
return None
|
||||
|
||||
@@ -510,6 +539,31 @@ class OmniBase(PDDisaggregationMixin):
|
||||
self.prom_metrics.set_waiting(max(0, total - running))
|
||||
|
||||
images = getattr(engine_outputs, "images", []) if output_type == "image" else []
|
||||
response_metrics: dict[str, Any] = {}
|
||||
stage_metrics: dict[str, dict[str, Any]] = {}
|
||||
rid_key = str(req_id)
|
||||
for evt in metrics.stage_events.get(rid_key, []):
|
||||
if evt.stage_id is None:
|
||||
continue
|
||||
sid = int(evt.stage_id)
|
||||
evt_stage_meta = self.engine.get_stage_metadata(sid)
|
||||
evt_output_type = evt.final_output_type
|
||||
if not evt_output_type:
|
||||
evt_output_type = evt_stage_meta.final_output_type
|
||||
stage_name = evt_stage_meta.model_stage
|
||||
sid_key = str(sid)
|
||||
stage_metrics[sid_key] = OrchestratorAggregator._merge_stage_metric_event(stage_metrics.get(sid_key), evt)
|
||||
stage_metrics[sid_key]["stage_name"] = stage_name or f"stage_{sid}"
|
||||
stage_metrics[sid_key]["final_output_type"] = evt_output_type
|
||||
if stage_metrics:
|
||||
response_metrics["stage_metrics"] = stage_metrics
|
||||
current_stage_metrics = stage_metrics.get(str(stage_id))
|
||||
if current_stage_metrics is not None:
|
||||
response_metrics["stage_id"] = current_stage_metrics["stage_id"]
|
||||
response_metrics["final_output_type"] = current_stage_metrics["final_output_type"]
|
||||
if current_stage_metrics["final_output_type"] == "text":
|
||||
response_metrics["num_tokens_in"] = current_stage_metrics["num_tokens_in"]
|
||||
response_metrics["num_tokens_out"] = current_stage_metrics["num_tokens_out"]
|
||||
return OmniRequestOutput(
|
||||
request_id=req_id or "",
|
||||
stage_id=stage_id,
|
||||
@@ -521,6 +575,7 @@ class OmniBase(PDDisaggregationMixin):
|
||||
trajectory_log_probs=getattr(engine_outputs, "trajectory_log_probs", None),
|
||||
trajectory_decoded=getattr(engine_outputs, "trajectory_decoded", None),
|
||||
_custom_output=getattr(engine_outputs, "_custom_output", {}),
|
||||
metrics=response_metrics,
|
||||
stage_durations=stage_durations,
|
||||
peak_memory_mb=peak_memory_mb,
|
||||
)
|
||||
|
||||
@@ -29,6 +29,7 @@ from PIL import Image
|
||||
from pydantic import BaseModel, Field
|
||||
from starlette.datastructures import State
|
||||
from starlette.routing import Route
|
||||
from starlette.types import ASGIApp, Receive, Scope, Send
|
||||
from vllm.engine.protocol import EngineClient
|
||||
from vllm.entrypoints.anthropic.serving import AnthropicServingMessages
|
||||
from vllm.entrypoints.chat_utils import load_chat_template
|
||||
@@ -445,6 +446,7 @@ async def omni_run_server_worker(listen_address, sock, args, client_config=None,
|
||||
|
||||
# OMNI: Pass supported_tasks to build_app (required by upstream vLLM)
|
||||
app = build_openai_app(args, supported_tasks)
|
||||
|
||||
# OMNI: Remove upstream routes that we override with omni-specific handlers
|
||||
_remove_route_from_app(app, "/v1/chat/completions", {"POST"})
|
||||
_remove_route_from_app(app, "/v1/models", {"GET"}) # Remove upstream /v1/models to use omni's handler
|
||||
@@ -477,8 +479,29 @@ async def omni_run_server_worker(listen_address, sock, args, client_config=None,
|
||||
vllm_config.parallel_config._api_process_rank,
|
||||
listen_address,
|
||||
)
|
||||
|
||||
class _TimestampMiddleware:
|
||||
"""Pure-ASGI outermost wrapper that stamps HTTP request arrival time.
|
||||
|
||||
Wraps the fully-built Starlette app as an outer ASGI layer so no
|
||||
Starlette internals (user_middleware, middleware_stack, etc.) are
|
||||
touched. Websocket and lifespan scopes pass through unchanged.
|
||||
"""
|
||||
|
||||
def __init__(self, inner: ASGIApp) -> None:
|
||||
self._inner = inner
|
||||
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
return getattr(self._inner, name)
|
||||
|
||||
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
||||
if scope["type"] == "http":
|
||||
scope.setdefault("state", {})
|
||||
scope["state"]["request_timestamp"] = time.time()
|
||||
await self._inner(scope, receive, send)
|
||||
|
||||
shutdown_task = await serve_http(
|
||||
app,
|
||||
_TimestampMiddleware(app),
|
||||
sock=sock,
|
||||
enable_ssl_refresh=args.enable_ssl_refresh,
|
||||
host=args.host,
|
||||
@@ -1592,6 +1615,7 @@ async def generate_images(request: ImageGenerationRequest, raw_request: Request)
|
||||
Raises:
|
||||
HTTPException: For validation errors, missing engine, or generation failures
|
||||
"""
|
||||
request_timestamp = float(getattr(raw_request.state, "request_timestamp", time.time()))
|
||||
# Get engine client (AsyncOmni) from app state
|
||||
engine_client, model_name, stage_configs = _get_engine_and_model(raw_request)
|
||||
|
||||
@@ -1654,6 +1678,7 @@ async def generate_images(request: ImageGenerationRequest, raw_request: Request)
|
||||
extra_body=extra_body,
|
||||
request_id=f"img_gen-{random_uuid()}",
|
||||
raw_request=raw_request,
|
||||
arrival_time=request_timestamp,
|
||||
)
|
||||
if isinstance(generation_result, ErrorResponse):
|
||||
return JSONResponse(
|
||||
@@ -1736,6 +1761,7 @@ async def generate_images(request: ImageGenerationRequest, raw_request: Request)
|
||||
stage_configs=stage_configs,
|
||||
prompt=prompt,
|
||||
request_id=request_id,
|
||||
arrival_time=request_timestamp,
|
||||
)
|
||||
|
||||
if result is None:
|
||||
@@ -1826,12 +1852,14 @@ async def edit_images(
|
||||
bot_task: str | None = Form(None),
|
||||
sys_type: str | None = Form(None),
|
||||
system_prompt: str | None = Form(None),
|
||||
return_stage_metrics: bool | None = Form(None),
|
||||
) -> ImageGenerationResponse:
|
||||
"""
|
||||
OpenAI-compatible image edit endpoint.
|
||||
"""
|
||||
|
||||
# 1. get engine and model
|
||||
request_timestamp = float(getattr(raw_request.state, "request_timestamp", time.time()))
|
||||
engine_client, model_name, stage_configs = _get_engine_and_model(raw_request)
|
||||
if model is not None and model != model_name:
|
||||
raise HTTPException(
|
||||
@@ -2050,6 +2078,8 @@ async def edit_images(
|
||||
extra_body["sys_type"] = sys_type
|
||||
if system_prompt is not None:
|
||||
extra_body["system_prompt"] = system_prompt
|
||||
if return_stage_metrics is not None:
|
||||
extra_body["return_stage_metrics"] = return_stage_metrics
|
||||
|
||||
prompt_text = prompt.get("prompt", "")
|
||||
generation_result = await chat_handler.generate_diffusion_images(
|
||||
@@ -2057,6 +2087,7 @@ async def edit_images(
|
||||
extra_body=extra_body,
|
||||
reference_images=ref_b64_list,
|
||||
request_id=request_id,
|
||||
arrival_time=request_timestamp,
|
||||
stream=stream,
|
||||
model=model_name,
|
||||
output_format=output_format,
|
||||
|
||||
@@ -193,6 +193,10 @@ class ImageEditARDeltaChunk(BaseModel):
|
||||
index: int = Field(default=0, description="Completion index for the AR stage output")
|
||||
created: int = Field(..., description="Unix timestamp of when the stream was created")
|
||||
model: str = Field(..., description="Model used for the image edit request")
|
||||
metrics: dict[str, Any] | None = Field(
|
||||
default=None,
|
||||
description="Optional vLLM-Omni per-stage metrics snapshot for benchmark clients.",
|
||||
)
|
||||
|
||||
|
||||
class ImageEditImageChunk(BaseModel):
|
||||
@@ -205,6 +209,10 @@ class ImageEditImageChunk(BaseModel):
|
||||
size: str = Field(..., description="The generated image size")
|
||||
created: int = Field(..., description="Unix timestamp of when the stream was created")
|
||||
model: str = Field(..., description="Model used for the image edit request")
|
||||
metrics: dict[str, Any] | None = Field(
|
||||
default=None,
|
||||
description="Optional vLLM-Omni per-stage metrics snapshot for benchmark clients.",
|
||||
)
|
||||
|
||||
|
||||
class ImageEditStreamError(BaseModel):
|
||||
|
||||
@@ -216,6 +216,27 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin):
|
||||
self._supported_speakers = set()
|
||||
return self._supported_speakers
|
||||
|
||||
@staticmethod
|
||||
def _truthy_extra_body_flag(request: Any, key: str) -> bool:
|
||||
if isinstance(request, dict):
|
||||
extra_body = request
|
||||
model_extra = {}
|
||||
else:
|
||||
extra_body = getattr(request, "extra_body", None) or {}
|
||||
model_extra = getattr(request, "model_extra", None) or {}
|
||||
value = extra_body.get(key, model_extra.get(key))
|
||||
if isinstance(value, str):
|
||||
return value.lower() in {"1", "true", "yes", "on"}
|
||||
return bool(value)
|
||||
|
||||
@classmethod
|
||||
def _filter_stage_metrics_detail(cls, metrics: dict[str, Any] | None, request: Any) -> dict[str, Any] | None:
|
||||
if not metrics:
|
||||
return metrics
|
||||
if cls._truthy_extra_body_flag(request, "return_stage_metrics"):
|
||||
return metrics
|
||||
return None
|
||||
|
||||
async def create_chat_completion(
|
||||
self,
|
||||
request: ChatCompletionRequest,
|
||||
@@ -244,6 +265,10 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin):
|
||||
if self._diffusion_mode:
|
||||
return await self._create_diffusion_chat_completion(request, raw_request)
|
||||
|
||||
request_timestamp = time.time()
|
||||
if raw_request is not None:
|
||||
request_timestamp = float(getattr(raw_request.state, "request_timestamp", request_timestamp))
|
||||
|
||||
error_check_ret = await self._check_model(request)
|
||||
if error_check_ret is not None:
|
||||
logger.error("Error with model %s", error_check_ret)
|
||||
@@ -555,6 +580,7 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin):
|
||||
request_id=request_id,
|
||||
sampling_params_list=sampling_params_list,
|
||||
output_modalities=output_modalities,
|
||||
arrival_time=request_timestamp,
|
||||
)
|
||||
|
||||
generators.append(generator)
|
||||
@@ -1624,7 +1650,7 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin):
|
||||
choices=[choice_data],
|
||||
model=model_name,
|
||||
modality=final_output_type,
|
||||
metrics=omni_res.metrics,
|
||||
metrics=self._filter_stage_metrics_detail(omni_res.metrics, request),
|
||||
)
|
||||
|
||||
# handle usage stats if requested & if continuous
|
||||
@@ -1705,6 +1731,48 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin):
|
||||
choices=choices_data,
|
||||
model=model_name,
|
||||
modality=final_output_type,
|
||||
metrics=self._filter_stage_metrics_detail(omni_res.metrics, request),
|
||||
)
|
||||
chunk.usage = UsageInfo(
|
||||
prompt_tokens=num_prompt_tokens,
|
||||
completion_tokens=0,
|
||||
total_tokens=num_prompt_tokens,
|
||||
)
|
||||
data = chunk.model_dump_json(exclude_unset=True)
|
||||
yield f"data: {data}\n\n"
|
||||
|
||||
elif final_output_type == "image":
|
||||
role = self.get_chat_request_role(request)
|
||||
choices_data = []
|
||||
for choice in self._create_image_choice(omni_res, role, request, stream=True):
|
||||
delta = DeltaMessage.model_construct(role=role)
|
||||
object.__setattr__(delta, "content", choice.message.content)
|
||||
if hasattr(delta, "__pydantic_fields_set__"):
|
||||
delta.__pydantic_fields_set__.add("content")
|
||||
stream_choice = ChatCompletionResponseStreamChoice(
|
||||
index=choice.index,
|
||||
delta=delta,
|
||||
logprobs=None,
|
||||
finish_reason=choice.finish_reason,
|
||||
stop_reason=choice.stop_reason,
|
||||
)
|
||||
if stream_choice.finish_reason is not None:
|
||||
modality_finished[stream_choice.index].add("image")
|
||||
if modality_seen[stream_choice.index] < set(request.modalities) or not all(
|
||||
m in modality_finished[stream_choice.index] for m in modality_seen[stream_choice.index]
|
||||
):
|
||||
stream_choice.finish_reason = None
|
||||
else:
|
||||
stop_reason_emitted[stream_choice.index] = True
|
||||
choices_data.append(stream_choice)
|
||||
chunk = OmniChatCompletionStreamResponse(
|
||||
id=request_id,
|
||||
object=chunk_object_type,
|
||||
created=created_time,
|
||||
choices=choices_data,
|
||||
model=model_name,
|
||||
modality=final_output_type,
|
||||
metrics=self._filter_stage_metrics_detail(omni_res.metrics, request),
|
||||
)
|
||||
# NOTE: Currently usage is only set the text stages to align with the behavior
|
||||
# of the full generator. TODO (Alex): Add support for usage on all stages for
|
||||
@@ -1772,7 +1840,7 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin):
|
||||
choices=[],
|
||||
model=model_name,
|
||||
usage=final_usage,
|
||||
metrics=last_metrics,
|
||||
metrics=self._filter_stage_metrics_detail(last_metrics, request),
|
||||
)
|
||||
final_usage_data = final_usage_chunk.model_dump_json(exclude_unset=True, exclude_none=True)
|
||||
yield f"data: {final_usage_data}\n\n"
|
||||
@@ -1926,6 +1994,8 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin):
|
||||
response_metrics.update(extra)
|
||||
choices.extend(choices_data)
|
||||
|
||||
response_metrics = self._filter_stage_metrics_detail(response_metrics, request)
|
||||
|
||||
# Compute prompt_text for non-streaming response (upstream #42052)
|
||||
prompt_text = (
|
||||
getattr(final_res, "prompt", None)
|
||||
@@ -2248,6 +2318,8 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin):
|
||||
# omits "sr" and runs at 24kHz; Ming-flash-omni surfaces a 44.1kHz
|
||||
# AudioVAE rate via multimodal_output["sr"].
|
||||
sr_raw = mm_output.get("sr")
|
||||
if isinstance(sr_raw, (list, tuple)):
|
||||
sr_raw = next((item for item in sr_raw if item is not None), None)
|
||||
if sr_raw is None:
|
||||
sample_rate = 24000
|
||||
elif hasattr(sr_raw, "item"):
|
||||
@@ -2696,6 +2768,7 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin):
|
||||
extra_body: dict[str, Any] | None = None,
|
||||
reference_images: list[str] | None = None,
|
||||
request_id: str | None = None,
|
||||
arrival_time: float | None = None,
|
||||
stream: bool = False,
|
||||
model: str | None = None,
|
||||
output_format: str = "png",
|
||||
@@ -2717,6 +2790,7 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin):
|
||||
engine, gen_prompt, gen_params, pil_images = prepared
|
||||
if extra_body is None:
|
||||
extra_body = {}
|
||||
return_stage_metrics = self._truthy_extra_body_flag(extra_body, "return_stage_metrics")
|
||||
|
||||
if isinstance(engine, AsyncOmni):
|
||||
diffusion_engine = cast(AsyncOmni, engine)
|
||||
@@ -2755,6 +2829,7 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin):
|
||||
prompt=engine_prompt,
|
||||
sampling_params_list=sampling_params_list,
|
||||
request_id=request_id,
|
||||
arrival_time=arrival_time,
|
||||
)
|
||||
if stream:
|
||||
return self._stream_diffusion_image_chunks(
|
||||
@@ -2763,6 +2838,7 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin):
|
||||
output_format=output_format,
|
||||
output_compression=output_compression,
|
||||
size=size,
|
||||
return_stage_metrics=return_stage_metrics,
|
||||
raw_request=raw_request,
|
||||
)
|
||||
|
||||
@@ -2818,6 +2894,7 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin):
|
||||
output_format: str,
|
||||
output_compression: int,
|
||||
size: str,
|
||||
return_stage_metrics: bool,
|
||||
raw_request: Request | None = None,
|
||||
) -> AsyncIterator[str]:
|
||||
"""Yield image edit SSE chunks from multi-stage diffusion outputs."""
|
||||
@@ -2827,6 +2904,7 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin):
|
||||
async for output in result_generator:
|
||||
final_output_type = getattr(output, "final_output_type", None)
|
||||
stage_id = getattr(output, "stage_id", None)
|
||||
metrics = getattr(output, "metrics", None) if return_stage_metrics else None
|
||||
if final_output_type == "text" and stage_id == 0:
|
||||
request_output = output.request_output
|
||||
for completion in request_output.outputs:
|
||||
@@ -2838,6 +2916,7 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin):
|
||||
index=completion.index,
|
||||
created=created,
|
||||
model=model,
|
||||
metrics=metrics,
|
||||
)
|
||||
yield f"data: {chunk.model_dump_json()}\n\n"
|
||||
elif final_output_type == "image":
|
||||
@@ -2861,6 +2940,7 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin):
|
||||
size=size,
|
||||
created=created,
|
||||
model=model,
|
||||
metrics=metrics,
|
||||
)
|
||||
yield f"data: {chunk.model_dump_json()}\n\n"
|
||||
emitted_image = True
|
||||
|
||||
@@ -94,7 +94,9 @@ def _normalize_video_tensor(video_tensor: torch.Tensor) -> np.ndarray:
|
||||
video_tensor = video_tensor.permute(1, 2, 3, 0)
|
||||
|
||||
if video_tensor.is_floating_point():
|
||||
video_tensor = video_tensor.clamp(-1, 1) * 0.5 + 0.5
|
||||
# Cast to float32 first: bf16 (e.g. SANA-WM's refiner output) has no
|
||||
# numpy dtype, so ``.numpy()`` below raises on it.
|
||||
video_tensor = video_tensor.float().clamp(-1, 1) * 0.5 + 0.5
|
||||
else:
|
||||
video_tensor = video_tensor.to(torch.float32) / 255.0
|
||||
video_array = video_tensor.numpy()
|
||||
|
||||
@@ -11,6 +11,8 @@ time-bearing metrics use the ``_s`` suffix (values in seconds), counters use
|
||||
``_total`` (auto-suffixed by the prometheus client), sizes use ``_bytes``.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
# vllm:omni_ avoids upstream's unregister_vllm_metrics() stripping, which
|
||||
# removes every collector whose ``_name`` does not start with ``vllm``.
|
||||
METRIC_PREFIX = "vllm:omni_"
|
||||
@@ -27,6 +29,78 @@ 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"
|
||||
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"
|
||||
|
||||
# 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_RTFS = "audio_rtfs"
|
||||
AUDIO_DURATIONS = "audio_durations"
|
||||
MISSING_AUDIO_DURATION_COUNT = "missing_audio_duration_count"
|
||||
MISSING_AUDIO_RTF_COUNT = "missing_audio_rtf_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"
|
||||
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)
|
||||
@@ -186,6 +260,16 @@ def compute_audio_rtf(stage_gen_time_s: float, audio_duration_s: float) -> float
|
||||
return stage_gen_time_s / audio_duration_s
|
||||
|
||||
|
||||
def compute_denoise_step_latency(stage_gen_time: float, num_inference_steps: int) -> float:
|
||||
"""Mean denoise step latency = image stage generation time / step count.
|
||||
|
||||
The returned value uses the same time unit as ``stage_gen_time``.
|
||||
"""
|
||||
if num_inference_steps <= 0:
|
||||
return 0.0
|
||||
return stage_gen_time / float(num_inference_steps)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Audio sample-rate resolution
|
||||
# ============================================================================
|
||||
@@ -195,21 +279,22 @@ def compute_audio_rtf(stage_gen_time_s: float, audio_duration_s: float) -> float
|
||||
# at runtime so this default only kicks in when the field is missing.
|
||||
DEFAULT_AUDIO_SAMPLE_RATE = 24000
|
||||
|
||||
_SAMPLE_RATE_KEYS = ("audio_sample_rate", "sample_rate", "sampling_rate", "sr")
|
||||
_SAMPLE_RATE_KEYS = ("output_sample_rate", "audio_sample_rate", "sample_rate", "sampling_rate", "sr")
|
||||
|
||||
|
||||
def resolve_audio_sample_rate(multimodal_output: dict | None) -> int:
|
||||
"""Extract audio sample_rate from a multimodal_output dict, with fallbacks.
|
||||
def resolve_audio_sample_rate(source: dict[str, Any] | Any | None) -> int:
|
||||
"""Extract audio sample_rate from a dict or config object, with fallbacks.
|
||||
|
||||
Tries the same key chain as serving_chat.py's audio response path so
|
||||
/metrics audio_duration_s = audio_frames / sample_rate stays consistent
|
||||
with what the OpenAI streaming endpoint reports back to clients.
|
||||
with what the OpenAI streaming endpoint reports back to clients. Also
|
||||
accepts config objects that expose the same values as attributes.
|
||||
Returns DEFAULT_AUDIO_SAMPLE_RATE when no usable value is present.
|
||||
"""
|
||||
if not multimodal_output:
|
||||
if not source:
|
||||
return DEFAULT_AUDIO_SAMPLE_RATE
|
||||
for key in _SAMPLE_RATE_KEYS:
|
||||
raw = multimodal_output.get(key)
|
||||
raw = source.get(key) if isinstance(source, dict) else getattr(source, key, None)
|
||||
if raw is None:
|
||||
continue
|
||||
try:
|
||||
|
||||
+152
-14
@@ -9,6 +9,7 @@ from typing import TYPE_CHECKING, Any
|
||||
|
||||
from vllm.logger import init_logger
|
||||
|
||||
from vllm_omni.metrics import definitions as defs
|
||||
from vllm_omni.metrics.utils import _build_field_defs, _build_row, _format_table
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -45,7 +46,22 @@ class StageRequestStats:
|
||||
postprocess_time_ms: float = 0.0
|
||||
diffusion_metrics: dict[str, int] = None
|
||||
audio_generated_frames: int = 0
|
||||
audio_sample_rate: int = 0
|
||||
audio_duration_s: float = 0.0
|
||||
audio_rtf: float = 0.0
|
||||
image_pixels: int = 0
|
||||
denoise_step_latency_ms: float = 0.0
|
||||
pipeline_timings: dict[str, float] | None = None
|
||||
output_unit_type: str | None = None
|
||||
output_unit_count: int = 0
|
||||
serving_time_to_first_output_ms: float = 0.0
|
||||
time_per_output_unit_ms: float = 0.0
|
||||
inter_output_latency_ms: float = 0.0
|
||||
inter_output_latencies_ms: list[float] | None = None
|
||||
vllm_ttft_ms: float = 0.0
|
||||
vllm_tpot_ms: float = 0.0
|
||||
vllm_itl_ms: float = 0.0
|
||||
vllm_itls_ms: list[float] | None = None
|
||||
|
||||
@property
|
||||
def rx_mbps(self) -> float:
|
||||
@@ -332,14 +348,15 @@ class OrchestratorAggregator:
|
||||
and len(multimodal_output) > 0
|
||||
):
|
||||
nframes = sum(
|
||||
int(t.shape[0]) if t.ndim > 0 else 1
|
||||
int(t.shape[-1]) if t.ndim > 0 else 1
|
||||
for t in (multimodal_output if isinstance(multimodal_output, list) else [multimodal_output])
|
||||
)
|
||||
stage_events_for_req = self.stage_events.get(request_id, [])
|
||||
if stage_events_for_req:
|
||||
for stage_event in stage_events_for_req:
|
||||
if stage_event.stage_id == stage_id:
|
||||
stage_event.audio_generated_frames += nframes
|
||||
if stage_event.audio_generated_frames <= 0:
|
||||
stage_event.audio_generated_frames = nframes
|
||||
break
|
||||
else:
|
||||
logger.warning(
|
||||
@@ -392,24 +409,40 @@ class OrchestratorAggregator:
|
||||
if output_to_yield is None:
|
||||
return
|
||||
|
||||
# 3. Not finished yet — empty metrics, skip audio recording
|
||||
rid_key = str(req_id)
|
||||
stage_snapshot = self._build_stage_metrics_snapshot(rid_key)
|
||||
|
||||
# 3. Not finished yet — expose incremental per-stage snapshot for streaming clients.
|
||||
if not finished:
|
||||
output_to_yield.metrics = {}
|
||||
if stage_snapshot:
|
||||
output_to_yield.metrics = {"stage_metrics": stage_snapshot}
|
||||
else:
|
||||
output_to_yield.metrics = {}
|
||||
return
|
||||
|
||||
# 4. Finished with output: assign text metrics if available
|
||||
output_to_yield.metrics = {}
|
||||
# 4. Finished with output: always attach per-stage snapshot; keep legacy top-level
|
||||
# token fields only for text stages (OpenAI-style completion token accounting).
|
||||
output_to_yield.metrics = {"stage_metrics": stage_snapshot}
|
||||
stage_event = next(
|
||||
(evt for evt in reversed(self.stage_events.get(req_id, [])) if evt.stage_id == stage_id),
|
||||
(evt for evt in reversed(self.stage_events.get(rid_key, [])) if evt.stage_id == stage_id),
|
||||
None,
|
||||
)
|
||||
if stage_event is not None and stage_event.final_output_type == "text":
|
||||
output_to_yield.metrics = {
|
||||
"num_tokens_in": stage_event.num_tokens_in,
|
||||
"num_tokens_out": stage_event.num_tokens_out,
|
||||
"stage_id": stage_event.stage_id,
|
||||
"final_output_type": stage_event.final_output_type,
|
||||
}
|
||||
output_to_yield.metrics.update(
|
||||
{
|
||||
defs.NUM_TOKENS_IN: stage_event.num_tokens_in,
|
||||
defs.NUM_TOKENS_OUT: stage_event.num_tokens_out,
|
||||
"stage_id": stage_event.stage_id,
|
||||
"final_output_type": stage_event.final_output_type,
|
||||
}
|
||||
)
|
||||
elif stage_event is not None:
|
||||
output_to_yield.metrics.update(
|
||||
{
|
||||
"stage_id": stage_event.stage_id,
|
||||
"final_output_type": stage_event.final_output_type,
|
||||
}
|
||||
)
|
||||
|
||||
# 5. Finished: record audio generated frames
|
||||
self.record_audio_generated_frames(output_to_yield, stage_id, req_id)
|
||||
@@ -421,6 +454,110 @@ class OrchestratorAggregator:
|
||||
req_id,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _merge_stage_metric_event(
|
||||
current: dict[str, Any] | None,
|
||||
evt: StageRequestStats,
|
||||
) -> dict[str, Any]:
|
||||
sid = int(evt.stage_id) if evt.stage_id is not None else -1
|
||||
if current is None:
|
||||
current = {
|
||||
"stage_id": sid,
|
||||
"final_output_type": evt.final_output_type,
|
||||
defs.NUM_TOKENS_IN: int(evt.num_tokens_in),
|
||||
defs.NUM_TOKENS_OUT: int(evt.num_tokens_out),
|
||||
defs.STAGE_GEN_TIME_MS: float(evt.stage_gen_time_ms),
|
||||
defs.POSTPROCESS_TIME_MS: float(evt.postprocess_time_ms),
|
||||
defs.AUDIO_FRAMES: int(evt.audio_generated_frames),
|
||||
defs.AUDIO_SAMPLE_RATE: int(evt.audio_sample_rate),
|
||||
f"{defs.AUDIO_DURATION}_s": float(evt.audio_duration_s),
|
||||
defs.AUDIO_RTF: float(evt.audio_rtf),
|
||||
defs.IMAGE_PIXELS: int(evt.image_pixels),
|
||||
defs.DENOISE_STEP_LATENCY_MS: float(evt.denoise_step_latency_ms),
|
||||
"output_unit_type": evt.output_unit_type,
|
||||
defs.OUTPUT_UNIT_COUNT: int(evt.output_unit_count),
|
||||
defs.SERVING_TIME_TO_FIRST_OUTPUT_MS: float(evt.serving_time_to_first_output_ms),
|
||||
defs.TIME_PER_OUTPUT_UNIT_MS: float(evt.time_per_output_unit_ms),
|
||||
defs.INTER_OUTPUT_LATENCY_MS: float(evt.inter_output_latency_ms),
|
||||
defs.INTER_OUTPUT_LATENCIES_MS: list(evt.inter_output_latencies_ms or []),
|
||||
defs.VLLM_TTFT_MS: float(evt.vllm_ttft_ms),
|
||||
defs.VLLM_TPOT_MS: float(evt.vllm_tpot_ms),
|
||||
defs.VLLM_ITL_MS: float(evt.vllm_itl_ms),
|
||||
defs.VLLM_ITLS_MS: list(evt.vllm_itls_ms or []),
|
||||
}
|
||||
return current
|
||||
|
||||
current[defs.NUM_TOKENS_IN] = int(current.get(defs.NUM_TOKENS_IN, 0)) + int(evt.num_tokens_in)
|
||||
current[defs.NUM_TOKENS_OUT] = int(current.get(defs.NUM_TOKENS_OUT, 0)) + int(evt.num_tokens_out)
|
||||
current[defs.STAGE_GEN_TIME_MS] = float(current.get(defs.STAGE_GEN_TIME_MS, 0.0)) + float(evt.stage_gen_time_ms)
|
||||
current[defs.POSTPROCESS_TIME_MS] = float(current.get(defs.POSTPROCESS_TIME_MS, 0.0)) + float(
|
||||
evt.postprocess_time_ms
|
||||
)
|
||||
current[defs.AUDIO_FRAMES] = int(current.get(defs.AUDIO_FRAMES, 0)) + int(evt.audio_generated_frames)
|
||||
if int(current.get(defs.AUDIO_SAMPLE_RATE, 0)) <= 0 and int(evt.audio_sample_rate) > 0:
|
||||
current[defs.AUDIO_SAMPLE_RATE] = int(evt.audio_sample_rate)
|
||||
current[f"{defs.AUDIO_DURATION}_s"] = float(current.get(f"{defs.AUDIO_DURATION}_s", 0.0)) + float(
|
||||
evt.audio_duration_s
|
||||
)
|
||||
current[defs.IMAGE_PIXELS] = int(current.get(defs.IMAGE_PIXELS, 0)) + int(evt.image_pixels)
|
||||
denoise_step_latency_ms = float(evt.denoise_step_latency_ms)
|
||||
if denoise_step_latency_ms > 0:
|
||||
current[defs.DENOISE_STEP_LATENCY_MS] = denoise_step_latency_ms
|
||||
current[defs.OUTPUT_UNIT_COUNT] = int(current.get(defs.OUTPUT_UNIT_COUNT, 0)) + int(evt.output_unit_count)
|
||||
|
||||
first_output_ms = float(evt.serving_time_to_first_output_ms)
|
||||
current_first_output_ms = float(current.get(defs.SERVING_TIME_TO_FIRST_OUTPUT_MS, 0.0))
|
||||
if current_first_output_ms <= 0 < first_output_ms:
|
||||
current[defs.SERVING_TIME_TO_FIRST_OUTPUT_MS] = first_output_ms
|
||||
|
||||
if evt.output_unit_type:
|
||||
current["output_unit_type"] = evt.output_unit_type
|
||||
if evt.final_output_type:
|
||||
current["final_output_type"] = evt.final_output_type
|
||||
|
||||
inter_latencies = list(current.get(defs.INTER_OUTPUT_LATENCIES_MS) or [])
|
||||
inter_latencies.extend(list(evt.inter_output_latencies_ms or []))
|
||||
current[defs.INTER_OUTPUT_LATENCIES_MS] = inter_latencies
|
||||
current[defs.INTER_OUTPUT_LATENCY_MS] = (
|
||||
sum(inter_latencies) / float(len(inter_latencies)) if inter_latencies else 0.0
|
||||
)
|
||||
|
||||
vllm_ttft_ms = float(evt.vllm_ttft_ms)
|
||||
current_vllm_ttft_ms = float(current.get(defs.VLLM_TTFT_MS, 0.0))
|
||||
if current_vllm_ttft_ms <= 0 < vllm_ttft_ms:
|
||||
current[defs.VLLM_TTFT_MS] = vllm_ttft_ms
|
||||
vllm_tpot_ms = float(evt.vllm_tpot_ms)
|
||||
if vllm_tpot_ms > 0:
|
||||
current[defs.VLLM_TPOT_MS] = vllm_tpot_ms
|
||||
vllm_itls = list(current.get(defs.VLLM_ITLS_MS) or [])
|
||||
vllm_itls.extend(list(evt.vllm_itls_ms or []))
|
||||
current[defs.VLLM_ITLS_MS] = vllm_itls
|
||||
current[defs.VLLM_ITL_MS] = sum(vllm_itls) / float(len(vllm_itls)) if vllm_itls else 0.0
|
||||
|
||||
output_count = int(current.get(defs.OUTPUT_UNIT_COUNT, 0))
|
||||
remaining_ms = max(
|
||||
float(current.get(defs.STAGE_GEN_TIME_MS, 0.0))
|
||||
- float(current.get(defs.SERVING_TIME_TO_FIRST_OUTPUT_MS, 0.0)),
|
||||
0.0,
|
||||
)
|
||||
current[defs.TIME_PER_OUTPUT_UNIT_MS] = remaining_ms / float(output_count - 1) if output_count > 1 else 0.0
|
||||
duration_s = float(current.get(f"{defs.AUDIO_DURATION}_s", 0.0))
|
||||
current[defs.AUDIO_RTF] = defs.compute_audio_rtf(
|
||||
float(current.get(defs.STAGE_GEN_TIME_MS, 0.0)) / 1000.0, duration_s
|
||||
)
|
||||
return current
|
||||
|
||||
def _build_stage_metrics_snapshot(self, req_id: str) -> dict[str, dict[str, Any]]:
|
||||
"""Aggregate per-stage metrics for ``req_id`` (string key), for streaming/benchmark clients."""
|
||||
snapshot: dict[str, dict[str, Any]] = {}
|
||||
for evt in self.stage_events.get(req_id, []):
|
||||
sid = int(evt.stage_id) if evt.stage_id is not None else -1
|
||||
if sid < 0:
|
||||
continue
|
||||
sid_key = str(sid)
|
||||
snapshot[sid_key] = self._merge_stage_metric_event(snapshot.get(sid_key), evt)
|
||||
return snapshot
|
||||
|
||||
def _as_stage_request_stats(
|
||||
self,
|
||||
stage_id: int,
|
||||
@@ -432,7 +569,8 @@ class OrchestratorAggregator:
|
||||
stats = metrics
|
||||
stats.stage_id = stage_id
|
||||
stats.request_id = req_id
|
||||
stats.final_output_type = final_output_type
|
||||
if final_output_type is not None:
|
||||
stats.final_output_type = final_output_type
|
||||
stats.diffusion_metrics = (
|
||||
{k: int(v) for k, v in self.diffusion_metrics.pop(req_id, {}).items()}
|
||||
if req_id in self.diffusion_metrics
|
||||
|
||||
@@ -134,9 +134,15 @@ def count_tokens_from_outputs(engine_outputs: list[Any]) -> int:
|
||||
try:
|
||||
outs = getattr(_ro, "outputs", None)
|
||||
if outs and len(outs) > 0:
|
||||
tokens = getattr(outs[0], "token_ids", None)
|
||||
if tokens is not None:
|
||||
total += len(tokens)
|
||||
for output in outs:
|
||||
# In DELTA mode token_ids only contains the latest chunk.
|
||||
# Omni's output processor attaches the cumulative sequence
|
||||
# for inter-stage routing and accurate metrics.
|
||||
tokens = getattr(output, "cumulative_token_ids", None)
|
||||
if tokens is None:
|
||||
tokens = getattr(output, "token_ids", None)
|
||||
if tokens is not None:
|
||||
total += len(tokens)
|
||||
except Exception:
|
||||
# Ignore any issues with individual outputs to keep token counting best-effort.
|
||||
pass
|
||||
|
||||
@@ -643,14 +643,70 @@ class CosyVoice3Model(
|
||||
) -> torch.Tensor:
|
||||
if self.model_stage == "cosyvoice3_talker":
|
||||
if is_multimodal is not None and any(is_multimodal):
|
||||
# Per-request rearrange for the talker prompt layout
|
||||
# [SOS_ph, TASK_ID_ph, AUDIO_ph * pstoken_len, ...text_tokens...]
|
||||
# → [SOS_emb, ...text_embs..., TASK_ID_emb, AUDIO_embs].
|
||||
#
|
||||
# In batched prefill ``input_ids`` is the flat concatenation of
|
||||
# N sequences and ``multimodal_embeddings`` is a list with one
|
||||
# tensor per multimodal request. Each request's audio
|
||||
# placeholders form one contiguous ``True`` group in
|
||||
# ``is_multimodal``; the 2 positions immediately before each
|
||||
# group are the SOS / TASK_ID placeholders. Walking the groups
|
||||
# lets us locate every request's segment without needing a
|
||||
# ``seq_token_counts`` kwarg (which only the generation runner
|
||||
# injects, not the AR runner used here).
|
||||
embed_tokens = self.model.llm.model.embed_tokens(input_ids)
|
||||
sos = self.model.speech_embedding.weight[self.model.sos].reshape(1, -1)
|
||||
task_id = self.model.speech_embedding.weight[self.model.task_id].reshape(1, -1)
|
||||
prompt_speech_token_emb = multimodal_embeddings[0]
|
||||
pstoken_len = prompt_speech_token_emb.shape[0] # Get length from tensor shape
|
||||
embed_tokens = torch.cat(
|
||||
[sos, embed_tokens[2 + pstoken_len :], task_id, prompt_speech_token_emb], dim=0
|
||||
)
|
||||
|
||||
is_mm = is_multimodal.to(torch.bool).reshape(-1)
|
||||
# vLLM v1's multimodal placeholder range covers the FULL
|
||||
# ``[SOS_ph, TASK_ID_ph, AUDIO_ph * pstoken_len]`` block
|
||||
# (length ``2 + pstoken_len``, see ``_get_prompt_updates``'s
|
||||
# ``insertion_end`` which inserts ``[1] * (1 + 1 + token_len)``
|
||||
# and ``mm_position.length`` matches that). So each contiguous
|
||||
# ``True`` group in ``is_mm`` starts at a request's SOS
|
||||
# placeholder, NOT at its first audio placeholder.
|
||||
prev_false = torch.cat([torch.ones(1, dtype=torch.bool, device=is_mm.device), ~is_mm[:-1]])
|
||||
group_starts = (is_mm & prev_false).nonzero(as_tuple=True)[0].tolist()
|
||||
if len(group_starts) != len(multimodal_embeddings):
|
||||
raise RuntimeError(
|
||||
f"cosyvoice3 talker: found {len(group_starts)} placeholder "
|
||||
f"blocks in is_multimodal but {len(multimodal_embeddings)} "
|
||||
f"multimodal_embeddings tensors — these must match 1:1."
|
||||
)
|
||||
|
||||
total_tokens = int(embed_tokens.shape[0])
|
||||
# vLLM v1 packs cached (decode) requests first, then new
|
||||
# (prefill) requests. The decode tokens at positions
|
||||
# ``[0:group_starts[0]]`` get a speech_embedding lookup (the
|
||||
# talker is generating codec tokens here, so input ids are
|
||||
# codec ids, not text ids). Positions starting at
|
||||
# ``group_starts[0]`` are the first new prefill request's
|
||||
# SOS placeholder.
|
||||
segments: list[torch.Tensor] = []
|
||||
first_prefill = group_starts[0]
|
||||
if first_prefill > 0:
|
||||
decode_ids = input_ids[:first_prefill].to(dtype=torch.long)
|
||||
segments.append(self.model.speech_embedding.weight[decode_ids])
|
||||
|
||||
for i, req_start in enumerate(group_starts):
|
||||
pstoken_len_i = int(multimodal_embeddings[i].shape[0])
|
||||
# Real text starts after the 2 + pstoken_len_i leading
|
||||
# placeholders of this request's prompt.
|
||||
text_start = req_start + 2 + pstoken_len_i
|
||||
# Next request's segment starts at its own SOS placeholder,
|
||||
# which is exactly the next True group's first index.
|
||||
if i + 1 < len(group_starts):
|
||||
req_end = group_starts[i + 1]
|
||||
else:
|
||||
req_end = total_tokens
|
||||
segments.append(sos)
|
||||
segments.append(embed_tokens[text_start:req_end])
|
||||
segments.append(task_id)
|
||||
segments.append(multimodal_embeddings[i])
|
||||
embed_tokens = torch.cat(segments, dim=0)
|
||||
else:
|
||||
embed_tokens = self.model.speech_embedding.weight[input_ids]
|
||||
return embed_tokens
|
||||
|
||||
@@ -26,7 +26,8 @@ HUNYUAN_IMAGE3_PIPELINE = PipelineConfig(
|
||||
model_stage="AR",
|
||||
execution_type=StageExecutionType.LLM_AR,
|
||||
input_sources=(),
|
||||
final_output=False,
|
||||
final_output=True,
|
||||
final_output_type="text",
|
||||
owns_tokenizer=False,
|
||||
requires_multimodal_data=True,
|
||||
model_arch=_HUNYUAN_IMAGE3_MODEL_ARCH,
|
||||
|
||||
@@ -36,6 +36,7 @@ from vllm.v1.sample.metadata import SamplingMetadata
|
||||
from vllm.v1.sample.sampler import Sampler
|
||||
|
||||
from vllm_omni.data_entry_keys import Embeddings, HiddenStates, Ids, OmniPayload, OmniPayloadMeta
|
||||
from vllm_omni.metrics import definitions as defs
|
||||
from vllm_omni.model_executor.custom_process_mixin import CustomProcessMixin
|
||||
from vllm_omni.model_executor.models.output_templates import OmniOutput
|
||||
from vllm_omni.model_executor.models.qwen3_omni.qwen3_omni_moe_thinker import (
|
||||
@@ -527,9 +528,16 @@ class Qwen3OmniMoeForConditionalGeneration(
|
||||
return OmniOutput(text_hidden_states=talker_hidden, multimodal_outputs=multimodal_outputs)
|
||||
elif self.model_stage == "code2wav":
|
||||
audio_tensors = model_outputs
|
||||
sample_rate = defs.resolve_audio_sample_rate(self.code2wav_config)
|
||||
# `sr` is the audio sample rate metadata consumed by downstream
|
||||
# audio serving and stage-local audio metrics.
|
||||
sr_tensors = [torch.tensor(sample_rate, dtype=torch.int32) for _ in audio_tensors]
|
||||
return OmniOutput(
|
||||
text_hidden_states=None,
|
||||
multimodal_outputs={"model_outputs": [audio_tensor.reshape(1, -1) for audio_tensor in audio_tensors]},
|
||||
multimodal_outputs={
|
||||
"model_outputs": [audio_tensor.reshape(1, -1) for audio_tensor in audio_tensors],
|
||||
"sr": sr_tensors,
|
||||
},
|
||||
)
|
||||
|
||||
return model_outputs
|
||||
|
||||
Reference in new issue
Block a user