[Bugfix]: modify diffusion pipeline profiler result in videos (#2647)

Signed-off-by: bjf-frz <frz123db@gmail.com>
This commit is contained in:
bjf-frz authored and GitHub committed 2026-04-10 16:48:06 +08:00
1 parent c2ae58bb84
commit fbb5dd5794
6 files changed
+158 -30

No files matched your search

+2
View File
@@ -306,6 +306,8 @@ async def async_request_v1_videos(
video_bytes = await content_response.read()
output.response_body = video_bytes
output.success = True
if "stage_durations" in poll_json:
output.stage_durations = poll_json["stage_durations"] or {}
if "peak_memory_mb" in poll_json:
output.peak_memory_mb = poll_json["peak_memory_mb"]
elif "peak_memory_mb" in resp_json:
@@ -34,12 +34,14 @@ pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
class MockVideoResult:
def __init__(self, videos, audios=None, sample_rate=None):
def __init__(self, videos, audios=None, sample_rate=None, stage_durations=None, peak_memory_mb=0.0):
self.multimodal_output = {"video": videos}
if audios is not None:
self.multimodal_output["audio"] = audios
if sample_rate is not None:
self.multimodal_output["audio_sample_rate"] = sample_rate
self.stage_durations = stage_durations or {}
self.peak_memory_mb = peak_memory_mb
class FakeAsyncOmni:
@@ -371,6 +373,33 @@ def test_audio_sample_rate_comes_from_model_config(test_client, mocker: MockerFi
assert audio_sample_rates == [16000]
def test_video_job_persists_profiler_metadata(test_client, mocker: MockerFixture):
engine = test_client.app.state.openai_serving_video._engine_client
async def _generate(prompt, request_id, sampling_params_list):
engine.captured_prompt = prompt
engine.captured_sampling_params_list = sampling_params_list
yield MockVideoResult(
[object()],
stage_durations={"diffuse": 2.5, "vae.decode": 0.3},
peak_memory_mb=4096.5,
)
engine.generate = _generate
mocker.patch(
"vllm_omni.entrypoints.openai.serving_video.encode_video_base64",
return_value="Zg==",
)
response = test_client.post("/v1/videos", data={"prompt": "profile me"})
assert response.status_code == 200
video_id = response.json()["id"]
completed = _wait_for_status(test_client, video_id, VideoGenerationStatus.COMPLETED.value)
assert completed["stage_durations"] == {"diffuse": 2.5, "vae.decode": 0.3}
assert completed["peak_memory_mb"] == 4096.5
def test_missing_handler_returns_503():
app = FastAPI()
app.include_router(router)
@@ -770,6 +799,31 @@ def test_sync_t2v_returns_video_bytes(test_client, mocker: MockerFixture):
assert response.headers["x-request-id"].startswith("video_sync-")
assert response.headers["x-model"] == "Wan-AI/Wan2.2-T2V-A14B-Diffusers"
assert float(response.headers["x-inference-time-s"]) >= 0
assert json.loads(response.headers["x-stage-durations"]) == {}
assert float(response.headers["x-peak-memory-mb"]) == 0.0
def test_sync_t2v_returns_profiler_headers(test_client, mocker: MockerFixture):
engine = test_client.app.state.openai_serving_video._engine_client
async def _generate(prompt, request_id, sampling_params_list):
engine.captured_prompt = prompt
engine.captured_sampling_params_list = sampling_params_list
yield MockVideoResult(
[object()],
stage_durations={"diffuse": 1.75},
peak_memory_mb=1234.25,
)
engine.generate = _generate
_mock_encode_video_bytes(mocker, b"profiled-video")
response = test_client.post("/v1/videos/sync", data={"prompt": "sync profile"})
assert response.status_code == 200
assert response.content == b"profiled-video"
assert json.loads(response.headers["x-stage-durations"]) == {"diffuse": 1.75}
assert float(response.headers["x-peak-memory-mb"]) == pytest.approx(1234.25, rel=0, abs=1e-3)
def test_sync_i2v_returns_video_bytes(test_client, mocker: MockerFixture):
@@ -93,3 +93,24 @@ def test_serve_cli_accepts_ulysses_mode():
assert args.ulysses_mode == "advanced_uaa"
assert parallel_config.ulysses_degree == 4
assert parallel_config.ulysses_mode == "advanced_uaa"
def test_serve_cli_accepts_diffusion_pipeline_profiler_flag():
"""Ensure diffusion serve CLI exposes the profiler switch."""
parser = FlexibleArgumentParser()
subparsers = parser.add_subparsers(dest="command")
OmniServeCommand().subparser_init(subparsers)
args = parser.parse_args(
[
"serve",
"Wan-AI/Wan2.2-T2V-A14B-Diffusers",
"--omni",
"--enable-diffusion-pipeline-profiler",
]
)
stage_cfg = _create_default_diffusion_stage_cfg(args)[0]
assert args.enable_diffusion_pipeline_profiler is True
assert stage_cfg["engine_args"]["enable_diffusion_pipeline_profiler"] is True
+5 -1
View File
@@ -2009,6 +2009,8 @@ async def _run_video_generation_job(
"file_name": file_name,
"completed_at": int(time.time()),
"inference_time_s": time.perf_counter() - started_at,
"stage_durations": response.stage_durations,
"peak_memory_mb": response.peak_memory_mb,
},
)
except Exception as exc:
@@ -2182,7 +2184,7 @@ async def create_video_sync(
request_id = f"video_sync-{random_uuid()}"
started_at = time.perf_counter()
try:
video_bytes = await asyncio.wait_for(
video_bytes, stage_durations, peak_memory_mb = await asyncio.wait_for(
handler.generate_video_bytes(request, request_id, reference_image=reference_image),
timeout=VIDEO_SYNC_TIMEOUT_S,
)
@@ -2208,6 +2210,8 @@ async def create_video_sync(
"X-Request-Id": request_id,
"X-Model": effective_model_name,
"X-Inference-Time-S": f"{inference_time_s:.3f}",
"X-Stage-Durations": json.dumps(stage_durations, separators=(",", ":")),
"X-Peak-Memory-MB": f"{peak_memory_mb:.3f}",
},
)
@@ -201,6 +201,14 @@ class VideoGenerationResponse(BaseModel):
created: int = Field(..., description="Unix timestamp of when the generation completed")
data: list[VideoData] = Field(..., description="Array of generated videos")
stage_durations: dict[str, float] = Field(
default_factory=dict,
description="Profiler stage durations reported by the diffusion pipeline.",
)
peak_memory_mb: float = Field(
default=0.0,
description="Peak device memory usage in MB reported by the diffusion pipeline.",
)
class VideoError(BaseModel):
@@ -250,6 +258,14 @@ class VideoResponse(BaseModel):
description="Filename of the saved output video files for this job.",
)
inference_time_s: float | None = Field(default=None, description="End-to-end inference time in seconds.")
stage_durations: dict[str, float] = Field(
default_factory=dict,
description="Profiler stage durations reported by the diffusion pipeline.",
)
peak_memory_mb: float = Field(
default=0.0,
description="Peak device memory usage in MB reported by the diffusion pipeline.",
)
@property
def file_extension(self) -> str:
+59 -28
View File
@@ -33,6 +33,18 @@ class ReferenceImage:
data: Image.Image
@dataclass
class VideoGenerationArtifacts:
"""Normalized outputs and profiler metadata extracted from one request."""
videos: list[Any]
audios: list[Any | None]
audio_sample_rate: int
output_fps: int
stage_durations: dict[str, float]
peak_memory_mb: float
class OmniOpenAIServingVideo:
"""OpenAI-style video generation handler for omni diffusion models."""
@@ -77,12 +89,8 @@ class OmniOpenAIServingVideo:
reference_id: str,
*,
reference_image: ReferenceImage | None = None,
) -> tuple[list[Any], list[Any | None], int, int]:
"""Run the generation pipeline and extract video/audio outputs.
Returns:
Tuple of (videos, audios, audio_sample_rate, output_fps).
"""
) -> VideoGenerationArtifacts:
"""Run the generation pipeline and extract video/audio/profiler outputs."""
prompt: OmniTextPrompt = OmniTextPrompt(prompt=request.prompt)
if request.negative_prompt is not None:
prompt["negative_prompt"] = request.negative_prompt
@@ -153,7 +161,14 @@ class OmniOpenAIServingVideo:
audios = self._extract_audio_outputs(result, expected_count=len(videos))
audio_sample_rate = self._resolve_audio_sample_rate(result)
output_fps = vp.fps or self._resolve_fps(result) or 24
return videos, audios, audio_sample_rate, output_fps
return VideoGenerationArtifacts(
videos=videos,
audios=audios,
audio_sample_rate=audio_sample_rate,
output_fps=output_fps,
stage_durations=self._extract_stage_durations(result),
peak_memory_mb=self._extract_peak_memory_mb(result),
)
async def generate_videos(
self,
@@ -162,28 +177,31 @@ class OmniOpenAIServingVideo:
*,
reference_image: ReferenceImage | None = None,
) -> VideoGenerationResponse:
videos, audios, audio_sample_rate, output_fps = await self._run_and_extract(
request, reference_id, reference_image=reference_image
)
artifacts = await self._run_and_extract(request, reference_id, reference_image=reference_image)
_t_encode_start = time.perf_counter()
video_data = [
VideoData(
b64_json=(
encode_video_base64(video, fps=output_fps)
if audios[idx] is None
encode_video_base64(video, fps=artifacts.output_fps)
if artifacts.audios[idx] is None
else encode_video_base64(
video,
fps=output_fps,
audio=audios[idx],
audio_sample_rate=audio_sample_rate,
fps=artifacts.output_fps,
audio=artifacts.audios[idx],
audio_sample_rate=artifacts.audio_sample_rate,
)
)
)
for idx, video in enumerate(videos)
for idx, video in enumerate(artifacts.videos)
]
_t_encode_ms = (time.perf_counter() - _t_encode_start) * 1000
logger.info("Video response encoding (MP4+base64): %.2f ms", _t_encode_ms)
return VideoGenerationResponse(created=int(time.time()), data=video_data)
return VideoGenerationResponse(
created=int(time.time()),
data=video_data,
stage_durations=artifacts.stage_durations,
peak_memory_mb=artifacts.peak_memory_mb,
)
async def generate_video_bytes(
self,
@@ -191,25 +209,25 @@ class OmniOpenAIServingVideo:
reference_id: str,
*,
reference_image: ReferenceImage | None = None,
) -> bytes:
) -> tuple[bytes, dict[str, float], float]:
"""Generate a video and return raw MP4 bytes, bypassing base64 encoding."""
videos, audios, audio_sample_rate, output_fps = await self._run_and_extract(
request, reference_id, reference_image=reference_image
)
if len(videos) > 1:
artifacts = await self._run_and_extract(request, reference_id, reference_image=reference_image)
if len(artifacts.videos) > 1:
logger.warning(
"Video request %s generated %d outputs; returning only the first.", reference_id, len(videos)
"Video request %s generated %d outputs; returning only the first.",
reference_id,
len(artifacts.videos),
)
audio = audios[0]
audio = artifacts.audios[0]
_t_encode_start = time.perf_counter()
video_bytes = _encode_video_bytes(
videos[0],
fps=output_fps,
**({"audio": audio, "audio_sample_rate": audio_sample_rate} if audio is not None else {}),
artifacts.videos[0],
fps=artifacts.output_fps,
**({"audio": audio, "audio_sample_rate": artifacts.audio_sample_rate} if audio is not None else {}),
)
_t_encode_ms = (time.perf_counter() - _t_encode_start) * 1000
logger.info("Video response encoding (MP4 bytes): %.2f ms", _t_encode_ms)
return video_bytes
return video_bytes, artifacts.stage_durations, artifacts.peak_memory_mb
@staticmethod
def _apply_lora(lora_body: Any, gen_params: OmniDiffusionSamplingParams) -> None:
@@ -483,3 +501,16 @@ class OmniOpenAIServingVideo:
return None
return sample_rate if sample_rate > 0 else None
@staticmethod
def _extract_stage_durations(result: Any) -> dict[str, float]:
stage_durations = getattr(result, "stage_durations", None)
return stage_durations if isinstance(stage_durations, dict) else {}
@staticmethod
def _extract_peak_memory_mb(result: Any) -> float:
peak_memory_mb = getattr(result, "peak_memory_mb", 0.0)
try:
return float(peak_memory_mb or 0.0)
except (TypeError, ValueError):
return 0.0