[BugFix] Fix Wan2.2-VACE-Fun cache and serving lifecycle (#4667)

Signed-off-by: Ianniu <985683179yzy@gmail.com>
Signed-off-by: david6666666 <530634352@qq.com>
Co-authored-by: Ianniu <985683179yzy@gmail.com>
This commit is contained in:
WeiQing ChenandIanniu authored and GitHub committed 2026-06-25 11:20:58 +08:00
1 parent d612233bef
commit d94108693a
9 files changed
+291 -14

No files matched your search

+1
View File
@@ -39,6 +39,7 @@ th {
| `Cosmos3OmniDiffusersPipeline` | Cosmos3 T2I, T2V, I2V, V2V, T2V with sound, action policy | `nvidia/Cosmos3-Nano`, `nvidia/Cosmos3-Super` | ✅︎ | ✅︎ | ✅︎ | ✅︎ | | `Cosmos3OmniDiffusersPipeline` | Cosmos3 T2I, T2V, I2V, V2V, T2V with sound, action policy | `nvidia/Cosmos3-Nano`, `nvidia/Cosmos3-Super` | ✅︎ | ✅︎ | ✅︎ | ✅︎ |
| `Wan22S2VPipeline` | Wan2.2-S2V | `Wan-AI/Wan2.2-S2V-14B` | ✅︎ | ✅︎ | ✅︎ | ✅︎ | | `Wan22S2VPipeline` | Wan2.2-S2V | `Wan-AI/Wan2.2-S2V-14B` | ✅︎ | ✅︎ | ✅︎ | ✅︎ |
| `Wan22VACEPipeline` | Wan2.1-VACE | `Wan-AI/Wan2.1-VACE-1.3B-diffusers`, `Wan-AI/Wan2.1-VACE-14B-diffusers` | ✅︎ | ✅︎ | ✅︎ | ✅︎ | | `Wan22VACEPipeline` | Wan2.1-VACE | `Wan-AI/Wan2.1-VACE-1.3B-diffusers`, `Wan-AI/Wan2.1-VACE-14B-diffusers` | ✅︎ | ✅︎ | ✅︎ | ✅︎ |
| `Wan22VACEPipeline` | Wan2.2-VACE | `Pyros13/Wan2.2-VACE-Fun-A14B-Diffusers` | ✅︎ | | | |
| `LTX2Pipeline` | LTX-2-T2V | `Lightricks/LTX-2` | ✅︎ | ✅︎ | | | | `LTX2Pipeline` | LTX-2-T2V | `Lightricks/LTX-2` | ✅︎ | ✅︎ | | |
| `LTX2ImageToVideoPipeline` | LTX-2-I2V | `Lightricks/LTX-2` | ✅︎ | ✅︎ | | | | `LTX2ImageToVideoPipeline` | LTX-2-I2V | `Lightricks/LTX-2` | ✅︎ | ✅︎ | | |
| `LTX2TwoStagesPipeline` | LTX-2-T2V | `rootonchair/LTX-2-19b-distilled` | ✅︎ | ✅︎ | | | | `LTX2TwoStagesPipeline` | LTX-2-T2V | `rootonchair/LTX-2-19b-distilled` | ✅︎ | ✅︎ | | |
@@ -0,0 +1,109 @@
# Wan2.2 VACE-Fun Diffusers
> All-in-one video creation and editing with Wan2.2 VACE-Fun A14B in diffusers format.
## Summary
- Vendor: Pyros13
- Model: `Pyros13/Wan2.2-VACE-Fun-A14B-Diffusers`
- Base model: `alibaba-pai/Wan2.2-VACE-Fun-A14B`
- Task: VACE video generation and editing (T2V / I2V / R2V / V2V / inpainting)
- Mode: Offline inference and OpenAI-compatible online serving
- Maintainer: Community
## When To Use This Recipe
Use this recipe to run Wan2.2-VACE-Fun-A14B through the standard diffusers-style
`Wan22VACEPipeline` path in vLLM-Omni. The checkpoint is a diffusers conversion of
`alibaba-pai/Wan2.2-VACE-Fun-A14B`, with `model_index.json`, `transformer/`,
`transformer_2/`, `text_encoder/`, `tokenizer/`, `vae/`, and `scheduler/`
subfolders. Because the model already uses the expected diffusers layout, vLLM-Omni
does not need any original-format key remapping or `.pth` component loaders.
The model is a two-expert architecture. The high-noise `transformer` and low-noise
`transformer_2` are switched by the Wan2.2 boundary during denoising.
## References
- Model card: <https://huggingface.co/Pyros13/Wan2.2-VACE-Fun-A14B-Diffusers>
- Base model card: <https://huggingface.co/alibaba-pai/Wan2.2-VACE-Fun-A14B>
- Related example: [`examples/offline_inference/vace/vace_video_generation.md`](../../examples/offline_inference/vace/vace_video_generation.md)
- Related issue: [vllm-project/vllm-omni#4206](https://github.com/vllm-project/vllm-omni/issues/4206)
- PR: [vllm-project/vllm-omni#4667](https://github.com/vllm-project/vllm-omni/pull/4667)
## Offline Inference
```bash
python examples/offline_inference/vace/vace_video_generation.py \
--model Pyros13/Wan2.2-VACE-Fun-A14B-Diffusers \
--mode i2v --image ./i2v_input.JPG \
--prompt "Summer beach vacation style, a white cat wearing sunglasses sits on a surfboard" \
--height 480 --width 832 --num-frames 81 --num-inference-steps 30 \
--seed 42 --guidance-scale 5.0 \
--vae-use-tiling --enforce-eager \
--output vace_fun_i2v.mp4
```
See [`examples/offline_inference/vace/vace_video_generation.md`](../../examples/offline_inference/vace/vace_video_generation.md)
for other VACE modes and their input flags.
## Online Serving
### Server
```bash
vllm serve Pyros13/Wan2.2-VACE-Fun-A14B-Diffusers --omni \
--model-class-name Wan22VACEPipeline \
--vae-use-tiling \
--enforce-eager \
--port 8091
```
For multi-GPU sequence-parallel serving, add the same parallelism flags you would
use offline, for example `--ulysses-degree 4`. The explicit `--model-class-name`
is optional for this diffusers checkpoint, but keeping it in examples makes the
selected pipeline obvious.
### Client
```bash
no_proxy=127.0.0.1 \
curl -X POST http://127.0.0.1:8091/v1/videos/sync \
-F "prompt=Summer beach vacation style, a white cat wearing sunglasses sits on a surfboard" \
-F "input_reference=@/absolute/path/to/i2v_input.JPG" \
-F "width=832" \
-F "height=480" \
-F "num_frames=81" \
-F "fps=16" \
-F "num_inference_steps=30" \
-F "guidance_scale=5.0" \
--output vace_fun_i2v_serve.mp4
```
For video-to-video or inpainting-style requests, upload the reference video with
`input_reference=@/absolute/path/to/input.mp4` or use `video_reference` with a URL
or JSON-safe data URI.
## Cache-DiT / TaylorSeer Notes
`Wan22VACEPipeline` uses the shared Wan2.2 Cache-DiT enabler for both the
high-noise and low-noise transformers. The adapter intentionally wraps only the
main denoising `blocks`. VACE `vace_blocks` form a conditioning branch: each step
they combine the current latent with the VACE context to produce hints that are
then injected into selected main blocks. Recomputing that branch preserves the
control signal; caching the main backbone still gives the acceleration target.
## Validation
Validated on NVIDIA B300 with `mkt_part_000.mp4`, 736x1280, 61 frames, 30
inference steps, `seed=1`, and `boundary_ratio=0.875`.
The Pyros13 diffusers checkpoint produced a byte-identical MP4 to the
`alibaba-pai/Wan2.2-VACE-Fun-A14B` original-format checkpoint under the same
vLLM-Omni pipeline and inputs:
- SHA256: `2e2db74a612a009b0d6ad6d739e039883f0de84ce5cdf2676324daf78fb05ef8`
- Frame shape: 61 x 736 x 1280
- MAE / RMSE / max pixel diff: 0 / 0 / 0
- PSNR: infinite
- Mean SSIM: 1.0
+32
View File
@@ -42,6 +42,10 @@ SEPARATE_CFG_TRANSFORMERS = [
SAMPLE_CACHE_CONFIG = DiffusionCacheConfig() SAMPLE_CACHE_CONFIG = DiffusionCacheConfig()
def test_wan22_vace_uses_wan22_custom_cache_dit_enabler():
assert cd_backend.CUSTOM_DIT_ENABLERS["Wan22VACEPipeline"] is cd_backend.enable_cache_for_wan22
@pytest.mark.parametrize("transformer_model", SEPARATE_CFG_TRANSFORMERS) @pytest.mark.parametrize("transformer_model", SEPARATE_CFG_TRANSFORMERS)
def test_cache_dit_configs_have_separate_cfg(transformer_model): def test_cache_dit_configs_have_separate_cfg(transformer_model):
"""Check models with separate CFG set has_separate_cfg=True in their configs.""" """Check models with separate CFG set has_separate_cfg=True in their configs."""
@@ -62,6 +66,34 @@ def test_separate_wan22_custom_enabler_has_separate_cfg(mock_cache_dit, mock_blo
assert adapter_kwargs["has_separate_cfg"] is True assert adapter_kwargs["has_separate_cfg"] is True
@pytest.mark.parametrize("has_transformer_2", [False, True])
@patch("vllm_omni.diffusion.cache.cache_dit_backend.BlockAdapter")
@patch("vllm_omni.diffusion.cache.cache_dit_backend.cache_dit")
def test_wan22_custom_enabler_passes_taylorseer_calibrator(
mock_cache_dit,
mock_block_adapter,
has_transformer_2,
):
mock_pipeline = Mock()
mock_pipeline.transformer.blocks = [Mock()]
if has_transformer_2:
mock_pipeline.transformer_2.blocks = [Mock()]
else:
mock_pipeline.transformer_2 = None
cache_config = DiffusionCacheConfig(enable_taylorseer=True, taylorseer_order=1)
cd_backend.enable_cache_for_wan22(mock_pipeline, cache_config)
enable_cache_kwargs = mock_cache_dit.enable_cache.call_args.kwargs
calibrator_config = enable_cache_kwargs["calibrator_config"]
assert calibrator_config is not None
assert calibrator_config.taylorseer_order == 1
adapter_kwargs = mock_block_adapter.call_args.kwargs
for modifier in adapter_kwargs["params_modifiers"]:
assert modifier._context_kwargs["calibrator_config"] is calibrator_config
@patch("vllm_omni.diffusion.cache.cache_dit_backend.BlockAdapter") @patch("vllm_omni.diffusion.cache.cache_dit_backend.BlockAdapter")
@patch("vllm_omni.diffusion.cache.cache_dit_backend.cache_dit") @patch("vllm_omni.diffusion.cache.cache_dit_backend.cache_dit")
def test_cosmos3_cache_dit_wraps_gen_layers(mock_cache_dit, mock_block_adapter): def test_cosmos3_cache_dit_wraps_gen_layers(mock_cache_dit, mock_block_adapter):
@@ -14,6 +14,7 @@ from vllm_omni.diffusion.models.wan2_2.pipeline_wan2_2_vace import (
create_vace_transformer_from_config, create_vace_transformer_from_config,
get_wan22_vace_pre_process_func, get_wan22_vace_pre_process_func,
) )
from vllm_omni.diffusion.models.wan2_2.wan2_2_vace_transformer import WanVACETransformer3DModel
pytestmark = [pytest.mark.core_model, pytest.mark.cpu, pytest.mark.diffusion] pytestmark = [pytest.mark.core_model, pytest.mark.cpu, pytest.mark.diffusion]
@@ -93,6 +94,10 @@ def test_create_vace_transformer_from_config_maps_vace_specific_keys(monkeypatch
} }
def test_vace_transformer_layerwise_offloads_conditioning_blocks() -> None:
assert WanVACETransformer3DModel._layerwise_offload_blocks_attrs == ["vace_blocks", "blocks"]
def test_vace_prepare_masks_encodes_spatial_stride_and_reference_padding() -> None: def test_vace_prepare_masks_encodes_spatial_stride_and_reference_padding() -> None:
pipeline = _make_vace_pipeline() pipeline = _make_vace_pipeline()
mask = torch.ones(1, 3, 5, 16, 16) mask = torch.ones(1, 3, 5, 16, 16)
@@ -11,6 +11,7 @@ import json
import os import os
import threading import threading
import time import time
from contextlib import asynccontextmanager
from types import SimpleNamespace from types import SimpleNamespace
import numpy as np import numpy as np
@@ -109,6 +110,14 @@ class BlockingVideoHandler:
raise raise
class FakeServerSocket:
def __init__(self):
self.closed = False
def close(self):
self.closed = True
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def isolated_video_backends(tmp_path, monkeypatch): def isolated_video_backends(tmp_path, monkeypatch):
"""Use isolated in-memory metadata and local storage for each test.""" """Use isolated in-memory metadata and local storage for each test."""
@@ -121,6 +130,91 @@ def isolated_video_backends(tmp_path, monkeypatch):
return store, tasks, storage return store, tasks, storage
@pytest.mark.asyncio
async def test_server_worker_keeps_engine_alive_until_http_shutdown(monkeypatch):
events: list[str] = []
serve_started = asyncio.Event()
http_shutdown = asyncio.Event()
engine_context_exited = asyncio.Event()
sock = FakeServerSocket()
class FakeEngine:
stage_configs = []
async def get_supported_tasks(self):
return ("generate",)
@asynccontextmanager
async def fake_build_async_omni(*args, **kwargs):
del args, kwargs
events.append("engine_enter")
try:
yield FakeEngine()
finally:
events.append("engine_exit")
engine_context_exited.set()
async def fake_serve_http(*args, **kwargs):
del args, kwargs
events.append("serve_http")
serve_started.set()
async def wait_for_shutdown():
await http_shutdown.wait()
events.append("http_shutdown")
return asyncio.create_task(wait_for_shutdown())
async def fake_storage_start():
events.append("storage_start")
async def fake_get_vllm_config(engine_client):
del engine_client
return None
async def fake_init_app_state(engine_client, state, args):
del engine_client, state, args
events.append("init_app_state")
monkeypatch.setattr(api_server, "build_async_omni", fake_build_async_omni)
monkeypatch.setattr(api_server, "build_openai_app", lambda args, supported_tasks: FastAPI())
monkeypatch.setattr(api_server, "serve_http", fake_serve_http)
monkeypatch.setattr(api_server.STORAGE_MANAGER, "start", fake_storage_start)
monkeypatch.setattr(api_server, "_get_vllm_config", fake_get_vllm_config)
monkeypatch.setattr(api_server, "omni_init_app_state", fake_init_app_state)
monkeypatch.setattr(api_server, "get_uvicorn_log_config", lambda args: None)
args = SimpleNamespace(
tool_parser_plugin="",
reasoning_parser_plugin="",
reasoning_parser=None,
structured_outputs_config=SimpleNamespace(reasoning_parser=None),
enable_ssl_refresh=False,
host="127.0.0.1",
port=0,
uvicorn_log_level="info",
disable_uvicorn_access_log=True,
ssl_keyfile=None,
ssl_certfile=None,
ssl_ca_certs=None,
ssl_cert_reqs=None,
ssl_ciphers=None,
h11_max_incomplete_event_size=None,
h11_max_header_count=None,
)
worker_task = asyncio.create_task(api_server.omni_run_server_worker("127.0.0.1:0", sock, args))
await asyncio.wait_for(serve_started.wait(), timeout=2)
assert not engine_context_exited.is_set()
http_shutdown.set()
await asyncio.wait_for(worker_task, timeout=2)
assert sock.closed
assert events.index("http_shutdown") < events.index("engine_exit")
@pytest.fixture @pytest.fixture
def test_client(): def test_client():
app = FastAPI() app = FastAPI()
+15 -1
View File
@@ -248,20 +248,27 @@ def enable_cache_for_wan22(pipeline: Any, cache_config: Any) -> RefreshCacheCont
""" """
# Build DBCacheConfig with optional SCM support # Build DBCacheConfig with optional SCM support
db_cache_config = _build_db_cache_config(cache_config) db_cache_config = _build_db_cache_config(cache_config)
calibrator_config = _resolve_calibrator_config(cache_config)
if getattr(pipeline, "transformer_2", None) is None: if getattr(pipeline, "transformer_2", None) is None:
logger.info("transformer_2 not found, enabling cache-dit for single transformer mode") logger.info("transformer_2 not found, enabling cache-dit for single transformer mode")
cache_dit.enable_cache( cache_dit.enable_cache(
BlockAdapter( BlockAdapter(
transformer=pipeline.transformer, transformer=pipeline.transformer,
# For VACE, cache only the main denoising blocks. The
# conditioning branch (vace_blocks) has a different forward
# contract and produces per-step hints from the current latent
# plus vace_context; keeping it outside CacheDiT preserves the
# control signal while still accelerating the repeated backbone.
blocks=[pipeline.transformer.blocks], blocks=[pipeline.transformer.blocks],
forward_pattern=[ForwardPattern.Pattern_2], forward_pattern=[ForwardPattern.Pattern_2],
params_modifiers=[ params_modifiers=[
ParamsModifier(cache_config=db_cache_config), ParamsModifier(cache_config=db_cache_config, calibrator_config=calibrator_config),
], ],
has_separate_cfg=True, has_separate_cfg=True,
), ),
cache_config=db_cache_config, cache_config=db_cache_config,
calibrator_config=calibrator_config,
) )
return build_cache_context_refresh(cache_config) return build_cache_context_refresh(cache_config)
@@ -272,6 +279,9 @@ def enable_cache_for_wan22(pipeline: Any, cache_config: Any) -> RefreshCacheCont
pipeline.transformer_2, pipeline.transformer_2,
], ],
blocks=[ blocks=[
# See the single-transformer branch above: VACE conditioning
# blocks are intentionally recomputed each step and are not
# wrapped by CacheDiT's main-block Pattern_2 adapter.
pipeline.transformer.blocks, pipeline.transformer.blocks,
pipeline.transformer_2.blocks, pipeline.transformer_2.blocks,
], ],
@@ -286,17 +296,20 @@ def enable_cache_for_wan22(pipeline: Any, cache_config: Any) -> RefreshCacheCont
max_warmup_steps=cache_config.max_warmup_steps, max_warmup_steps=cache_config.max_warmup_steps,
max_cached_steps=cache_config.max_cached_steps, max_cached_steps=cache_config.max_cached_steps,
), ),
calibrator_config=calibrator_config,
), ),
ParamsModifier( ParamsModifier(
cache_config=DBCacheConfig().reset( cache_config=DBCacheConfig().reset(
max_warmup_steps=2, max_warmup_steps=2,
max_cached_steps=20, max_cached_steps=20,
), ),
calibrator_config=calibrator_config,
), ),
], ],
has_separate_cfg=True, has_separate_cfg=True,
), ),
cache_config=db_cache_config, cache_config=db_cache_config,
calibrator_config=calibrator_config,
) )
refresh_trans_one = build_cache_context_refresh(cache_config) refresh_trans_one = build_cache_context_refresh(cache_config)
@@ -965,6 +978,7 @@ CUSTOM_DIT_ENABLERS.update(
"Wan22Pipeline": enable_cache_for_wan22, "Wan22Pipeline": enable_cache_for_wan22,
"Wan22I2VPipeline": enable_cache_for_wan22, "Wan22I2VPipeline": enable_cache_for_wan22,
"Wan22TI2VPipeline": enable_cache_for_wan22, "Wan22TI2VPipeline": enable_cache_for_wan22,
"Wan22VACEPipeline": enable_cache_for_wan22,
"Wan22S2VPipeline": enable_cache_for_wan22_s2v, "Wan22S2VPipeline": enable_cache_for_wan22_s2v,
"Cosmos3OmniDiffusersPipeline": enable_cache_for_cosmos3, "Cosmos3OmniDiffusersPipeline": enable_cache_for_cosmos3,
} }
@@ -198,6 +198,7 @@ class Wan22VACEPipeline(Wan22Pipeline, SupportImageInput):
attention_kwargs: dict[str, object], attention_kwargs: dict[str, object],
vace_context: torch.Tensor | None, vace_context: torch.Tensor | None,
vace_context_scale: float, vace_context_scale: float,
boundary_timestep: float | None = None,
) -> torch.Tensor: ) -> torch.Tensor:
if attention_kwargs is None: if attention_kwargs is None:
attention_kwargs = {} attention_kwargs = {}
@@ -206,6 +207,11 @@ class Wan22VACEPipeline(Wan22Pipeline, SupportImageInput):
self._current_timestep = t self._current_timestep = t
set_forward_context_denoise_step_idx(step_idx) set_forward_context_denoise_step_idx(step_idx)
if boundary_timestep is not None and t < boundary_timestep and self.transformer_2 is not None:
current_model = self.transformer_2
else:
current_model = self.transformer if self.transformer is not None else self.transformer_2
latent_model_input = latents.to(dtype) latent_model_input = latents.to(dtype)
timestep = t.expand(latents.shape[0]) timestep = t.expand(latents.shape[0])
@@ -219,6 +225,7 @@ class Wan22VACEPipeline(Wan22Pipeline, SupportImageInput):
"vace_context": vace_context, "vace_context": vace_context,
"vace_context_scale": vace_context_scale, "vace_context_scale": vace_context_scale,
"return_dict": False, "return_dict": False,
"current_model": current_model,
} }
negative_kwargs = ( negative_kwargs = (
{ {
@@ -229,6 +236,7 @@ class Wan22VACEPipeline(Wan22Pipeline, SupportImageInput):
"vace_context": vace_context, "vace_context": vace_context,
"vace_context_scale": vace_context_scale, "vace_context_scale": vace_context_scale,
"return_dict": False, "return_dict": False,
"current_model": current_model,
} }
if do_true_cfg if do_true_cfg
else None else None
@@ -598,7 +606,10 @@ class Wan22VACEPipeline(Wan22Pipeline, SupportImageInput):
) )
num_reference_images = 0 num_reference_images = 0
if self.transformer.vace_patch_embedding is not None: # Either expert may be the one loaded (boundary_ratio 0.0/1.0 skips one),
# so read it from whichever transformer exists.
active_transformer = self.transformer if self.transformer is not None else self.transformer_2
if active_transformer.vace_patch_embedding is not None:
video, mask, ref_images_processed = self.preprocess_conditions( video, mask, ref_images_processed = self.preprocess_conditions(
video=source_video, video=source_video,
mask=source_mask, mask=source_mask,
@@ -640,6 +651,19 @@ class Wan22VACEPipeline(Wan22Pipeline, SupportImageInput):
timesteps = self.scheduler.timesteps timesteps = self.scheduler.timesteps
self._num_timesteps = len(timesteps) self._num_timesteps = len(timesteps)
# MoE boundary: only the two-expert checkpoints (Wan2.2 A14B) have a
# transformer_2 to switch to. For single-expert VACE (e.g. Wan2.1) this
# stays None and diffuse() uses the same transformer for every step.
boundary_timestep = None
if self.transformer_2 is not None:
boundary_ratio = (
self.boundary_ratio if self.boundary_ratio is not None else req.sampling_params.boundary_ratio
)
if boundary_ratio is None:
boundary_ratio = 0.875
logger.warning_once("boundary_ratio is required for Wan2.2 VACE generation. using default value 0.875")
boundary_timestep = boundary_ratio * self.scheduler.config.num_train_timesteps
latents = self.diffuse( latents = self.diffuse(
latents=latents, latents=latents,
timesteps=timesteps, timesteps=timesteps,
@@ -650,6 +674,7 @@ class Wan22VACEPipeline(Wan22Pipeline, SupportImageInput):
attention_kwargs=attention_kwargs, attention_kwargs=attention_kwargs,
vace_context=vace_context, vace_context=vace_context,
vace_context_scale=vace_context_scale, vace_context_scale=vace_context_scale,
boundary_timestep=boundary_timestep,
) )
self._current_timestep = None self._current_timestep = None
@@ -81,9 +81,7 @@ class VaceWanTransformerBlock(WanTransformerBlock):
class WanVACETransformer3DModel(WanTransformer3DModel): class WanVACETransformer3DModel(WanTransformer3DModel):
"""VACE-extended WAN Transformer with conditioning blocks for video editing.""" """VACE-extended WAN Transformer with conditioning blocks for video editing."""
# TODO: `vace_blocks` are not layerwise-offloaded yet. The current offloader only _layerwise_offload_blocks_attrs = ["vace_blocks", "blocks"]
# supports a single block group (`blocks`); extend it to support both
# `vace_blocks` and `blocks`.
# Shard hidden_states before VACE blocks (replaces parent's blocks.0) # Shard hidden_states before VACE blocks (replaces parent's blocks.0)
_sp_plan = { _sp_plan = {
+8 -9
View File
@@ -574,15 +574,14 @@ async def omni_run_server_worker(listen_address, sock, args, client_config=None,
**uvicorn_kwargs, **uvicorn_kwargs,
) )
# NB: Await server shutdown only after the backend context is exited try:
try: await shutdown_task
await shutdown_task finally:
finally: state = getattr(app, "state", None)
state = getattr(app, "state", None) serving_speech = getattr(state, "openai_serving_speech", None) if state is not None else None
serving_speech = getattr(state, "openai_serving_speech", None) if state is not None else None if serving_speech is not None:
if serving_speech is not None: serving_speech.shutdown()
serving_speech.shutdown() sock.close()
sock.close()
@asynccontextmanager @asynccontextmanager